graphic design tool
git clone https://git.lucas.co/cce-designer.git
feat: node names are lowercase, as Houdini's are
`sanitize_node_name` lowercases as well as stripping whitespace, so minting
gives `sphere1` and `camera1`, and the load-time migration brings older
saves along. The app's own nodes follow the same rule — the root meta
node's utility subnets are `main`, `view`, `guides` and `render`, and every
lookup names them so — because a path convention with exceptions is two
conventions. The template merge matches an instance to its template
case-insensitively. The versioned project files carry the new names.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
CLAUDE.md | 29 +++++++++-------
default_project.json | 6 ++--
project.json | 6 ++--
src/app.rs | 35 +++++++++++--------
src/dialog.rs | 20 +++++------
src/main.rs | 98 ++++++++++++++++++++++++++--------------------------
src/project.rs | 18 +++++-----
src/window.rs | 4 +--
8 files changed, 114 insertions(+), 102 deletions(-)
diff --git a/CLAUDE.md b/CLAUDE.md
index 8903272..88a4b2a 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -234,7 +234,7 @@ is the introspection surface.
Session-wide settings live under one permanent root node: `meta` (node type
`meta` — retyped/renamed from the old `Session`/`session` on load, children
-intact) contains the Main/View/Guides/Render utility subnets that used to
+intact) contains the main/view/guides/render utility subnets that used to
sit flat in `/`. It is the root network's counterpart of every node's
per-node `meta` child, but still a subnet. `ensure_menubar_subnets` creates
it and MIGRATES older saves into it (root-level settings nodes moved, not
@@ -916,18 +916,23 @@ templates (Sphere/Plane/Extrude) refresh their children's `Code` outright —
values.** A kernel hand-edited inside a template instance reverts on load;
custom kernels belong in bare OpenCL nodes, which the merge never touches.
Native nodes match their template by type, subnet instances by name
-("Sphere3" → "Sphere") plus a full child name/type match; the merge never
+("sphere3" → "Sphere", case-insensitively) plus a full child name/type match; the merge never
injects or deletes children and never rewrites files on disk.
-### Node names carry no whitespace
-
-A node's name is a segment of its path — `/Sphere1/opencl1` is how the
-breadcrumb, the MCP tools and every `Input` wire name it — so names do not
-carry spaces (since 2026-09-21). `sanitize_node_name` (src/app.rs) is the
-rule: the conventional space between a template name and its index goes
-("Sphere 1" → "Sphere1", which is also what minting now produces), any
-other whitespace becomes an underscore ("My Region" → "My_Region"), and
-empty comes back as `node`. It runs at every entry point — minting, the
+### Node names are lowercase and carry no whitespace
+
+A node's name is a segment of its path — `/sphere1/opencl1` is how the
+breadcrumb, the MCP tools and every `Input` wire name it — so names are
+lowercase, as Houdini's are, and carry no spaces (since 2026-09-21).
+`sanitize_node_name` (src/app.rs) is the rule: the conventional space
+between a template name and its index goes ("Sphere 1" → "sphere1", which
+is also what minting now produces), any other whitespace becomes an
+underscore ("My Region" → "my_region"), the whole thing is lowercased, and
+empty comes back as `node`. The app's OWN nodes follow it — the root meta
+node's utility subnets are `main`, `view`, `guides` and `render`, and every
+lookup names them so — because a path convention with exceptions is two
+conventions. The template merge matches an instance to its template
+case-insensitively ("sphere3" → "Sphere"). It runs at every entry point — minting, the
`add_node` name override, `rename_node` — and as a LOAD-TIME MIGRATION on
every load path, `Project::sanitize_node_names`, called before the template
merge in all five places a project is deserialized (the two `load_from_file`
@@ -939,7 +944,7 @@ value was one of the old names (`Input`, `With`, `Rest`, `Target`, `Source`,
`Collider` — any of them, since it matches values rather than a list), and
maps the view state's active camera, the one reference outside the tree. A
sanitized name that lands on a sibling's ("Sphere 1" beside a hand-named
-"Sphere1") steps aside with a `_2` suffix rather than leaving two nodes one
+"sphere1") steps aside with a `_2` suffix rather than leaving two nodes one
name and every wire to them ambiguous.
## Repo hygiene
diff --git a/default_project.json b/default_project.json
index 8a34ce7..eac2d71 100644
--- a/default_project.json
+++ b/default_project.json
@@ -5,7 +5,7 @@
"type": "node",
"children": [
{
- "name": "Camera1",
+ "name": "camera1",
"type": "camera",
"children": [],
"params": [
@@ -46,7 +46,7 @@
]
},
{
- "name": "Sphere1",
+ "name": "sphere1",
"type": "node",
"position": [
4.0,
@@ -137,7 +137,7 @@
]
},
"view_state": {
- "active_camera": "Camera1",
+ "active_camera": "camera1",
"pan": [
0.0,
0.0
diff --git a/project.json b/project.json
index 41346d8..c89250c 100644
--- a/project.json
+++ b/project.json
@@ -5,7 +5,7 @@
"type": "node",
"children": [
{
- "name": "Camera1",
+ "name": "camera1",
"type": "camera",
"children": [],
"params": [
@@ -46,7 +46,7 @@
]
},
{
- "name": "Sphere1",
+ "name": "sphere1",
"type": "node",
"position": [
4.0,
@@ -137,7 +137,7 @@
]
},
"view_state": {
- "active_camera": "Camera1",
+ "active_camera": "camera1",
"pan": [
0.0,
122.9747
diff --git a/src/app.rs b/src/app.rs
index 4e0cdd2..e85e250 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -562,24 +562,28 @@ pub fn meta_pref(node: &FsNode, name: &str) -> bool {
///
/// Matching is conservative: native nodes (group, attribute, scatter, …)
/// match their template by node type exactly; subnet instances match by name
-/// ("Sphere3" → "Sphere" — and "Sphere_3", should someone type one — so a
+/// ("sphere3" → "Sphere" — and "sphere_3", should someone type one — so a
/// renamed instance simply keeps its saved
/// shape), and only merge when EVERY template child is present by name and
/// type — a hand-built subnet that happens to share the name is left alone,
/// and nothing is ever injected or deleted. Simnet children (the user's sim
/// chain) are out of scope by construction: simnet is a native type.
-/// A node name as this app will keep it: no whitespace.
+/// A node name as this app will keep it: lowercase, no whitespace.
///
-/// A node's name is a segment of its path — `/Sphere1/opencl1` is how the
+/// A node's name is a segment of its path — `/sphere1/opencl1` is how the
/// breadcrumb, the MCP tools and every `Input` wire name it — and a path
/// with spaces in it is a path that has to be quoted everywhere it goes.
/// So names do not carry them. The one space that was CONVENTIONAL, the one
/// between a template's name and its index ("Sphere 1"), simply goes, so a
/// migrated save reads like a fresh one; any other whitespace becomes an
-/// underscore, so "My Region" keeps its two words. Empty comes back as
+/// underscore, so "My Region" keeps its two words. And the whole thing is
+/// lowercased, as Houdini names its nodes (`sphere1`, `camera1`) — the
+/// app's own utility nodes included (`/meta/guides`), since a path
+/// convention with exceptions is two conventions. Empty comes back as
/// `node`, since a node with no name has no path at all.
pub fn sanitize_node_name(name: &str) -> String {
- let trimmed = name.trim();
+ let lowered = name.to_lowercase();
+ let trimmed = lowered.trim();
// "Sphere 1" → "Sphere1": drop the whitespace between a base and a
// trailing run of digits.
let digits = trimmed.trim_end_matches(|c: char| c.is_ascii_digit());
@@ -689,9 +693,12 @@ pub fn merge_template_defs(root: &mut FsNode, templates: &[NodeTemplate]) {
.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 == node.name || (!base.is_empty() && t.name == base))
+ && (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| {
@@ -1646,7 +1653,7 @@ impl State {
let main_node = self
.session_node_mut()
- .and_then(|s| s.children.iter_mut().find(|c| c.name == "Main"));
+ .and_then(|s| s.children.iter_mut().find(|c| c.name == "main"));
if let Some(main_node) = main_node {
if let Some(p) = main_node.params.iter_mut().find(|p| p.name == "Open") {
p.options = opts;
@@ -2906,14 +2913,14 @@ impl State {
/// so a command that changed the live state leaves the node agreeing
/// with it. Nothing when the project has no Render node yet.
pub(crate) fn write_render_toggle(&mut self, name: &str, val: bool) {
- self.write_meta_toggle("Render", name, val);
+ self.write_meta_toggle("render", name, val);
}
/// The Guides node's counterpart: what Show Grid, Show Cube and Show
/// Origin write, for the same reason Show Wireframe writes the Render
/// node — see [`State::write_meta_toggle`].
pub(crate) fn write_guides_toggle(&mut self, name: &str, val: bool) {
- self.write_meta_toggle("Guides", name, val);
+ self.write_meta_toggle("guides", name, val);
}
/// Write a toggle's value onto the utility subnet that OWNS it.
@@ -2964,10 +2971,10 @@ impl State {
let dir = self.param_editor_dir_mut();
let Some(child) = dir.children.get_mut(slot_idx) else { return };
let live: &[(&str, bool)] = match child.name.as_str() {
- "Main" => &live_main,
- "View" => &live_view,
- "Guides" => &live_guides,
- "Render" => &live_render,
+ "main" => &live_main,
+ "view" => &live_view,
+ "guides" => &live_guides,
+ "render" => &live_render,
_ => return,
};
for &(name, on) in live {
@@ -5839,7 +5846,7 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
// The Main node owns this one, as Guides owns the guide
// toggles above: without the write, the next parameter edit
// put the pane back the way the node said.
- self.write_meta_toggle("Main", "Circular Pane", val);
+ self.write_meta_toggle("main", "Circular Pane", val);
self.menu_mut(LEFT_MENUBAR_IDX).set_item_checked(2, 2, val);
self.rebuild_positions();
self.apply_layout();
diff --git a/src/dialog.rs b/src/dialog.rs
index b35d870..ff2d113 100644
--- a/src/dialog.rs
+++ b/src/dialog.rs
@@ -903,28 +903,28 @@ impl Setting {
/// table.
pub const SETTINGS: &[Setting] = &[
Setting::section("Viewport"),
- Setting::row("Background Color", Owner::Subnet("Main", "Background Color")),
+ Setting::row("Background Color", Owner::Subnet("main", "Background Color")),
Setting::row("Square Aspect", Owner::Command("toggle_square_viewport")),
Setting::section("Wireframe"),
- Setting::row("Wireframe Color", Owner::Subnet("Render", "Wire Color")),
+ Setting::row("Wireframe Color", Owner::Subnet("render", "Wire Color")),
// The colour applies only in single-colour mode (off, the wires carry
// the geometry's vertex colours and the colour row sets their alpha
// alone) — so the switch sits beside the colour, or a colour set here
// looks ignored.
- Setting::row("Wireframe Single Color", Owner::Subnet("Render", "Wire Single Color")),
+ Setting::row("Wireframe Single Color", Owner::Subnet("render", "Wire Single Color")),
Setting::section("Grid"),
- Setting::row("Show Grid", Owner::Subnet("Guides", "Show Grid Guide")),
- Setting::row("Grid Color", Owner::Subnet("Guides", "Grid Color")),
- Setting::row("Grid Thickness", Owner::Subnet("Guides", "Grid Thickness")),
+ Setting::row("Show Grid", Owner::Subnet("guides", "Show Grid Guide")),
+ Setting::row("Grid Color", Owner::Subnet("guides", "Grid Color")),
+ Setting::row("Grid Thickness", Owner::Subnet("guides", "Grid Thickness")),
Setting::section("Guides"),
- Setting::row("Show Origin Axes", Owner::Subnet("Guides", "Show Origin Axes")),
- Setting::row("Origin Size", Owner::Subnet("Guides", "Origin Guide Size")),
- Setting::row("Show Reference Cube", Owner::Subnet("Guides", "Show Reference Cube")),
+ Setting::row("Show Origin Axes", Owner::Subnet("guides", "Show Origin Axes")),
+ Setting::row("Origin Size", Owner::Subnet("guides", "Origin Guide Size")),
+ Setting::row("Show Reference Cube", Owner::Subnet("guides", "Show Reference Cube")),
Setting::section("Camera"),
Setting::row("Show Camera Pivot", Owner::Command("toggle_camera_pivot")),
Setting::row("Camera Pivot Size", Owner::ActiveCamera("Camera Pivot Size")),
Setting::section("Network"),
- Setting::row("Show Network Plate", Owner::Subnet("View", "Show Network Plate")),
+ Setting::row("Show Network Plate", Owner::Subnet("view", "Show Network Plate")),
];
fn setting_by_label(label: &str) -> Option<&'static Setting> {
diff --git a/src/main.rs b/src/main.rs
index 0e9288c..c92ba68 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -473,29 +473,29 @@ mod tests {
// ----- Node names carry no spaces -----
/// A node's name is a path segment, so the conventional "Sphere 1"
- /// becomes "Sphere1" and any other whitespace an underscore.
+ /// becomes "sphere1" and any other whitespace an underscore, all lowercase.
#[test]
fn node_names_are_sanitized_of_whitespace() {
use crate::app::sanitize_node_name;
- assert_eq!(sanitize_node_name("Sphere 1"), "Sphere1");
- assert_eq!(sanitize_node_name("Camera 12"), "Camera12");
- assert_eq!(sanitize_node_name("Sphere1"), "Sphere1");
- assert_eq!(sanitize_node_name("My Region"), "My_Region");
- assert_eq!(sanitize_node_name(" My Region 2 "), "My_Region2");
+ assert_eq!(sanitize_node_name("Sphere 1"), "sphere1");
+ assert_eq!(sanitize_node_name("Camera 12"), "camera12");
+ assert_eq!(sanitize_node_name("Sphere1"), "sphere1");
+ assert_eq!(sanitize_node_name("My Region"), "my_region");
+ assert_eq!(sanitize_node_name(" My Region 2 "), "my_region2");
assert_eq!(sanitize_node_name("mold\tshell"), "mold_shell");
assert_eq!(sanitize_node_name(""), "node");
assert_eq!(sanitize_node_name(" "), "node");
// Minting and both MCP entry points go through it.
let mut state = State::new(false);
- assert_eq!(state.get_lowest_unused_name("Sphere"), "Sphere2", "Sphere1 is taken by the default project");
+ assert_eq!(state.get_lowest_unused_name("Sphere"), "sphere2", "sphere1 is taken by the default project");
let mut redraw = false;
state.apply_action(crate::app::McpAction::AddNode { template_name: "Plane".into(), name: Some("my plane".into()), x: 5.0, y: 5.0 }, &mut redraw).unwrap();
let slot = state.current_dir().children.iter().position(|c| c.name == "my_plane").expect("the added node, sanitized");
state.apply_action(crate::app::McpAction::RenameNode { slot, new_name: "flat one 3".into() }, &mut redraw).unwrap();
assert_eq!(state.current_dir().children[slot].name, "flat_one3");
state.apply_action(crate::app::McpAction::AddNode { template_name: "Plane".into(), name: None, x: 6.0, y: 6.0 }, &mut redraw).unwrap();
- assert!(state.current_dir().children.iter().any(|c| c.name == "Plane1"), "a minted name has no space");
+ assert!(state.current_dir().children.iter().any(|c| c.name == "plane1"), "a minted name is lowercase with no space");
}
/// Loading an older save renames its nodes and follows every reference:
@@ -507,8 +507,8 @@ mod tests {
let mut proj: Project = serde_json::from_str(&content).unwrap();
// Age the file: put the spaces back, add a consumer wired to the
// sphere by its old name, and a sibling already holding the new one.
- let sphere = proj.root.children.iter().position(|c| c.name == "Sphere1").unwrap();
- let camera = proj.root.children.iter().position(|c| c.name == "Camera1").unwrap();
+ let sphere = proj.root.children.iter().position(|c| c.name == "sphere1").unwrap();
+ let camera = proj.root.children.iter().position(|c| c.name == "camera1").unwrap();
proj.root.children[sphere].name = "Sphere 1".into();
proj.root.children[camera].name = "Camera 1".into();
proj.view_state.active_camera = "Camera 1".into();
@@ -520,7 +520,7 @@ mod tests {
group.params = vec![ParamDef { name: "Input".into(), label: "Input".into(), param_type: "text".into(), default: "Sphere 1".into(), options: vec![], min: None, max: None, step: None, show_when: String::new() }];
let mut clash = group.clone();
clash.id = "c".into();
- clash.name = "Sphere1".into();
+ clash.name = "sphere1".into();
clash.params[0].default = "Camera 1".into();
proj.root.children.push(group);
proj.root.children.push(clash);
@@ -528,16 +528,16 @@ mod tests {
proj.sanitize_node_names();
let names: Vec<&str> = proj.root.children.iter().map(|c| c.name.as_str()).collect();
- assert!(names.contains(&"Camera1"));
- assert!(names.contains(&"My_Region"));
- assert!(names.contains(&"Sphere1"), "the hand-named sibling keeps its name");
- assert!(names.contains(&"Sphere1_2"), "the migrated sphere steps aside from it: {names:?}");
+ assert!(names.contains(&"camera1"));
+ assert!(names.contains(&"my_region"));
+ assert!(names.contains(&"sphere1"), "the hand-named sibling keeps its name");
+ assert!(names.contains(&"sphere1_2"), "the migrated sphere steps aside from it: {names:?}");
let by_name = |n: &str| proj.root.children.iter().find(|c| c.name == n).unwrap();
- assert_eq!(by_name("My_Region").params[0].default, "Sphere1_2", "the wire followed the rename");
- assert_eq!(by_name("Sphere1").params[0].default, "Camera1");
- assert_eq!(proj.view_state.active_camera, "Camera1");
+ assert_eq!(by_name("my_region").params[0].default, "sphere1_2", "the wire followed the rename");
+ assert_eq!(by_name("sphere1").params[0].default, "camera1");
+ assert_eq!(proj.view_state.active_camera, "camera1");
// The template children inside the sphere were never spaced and are untouched.
- assert!(by_name("Sphere1_2").children.iter().any(|c| c.name == "opencl1"));
+ assert!(by_name("sphere1_2").children.iter().any(|c| c.name == "opencl1"));
// A clean file is left exactly alone.
let before = serde_json::to_string(&proj).unwrap();
@@ -777,7 +777,7 @@ mod tests {
/// Session) now; tests that need Main resolve it through there.
fn session_and_main(state: &State) -> (usize, usize) {
let s_idx = state.fs_root.children.iter().position(|c| c.node_type == "meta").expect("root meta node");
- let m_idx = state.fs_root.children[s_idx].children.iter().position(|c| c.name == "Main").expect("Main inside Session");
+ let m_idx = state.fs_root.children[s_idx].children.iter().position(|c| c.name == "main").expect("main inside Session");
(s_idx, m_idx)
}
@@ -828,13 +828,13 @@ mod tests {
assert_eq!(session.name, "meta");
assert!(session.is_enterable(), "the root meta stays a subnet");
let names: Vec<&str> = session.children.iter().map(|c| c.name.as_str()).collect();
- for expected in ["Main", "View", "Guides", "Render"] {
+ for expected in ["main", "view", "guides", "render"] {
assert!(names.contains(&expected), "Session is missing {expected}: {names:?}");
}
// None of the four remain at root.
for c in &state.fs_root.children {
assert!(
- !(c.node_type == "utility" && matches!(c.name.as_str(), "Main" | "View" | "Guides" | "Render")),
+ !(c.node_type == "utility" && matches!(c.name.as_str(), "main" | "view" | "guides" | "render")),
"settings node '{}' still at root", c.name
);
}
@@ -854,7 +854,7 @@ mod tests {
assert_eq!(state.fs_root.children[s_idx].name, "meta");
let names: Vec<&str> =
state.fs_root.children[s_idx].children.iter().map(|c| c.name.as_str()).collect();
- for expected in ["Main", "View", "Guides", "Render"] {
+ for expected in ["main", "view", "guides", "render"] {
assert!(names.contains(&expected), "retype lost {expected}: {names:?}");
}
@@ -862,7 +862,7 @@ mod tests {
// applying the settings drives the overlay size.
{
let guides = state.fs_root.children[s_idx].children.iter_mut()
- .find(|c| c.name == "Guides").unwrap();
+ .find(|c| c.name == "guides").unwrap();
let p = guides.params.iter_mut().find(|p| p.name == "Point Marker Size")
.expect("Guides has Point Marker Size");
assert_eq!(p.default, "20", "default = 0.02 world units");
@@ -902,7 +902,7 @@ mod tests {
let s_idx = state.fs_root.children.iter().position(|c| c.node_type == "meta").unwrap();
let mut session = state.fs_root.children.remove(s_idx);
for mut child in session.children.drain(..) {
- if child.name == "Guides" {
+ if child.name == "guides" {
child.params.push(crate::app::ParamDef {
name: "migration probe".to_string(),
label: String::new(),
@@ -920,7 +920,7 @@ mod tests {
state.ensure_menubar_subnets();
let s_idx = state.fs_root.children.iter().position(|c| c.node_type == "meta").expect("root meta recreated");
- let guides = state.fs_root.children[s_idx].children.iter().find(|c| c.name == "Guides").expect("Guides migrated in");
+ let guides = state.fs_root.children[s_idx].children.iter().find(|c| c.name == "guides").expect("Guides migrated in");
let v = guides.params.iter().find(|p| p.name == "migration probe").map(|p| p.default.as_str());
assert_eq!(v, Some("survived"), "migration recreated Guides instead of moving it");
}
@@ -1002,12 +1002,12 @@ mod tests {
.fs_root
.children
.iter()
- .position(|c| c.name == "Sphere1")
- .expect("default project has Sphere1");
+ .position(|c| c.name == "sphere1")
+ .expect("default project has sphere1");
state.current_path2 = vec![sphere];
state.sync_nodes();
assert!(state.current_path.is_empty(), "primary path must not follow");
- assert_eq!(state.path_names_at(&state.current_path2), vec!["Sphere1".to_string()]);
+ assert_eq!(state.path_names_at(&state.current_path2), vec!["sphere1".to_string()]);
state.current_path2 = vec![99];
state.sync_nodes();
@@ -1031,8 +1031,8 @@ mod tests {
let mut state = State::new(false);
state.add_dock_tab(Dock::Left, NETWORK_PANEL2_IDX);
- let sphere = state.fs_root.children.iter().position(|c| c.name == "Sphere1").unwrap();
- let camera = state.fs_root.children.iter().position(|c| c.name == "Camera1").unwrap();
+ let sphere = state.fs_root.children.iter().position(|c| c.name == "sphere1").unwrap();
+ let camera = state.fs_root.children.iter().position(|c| c.name == "camera1").unwrap();
// Pane 1 selects the sphere; the spreadsheet pins to pane 1.
state.graph_mut().set_selected_node(Some(sphere));
@@ -1102,8 +1102,8 @@ mod tests {
.fs_root
.children
.iter()
- .position(|c| c.name == "Sphere1")
- .expect("default project has Sphere1");
+ .position(|c| c.name == "sphere1")
+ .expect("default project has sphere1");
a.current_path2 = vec![sphere];
a.save_to_file(&dir).expect("save");
@@ -1299,12 +1299,12 @@ mod tests {
let content = fs::read_to_string(&path).expect("failed to read default project");
let proj: Project = serde_json::from_str(&content).expect("failed to deserialize project");
assert_eq!(proj.name, "Default Project");
- assert_eq!(proj.view_state.active_camera, "Camera1");
+ assert_eq!(proj.view_state.active_camera, "camera1");
assert_eq!(proj.root.name, "root");
assert_eq!(proj.root.children.len(), 2);
- assert_eq!(proj.root.children[0].name, "Camera1");
+ assert_eq!(proj.root.children[0].name, "camera1");
assert_eq!(proj.root.children[0].position, (1.0, 1.0));
- assert_eq!(proj.root.children[1].name, "Sphere1");
+ assert_eq!(proj.root.children[1].name, "sphere1");
assert_eq!(proj.root.children[1].position, (4.0, 2.0));
}
@@ -1355,7 +1355,7 @@ mod tests {
use glam::Vec3;
let mut vp = crate::viewport_3d::Viewport3D::new();
let inner = vp.as_any_mut().downcast_mut::<crate::viewport_3d::Viewport3D>().unwrap();
- inner.active_camera = "Camera1".to_string();
+ inner.active_camera = "camera1".to_string();
let pos = Vec3::new(2.5, 1.8, 2.5);
let piv = Vec3::ZERO;
let (_, v1, _) = inner.get_matrices(1.0, Some(pos), Some(Vec3::new(23.62, -58.83, 0.0)), Some(piv));
@@ -4268,7 +4268,7 @@ mod tests {
assert!(shown.iter().any(|(k, _, t)| k == "Wireframe Color" && t == "rgba"), "{shown:?}");
assert!(shown.iter().any(|(k, _, t)| k == "Wireframe Single Color" && t == "toggle"), "{shown:?}");
let s = crate::dialog::SETTINGS.iter().find(|s| s.label == "Wireframe Color").unwrap();
- assert_eq!(s.owner, Some(crate::dialog::Owner::Subnet("Render", "Wire Color")));
+ assert_eq!(s.owner, Some(crate::dialog::Owner::Subnet("render", "Wire Color")));
// Editing both rows reaches the live state: the colour AND the switch
// that makes the wire pass use it (off, the wires carry the
@@ -4294,8 +4294,8 @@ mod tests {
use crate::geometry::Vertex3D;
let mut state = State::new(false);
// The root holds Camera 1; a subnet holds no camera at all.
- state.active_camera = "Camera1".to_string();
- let sub = state.current_dir().children.iter().position(|c| c.name == "Sphere1").expect("Sphere1 at the root");
+ state.active_camera = "camera1".to_string();
+ let sub = state.current_dir().children.iter().position(|c| c.name == "sphere1").expect("sphere1 at the root");
state.current_path.push(sub);
state.on_path_changed();
assert!(!state.current_dir().children.iter().any(|c| c.node_type == "camera"), "no camera in the subnet");
@@ -4371,7 +4371,7 @@ mod tests {
assert_eq!(b.viewport().pivot, Vec3::new(3.0, 0.5, -2.0));
// And the nodes agree with the live state after the load.
let render = b.fs_root.children.iter().find(|c| c.node_type == "meta").unwrap()
- .children.iter().find(|c| c.name == "Render").unwrap();
+ .children.iter().find(|c| c.name == "render").unwrap();
assert_eq!(render.params.iter().find(|p| p.name == "Show Wireframe").unwrap().default, "true");
let _ = std::fs::remove_dir_all(&dir);
}
@@ -4388,13 +4388,13 @@ mod tests {
assert!(!state.wire_single_color);
let render_param = |state: &State, name: &str| -> String {
state.fs_root.children.iter().find(|c| c.node_type == "meta").unwrap()
- .children.iter().find(|c| c.name == "Render").unwrap()
+ .children.iter().find(|c| c.name == "render").unwrap()
.params.iter().find(|p| p.name == name).unwrap().default.clone()
};
// An edit through the node, as the params pane and the dialog make it.
{
let meta = state.fs_root.children.iter_mut().find(|c| c.node_type == "meta").unwrap();
- let render = meta.children.iter_mut().find(|c| c.name == "Render").unwrap();
+ let render = meta.children.iter_mut().find(|c| c.name == "render").unwrap();
render.params.iter_mut().find(|p| p.name == "Wire Color").unwrap().default = "#000000ff".to_string();
}
state.apply_settings_from_menubar_subnets();
@@ -4437,7 +4437,7 @@ mod tests {
.children
.iter()
.find(|c| c.node_type == "meta")
- .and_then(|s| s.children.iter().find(|c| c.name == "Render"))
+ .and_then(|s| s.children.iter().find(|c| c.name == "render"))
.and_then(|n| n.params.iter().find(|p| p.name == "Show Wireframe"))
.map(|p| p.default.clone())
.expect("a Render node with a Show Wireframe toggle")
@@ -5096,7 +5096,7 @@ mod tests {
let view = state.fs_root.children[session]
.children
.iter()
- .position(|c| c.name == "View")
+ .position(|c| c.name == "view")
.expect("View node");
let plate_row = |state: &State| {
state.fs_root.children[session].children[view]
@@ -5135,7 +5135,7 @@ mod tests {
let guides_value = |state: &State, name: &str| -> String {
state
.session_node()
- .and_then(|s| s.children.iter().find(|c| c.name == "Guides"))
+ .and_then(|s| s.children.iter().find(|c| c.name == "guides"))
.and_then(|g| g.params.iter().find(|p| p.name == name))
.map(|p| p.default.clone())
.expect("the Guides param")
@@ -5163,7 +5163,7 @@ mod tests {
// And a real edit through the action path, on an unrelated node.
let mut redraw = false;
- let sphere = state.current_dir().children.iter().position(|c| c.name.starts_with("Sphere")).expect("a sphere");
+ let sphere = state.current_dir().children.iter().position(|c| c.name.starts_with("sphere")).expect("a sphere");
state
.apply_action(crate::app::McpAction::SetParam { slot: sphere, name: "Radius".into(), value: "0.7".into() }, &mut redraw)
.expect("set a sphere param");
@@ -8125,7 +8125,7 @@ mod tests {
.expect("meta")
.children
.iter()
- .find(|c| c.name == "Guides")
+ .find(|c| c.name == "guides")
.expect("Guides");
let p = guides.params.iter().find(|p| p.name == "Show Grid Guide").expect("the param");
assert_eq!(p.default == "true", !was, "and so did its owner");
@@ -8196,7 +8196,7 @@ mod tests {
assert!(!state.dialog_visible(), "the pick closes the dialog");
assert_eq!(state.current_dir().children.len(), before + 1);
let added = state.current_dir().children.last().expect("the new node");
- assert!(added.name.starts_with("Box"), "added {}", added.name);
+ assert!(added.name.starts_with("box"), "added {}", added.name);
assert_eq!(added.position, (3.0, 2.0), "placed at the grid cursor");
}
diff --git a/src/project.rs b/src/project.rs
index a26ee53..384f864 100644
--- a/src/project.rs
+++ b/src/project.rs
@@ -250,7 +250,7 @@ impl State {
root.children
.iter()
.find(|c| c.node_type == "meta")
- .and_then(|m| m.children.iter().find(|c| c.name == "View"))
+ .and_then(|m| m.children.iter().find(|c| c.name == "view"))
.map(|v| {
v.params
.iter()
@@ -672,7 +672,7 @@ impl State {
while idx < self.fs_root.children.len() {
let c = &self.fs_root.children[idx];
if c.node_type == "utility"
- && matches!(c.name.as_str(), "Main" | "View" | "Guides" | "Render")
+ && matches!(c.name.as_str(), "main" | "view" | "guides" | "render")
{
migrated.push(self.fs_root.children.remove(idx));
} else {
@@ -715,7 +715,7 @@ impl State {
let session = &mut self.fs_root.children[session_idx];
// 1. Main subnet
- let main_node = find_or_create_subnet(session, "Main", "utility", (0.0, 0.0));
+ let main_node = find_or_create_subnet(session, "main", "utility", (0.0, 0.0));
main_node.children.clear();
ensure_param(main_node, "File", "section", "", &[], None, None, None);
@@ -905,7 +905,7 @@ impl State {
// syncs this mirror before cloning the tree, and load_from_file reads
// the loaded values (before this refresh clobbers them) and applies
// the diffs via apply_pane_state_from_project.
- let view_node = find_or_create_subnet(&mut self.fs_root.children[session_idx], "View", "utility", (0.0, 2.0));
+ let view_node = find_or_create_subnet(&mut self.fs_root.children[session_idx], "view", "utility", (0.0, 2.0));
view_node.children.clear();
ensure_param(view_node, "Panes", "section", "", &[], None, None, None);
ensure_param(view_node, "Show Network Pane", "toggle", bool_str(show_network), &[], None, None, None);
@@ -955,7 +955,7 @@ impl State {
// migrated off Main). The utility column keeps one empty cell between
// nodes: Main (0,0), View (0,2), Guides (0,4), Render (0,6); older
// saves parked at prior defaults slide to the spaced slots.
- let guides_node = find_or_create_subnet(&mut self.fs_root.children[session_idx], "Guides", "utility", (0.0, 4.0));
+ let guides_node = find_or_create_subnet(&mut self.fs_root.children[session_idx], "guides", "utility", (0.0, 4.0));
if guides_node.position == (0.0, 1.0) || guides_node.position == (0.0, 2.0) {
guides_node.position = (0.0, 4.0);
}
@@ -999,7 +999,7 @@ impl State {
// Main. Toggles reflect live state so a reopened project shows real
// switches. Two rows below Guides (the spaced column); older saves
// parked at the prior defaults slide down.
- let render_node = find_or_create_subnet(&mut self.fs_root.children[session_idx], "Render", "utility", (0.0, 6.0));
+ let render_node = find_or_create_subnet(&mut self.fs_root.children[session_idx], "render", "utility", (0.0, 6.0));
if render_node.position == (0.0, 1.0) || render_node.position == (0.0, 2.0) || render_node.position == (0.0, 4.0) {
render_node.position = (0.0, 6.0);
}
@@ -1117,7 +1117,7 @@ impl State {
.and_then(|s| s.children.iter().find(|c| c.name == name))
.map(|n| n.params.clone())
};
- if let Some(params) = session_params(&self.fs_root, "Guides") {
+ if let Some(params) = session_params(&self.fs_root, "guides") {
for p in ¶ms {
match p.name.as_str() {
"Show Grid Guide" => if let Ok(val) = p.default.parse::<bool>() { self.viewport_mut().show_grid = val; }
@@ -1152,7 +1152,7 @@ impl State {
}
}
}
- if let Some(params) = session_params(&self.fs_root, "Main") {
+ if let Some(params) = session_params(&self.fs_root, "main") {
for p in ¶ms {
match p.name.as_str() {
// Network Settings
@@ -1201,7 +1201,7 @@ impl State {
}
}
- if let Some(params) = session_params(&self.fs_root, "Render") {
+ if let Some(params) = session_params(&self.fs_root, "render") {
let before = self.wire_color;
for p in ¶ms {
match p.name.as_str() {
diff --git a/src/window.rs b/src/window.rs
index 081ea5a..c2cd64a 100644
--- a/src/window.rs
+++ b/src/window.rs
@@ -343,7 +343,7 @@ impl State {
2 => {
state.circular_network_pane = !state.circular_network_pane;
let val = state.circular_network_pane;
- state.write_meta_toggle("Main", "Circular Pane", val);
+ state.write_meta_toggle("main", "Circular Pane", val);
state.menu_mut(LEFT_MENUBAR_IDX).set_item_checked(2, 2, val);
state.rebuild_positions();
state.apply_layout();
@@ -974,7 +974,7 @@ impl State {
McpAction::ToggleCircularPane => {
state.circular_network_pane = !state.circular_network_pane;
let val = state.circular_network_pane;
- state.write_meta_toggle("Main", "Circular Pane", val);
+ state.write_meta_toggle("main", "Circular Pane", val);
state.menu_mut(LEFT_MENUBAR_IDX).set_item_checked(2, 2, val);
state.rebuild_positions();
state.apply_layout();