git.lucas.co / cce-designer
graphic design tool
git clone https://git.lucas.co/cce-designer.git

commita7927d97f1d1d9eaa628ef5132c3623115653968
parent28d6ecc1ad
authorLucas Galante <[email protected]>
date2026-09-19 00:17
feat(network): the plate is optional, so the graph can overlay the scene

The network pane can now drop its PLATE — the filled, frosted surface its
graph sits on — leaving the nodes and wires over the 3D scene. The
viewport is already full-bleed, with the other panes floating over it, so
not drawing the plate is the whole mechanism: what is behind the pane is
the scene.

The pane itself is untouched. It keeps its rect, its focus domain, its
corner menus, its clip and its keyboard navigation; two
append_widget_plate_radii calls are skipped, CONTENT_IDX's (the graph's
own plate) and NETWORK_PANEL_IDX's (the panel behind it). Skipping one
and not the other still leaves a surface, so both are gated on the one
flag. Node bodies keep their blur-behind fill, which is what makes the
result readable — each node frosts the scene behind it while the gaps
stay clear.

Persisted as ViewportSettings::network_plate, beside the viewport
toggles rather than in the project's pane-state list: a pane's VISIBILITY
belongs to the project, but whether its surface is drawn is how you like
to work and should outlive any one file. Reachable three ways that cannot
disagree, because all three run one Action::ToggleNetworkPlate — the View
settings node's Network > Plate row, the network pane's View menu, and
the toggle_network_plate command (Shift+P, rebindable like the rest). The
action marks settings_changed and lets execute_action save once at its
end, the way every other viewport toggle does, instead of writing the
file itself.

The test deliberately does not flip the toggle: that writes the real
~/.config/cce/cce-designer/state.kdl, since tests run with the real HOME,
and a test that rewrote the user's settings as a side effect would be a
worse bug than the one it caught. It asserts the default, the wiring, and
that the View node's row follows the live flag.

Co-Authored-By: Claude Opus 5 <[email protected]>

 CLAUDE.md       | 25 +++++++++++++++++++++++
 src/app.rs      | 44 ++++++++++++++++++++++++++++++++++++++---
 src/command.rs  |  1 +
 src/main.rs     | 61 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 src/project.rs  | 20 +++++++++++++++++++
 src/render.rs   | 11 +++++++++--
 src/shortcut.rs |  2 ++
 7 files changed, 159 insertions(+), 5 deletions(-)

diff --git a/CLAUDE.md b/CLAUDE.md
index 53db2da..4c1ec70 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -443,6 +443,31 @@ The GPU image is owned by `State::page_image` and freed when replaced;
 everything-at-once node, is deliberately not ported: it is these four chained,
 and that collapse is the whole premise of "fifty operators, ten nodes".
 
+### The network plate is optional
+
+The network pane can drop its PLATE — the filled, frosted surface its graph
+sits on — so the nodes and wires overlay the 3D scene directly. The viewport is
+full-bleed (`CANVAS_IDX` covers the window and the other panes float over it),
+so removing the plate is all it takes: what is behind the pane is the scene.
+
+The pane itself is untouched. It keeps its rect, its focus domain, its corner
+menus, its clip and its keyboard navigation; only two `append_widget_plate_radii`
+calls are skipped — `CONTENT_IDX`'s (the graph's own plate) and
+`NETWORK_PANEL_IDX`'s (the panel behind it). Skipping one and not the other
+leaves a surface, so both are gated on the same flag. Node bodies keep their
+blur-behind fill, which is what makes the result legible: they frost the scene
+behind each node while the gaps stay clear.
+
+`ViewportSettings::network_plate` persists it, beside the viewport toggles
+rather than in the project's pane-state list: a pane's VISIBILITY belongs to
+the project, but whether its surface is drawn is how you like to work, and it
+should outlive any one file. It is reachable three ways that cannot disagree,
+because all three run one `Action::ToggleNetworkPlate` — the View settings
+node's Network > Plate row, the network pane's View menu ("Network Plate"), and
+the `toggle_network_plate` command. The action marks `settings_changed` and
+lets `execute_action` save once at its end, like every other viewport toggle,
+rather than writing the file itself.
+
 ### Keyboard graph navigation
 
 The network pane's keyboard scheme is the plugin's, ported: **hjkl rather than
diff --git a/src/app.rs b/src/app.rs
index 93ba77c..310b23b 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -724,6 +724,16 @@ pub struct ViewportSettings {
     pub grid_thickness: f32,
     #[serde(default = "default_grid_color")]
     pub grid_color: [f32; 3],
+    /// Whether the network pane draws its plate. Lives beside the viewport
+    /// settings rather than in a pane-state list because it is an appearance
+    /// choice that outlives any one project — a pane's VISIBILITY belongs to
+    /// the project, but whether its surface is drawn is how you like to work.
+    #[serde(default = "default_network_plate")]
+    pub network_plate: bool,
+}
+
+fn default_network_plate() -> bool {
+    true
 }
 
 impl Default for ViewportSettings {
@@ -736,6 +746,7 @@ impl Default for ViewportSettings {
             show_grid_enabled: true,
             show_cube_enabled: false,
             show_origin_enabled: true,
+            network_plate: true,
             origin_size: 1.0,
             grid_thickness: default_grid_thickness(),
             grid_color: default_grid_color(),
@@ -1178,6 +1189,12 @@ pub struct State {
     pub space_pressed: bool,
     pub active_camera: String,
     pub show_network: bool,
+    /// Whether the network pane draws its PLATE — the filled, frosted surface
+    /// the graph sits on. With it off the nodes and wires overlay the 3D scene
+    /// directly, since the viewport is full-bleed and the network floats over
+    /// it. The pane is still there: it keeps its rect, its focus, its corner
+    /// menus and its clip; only the surface under it stops being drawn.
+    pub network_plate: bool,
     pub show_viewport: bool,
     pub show_parameters: bool,
     pub show_spreadsheet: bool,
@@ -1509,6 +1526,7 @@ impl State {
                 origin_size: self.origin_size,
                 grid_thickness: self.grid_thickness,
                 grid_color: self.viewport().grid_color,
+                network_plate: self.network_plate,
             },
             default_project: self.default_project_setting.clone(),
         };
@@ -2406,6 +2424,11 @@ impl State {
             "Detach Circular Window" | "Detach Pane" => {
                 self.execute_action(Action::DetachCircularWindow);
             }
+            // Both spellings reach the one action: "Show Network Plate" is
+            // the View node's parameter name, "Network Plate" the menu row.
+            "Show Network Plate" | "Network Plate" => {
+                self.execute_action(Action::ToggleNetworkPlate);
+            }
             "Show Network Pane" => {
                 self.show_network = !self.show_network;
                 self.slots.content.set_visible(self.show_network);
@@ -2616,12 +2639,13 @@ impl State {
         }
     }
 
-    fn refresh_main_node_live_toggles(&mut self, slot_idx: usize) {
+    pub(crate) fn refresh_main_node_live_toggles(&mut self, slot_idx: usize) {
         let live_main: [(&str, bool); 2] = [
             ("Circular Pane", self.circular_network_pane),
             ("Ray Traced Preview", self.viewport().rt_mode),
         ];
-        let live_view: [(&str, bool); 5] = [
+        let live_view: [(&str, bool); 6] = [
+            ("Show Network Plate", self.network_plate),
             ("Show Network Pane", self.show_network),
             ("Show Viewport Pane", self.show_viewport),
             ("Show Parameters Pane", self.show_parameters),
@@ -3890,7 +3914,7 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
             splitter2: Splitter::new(SPLITTER_W),
             param: ParametersBg::new(),
             canvas: Canvas::new(),
-            left_menubar: MenuBar::new(0.0, 0.0, 0.0, MENUBAR_H).with_title("0: Network").with_label("Network Menu Bar").with_item("File", &["New", "Save", "Save As"]).with_item("Edit", &["Undo", "Redo"]).with_item("View", &["Zoom In", "Zoom Out", "Circular Pane", "Detach Pane", "Close Pane"]).with_context_options(context_opts.clone(), 0),
+            left_menubar: MenuBar::new(0.0, 0.0, 0.0, MENUBAR_H).with_title("0: Network").with_label("Network Menu Bar").with_item("File", &["New", "Save", "Save As"]).with_item("Edit", &["Undo", "Redo"]).with_item("View", &["Zoom In", "Zoom Out", "Network Plate", "Circular Pane", "Detach Pane", "Close Pane"]).with_context_options(context_opts.clone(), 0),
             right_menubar: MenuBar::new(0.0, 0.0, 0.0, MENUBAR_H).with_title("1: Viewport").with_label("Viewport Menu Bar").with_item("Camera", &["Perspective", "Orthographic"]).with_item("Display", &["Square Aspect"]).with_item("Guides", &["Show Grid", "Cube", "Origin", "Camera Pivot"]).with_item("View", &["Close Pane"]).with_context_options(context_opts.clone(), 1),
             param_menubar: MenuBar::new(0.0, 0.0, 0.0, MENUBAR_H).with_title("2: Parameters").with_label("Parameters Menu Bar").with_item("Preset", &["Default", "Custom"]).with_item("Reset", &["All"]).with_item("View", &["Close Pane"]).with_context_options(context_opts.clone(), 2),
             status: StatusBar::new().with_text("Ready"),
@@ -4076,6 +4100,7 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
             space_pressed: false,
             active_camera,
             show_network: true,
+            network_plate: settings.viewport.network_plate,
             show_viewport: true,
             show_parameters: true,
             show_spreadsheet: false,
@@ -5386,6 +5411,19 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
             Action::NetworkPan(dc, dr) => {
                 self.network_pan_view(dc, dr);
             }
+            Action::ToggleNetworkPlate => {
+                self.network_plate = !self.network_plate;
+                self.rebuild_positions();
+                self.apply_layout();
+                self.update_status_text(if self.network_plate {
+                    "Network plate on."
+                } else {
+                    "Network plate off — the graph overlays the scene."
+                });
+                // Saved through the same `settings_changed` path every other
+                // viewport toggle uses, rather than a save call of its own.
+                settings_changed = true;
+            }
             Action::LayoutNodes => {
                 self.layout_current_level();
             }
diff --git a/src/command.rs b/src/command.rs
index 13c53f2..971068d 100644
--- a/src/command.rs
+++ b/src/command.rs
@@ -146,6 +146,7 @@ pub const COMMANDS: &[Command] = &[
     Command { id: "zoom_in", label: "Zoom In", context: Context::Network, run: Run::Menu("Zoom In"), default_chord: None },
     Command { id: "zoom_out", label: "Zoom Out", context: Context::Network, run: Run::Menu("Zoom Out"), default_chord: None },
     Command { id: "reset_zoom", label: "Reset Zoom", context: Context::Network, run: Run::Menu("Reset Zoom"), default_chord: None },
+    Command { id: "toggle_network_plate", label: "Network Plate", context: Context::Network, run: Run::Key(Action::ToggleNetworkPlate), default_chord: Some("Shift+p") },
     Command { id: "toggle_circular_pane", label: "Circular Pane", context: Context::Network, run: Run::Key(Action::ToggleCircularPane), default_chord: Some("Ctrl+d") },
     Command { id: "detach_circular_window", label: "Detach Circular Window", context: Context::Network, run: Run::Key(Action::DetachCircularWindow), default_chord: None },
 
diff --git a/src/main.rs b/src/main.rs
index fec396f..756fe85 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -4551,6 +4551,67 @@ mod tests {
         assert!(!state.layout_current_level());
     }
 
+    /// The network plate is optional, and the option is reachable three ways
+    /// that cannot disagree: the View settings node's toggle, the network
+    /// pane's View menu, and the command palette.
+    ///
+    /// The toggle is NOT exercised here. Flipping it marks settings dirty and
+    /// `execute_action` then writes `~/.config/cce/cce-designer/state.kdl` —
+    /// the real one, since tests run with the real HOME — so a test that
+    /// toggled it would rewrite the user's own settings as a side effect. What
+    /// is asserted instead is everything around the flip: the default, the
+    /// wiring, and the mirror.
+    #[test]
+    fn test_the_network_plate_is_an_option() {
+        use crate::command::{by_id, Run};
+
+        let state = State::new(false);
+        assert!(state.network_plate, "the plate is on unless the user turned it off");
+
+        // The command exists, is rebindable, and runs the same action the menu
+        // row does — one implementation behind both.
+        let cmd = by_id("toggle_network_plate").expect("no toggle_network_plate command");
+        assert_eq!(cmd.label, "Network Plate");
+        assert_eq!(cmd.run, Run::Key(crate::shortcut::Action::ToggleNetworkPlate));
+        assert!(cmd.default_chord.is_some(), "the plate toggle has no chord");
+
+        // The View settings node mirrors the live flag, so the row in the
+        // params pane shows what is actually on screen.
+        let mut state = State::new(false);
+        let session = state
+            .fs_root
+            .children
+            .iter()
+            .position(|c| c.node_type == "meta")
+            .expect("root meta node");
+        let view = state.fs_root.children[session]
+            .children
+            .iter()
+            .position(|c| c.name == "View")
+            .expect("View node");
+        let plate_row = |state: &State| {
+            state.fs_root.children[session].children[view]
+                .params
+                .iter()
+                .find(|p| p.name == "Show Network Plate")
+                .map(|p| (p.default.clone(), p.label.clone()))
+        };
+        assert_eq!(
+            plate_row(&state),
+            Some(("true".to_string(), "Plate".to_string())),
+            "the View node has no Plate row, or it does not read as on"
+        );
+
+        state.network_plate = false;
+        state.current_path = vec![session];
+        state.refresh_main_node_live_toggles(view);
+        assert_eq!(
+            plate_row(&state).map(|(v, _)| v),
+            Some("false".to_string()),
+            "the View node's row did not follow the live flag"
+        );
+    }
+
     /// A page's raster is its physical size times its resolution — the
     /// property that makes DPI a page parameter rather than an export one.
     #[test]
diff --git a/src/project.rs b/src/project.rs
index 5fed3f5..6293869 100644
--- a/src/project.rs
+++ b/src/project.rs
@@ -529,6 +529,7 @@ impl State {
         let show_parameters = self.show_parameters;
         let show_spreadsheet = self.show_spreadsheet;
         let show_playbar = self.show_playbar;
+        let network_plate = self.network_plate;
         let wireframe = self.wireframe;
         let wire_single_color = self.wire_single_color;
         let wire_color = self.wire_color;
@@ -855,6 +856,20 @@ impl State {
         ensure_param(view_node, "Show Parameters Pane", "toggle", bool_str(show_parameters), &[], None, None, None);
         ensure_param(view_node, "Show Spreadsheet Pane", "toggle", bool_str(show_spreadsheet), &[], None, None, None);
         ensure_param(view_node, "Show Playbar Pane", "toggle", bool_str(show_playbar), &[], None, None, None);
+        // Appearance, not visibility — hence its own section. The pane
+        // toggles above say which panes EXIST; this one says whether the
+        // network draws a surface under its graph or lets the scene through.
+        ensure_param(view_node, "Network", "section", "", &[], None, None, None);
+        ensure_param(
+            view_node,
+            "Show Network Plate",
+            "toggle",
+            bool_str(network_plate),
+            &[],
+            None,
+            None,
+            None,
+        );
         for p in view_node.params.iter_mut() {
             match p.name.as_str() {
                 "Show Network Pane" => set_toggle(p, show_network),
@@ -862,6 +877,7 @@ impl State {
                 "Show Parameters Pane" => set_toggle(p, show_parameters),
                 "Show Spreadsheet Pane" => set_toggle(p, show_spreadsheet),
                 "Show Playbar Pane" => set_toggle(p, show_playbar),
+                "Show Network Plate" => set_toggle(p, network_plate),
                 _ => {}
             }
             // "Show Network Pane" -> "Network": inside the Panes section the
@@ -870,6 +886,10 @@ impl State {
             if p.param_type == "toggle" {
                 if let Some(rest) = p.name.strip_prefix("Show ").and_then(|r| r.strip_suffix(" Pane")) {
                     p.label = rest.to_string();
+                } else if p.name == "Show Network Plate" {
+                    // Under its own "Network" section the row reads as
+                    // "Plate", the same way the pane rows read as their pane.
+                    p.label = "Plate".to_string();
                 }
             }
         }
diff --git a/src/render.rs b/src/render.rs
index 1ed092c..5866dc8 100644
--- a/src/render.rs
+++ b/src/render.rs
@@ -365,7 +365,10 @@ impl State {
             } else {
                 clip
             };
-            if second || !self.circular_network_pane {
+            // The plate is skipped entirely when it is switched off, so the
+            // graph draws straight onto whatever is behind the pane — which,
+            // the viewport being full-bleed, is the 3D scene.
+            if (second || !self.circular_network_pane) && self.network_plate {
                 let (wx, wy, ww2, wh2) = w.rect();
                 append_widget_plate_radii(w, pc, self.plate_focus_tint(idx), self.pane_plate_radii(wx, wy, ww2, wh2));
             }
@@ -523,7 +526,11 @@ impl State {
             }
         } else {
             let (wx, wy, ww2, wh2) = w.rect();
-            append_widget_plate_radii(w, pc, self.plate_focus_tint(idx), self.pane_plate_radii(wx, wy, ww2, wh2));
+            let network_panel =
+                idx == NETWORK_PANEL_IDX || idx == crate::slots::NETWORK_PANEL2_IDX;
+            if !(network_panel && !self.network_plate) {
+                append_widget_plate_radii(w, pc, self.plate_focus_tint(idx), self.pane_plate_radii(wx, wy, ww2, wh2));
+            }
 
             for (qx, qy, qw, qh, qc) in w.extra_quads() {
                 pc.quad(rect(qx, qy, qw, qh), qc);
diff --git a/src/shortcut.rs b/src/shortcut.rs
index 769c6dc..6c4cc5d 100644
--- a/src/shortcut.rs
+++ b/src/shortcut.rs
@@ -40,6 +40,8 @@ pub enum Action {
     FrameAll,
     /// Arrange the current level's nodes from their wiring.
     LayoutNodes,
+    /// Draw the network pane's plate, or let the graph overlay the scene.
+    ToggleNetworkPlate,
 }
 
 #[derive(Debug, Clone)]