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

commitea2586cb1b972bfd5b2825166347574ef469949a
parent772bd1bb31
authorLucas Galante <lsgalante12@gmail.com>
date2026-10-07 00:25
refactor(network): take the network out of the docks

The network has no plate and spans the window, so its place in the left
dock decided nothing that could be seen, except that the spreadsheet
stopped short of the network's invisible dock. It is in no dock now, as
the params HUD is in none:

- The left dock starts empty, and the spreadsheet runs flush left.
- TAB_CANDIDATES is the spreadsheet and the second editor; the network's
  plate menu is Detach alone, and it does not collapse.
- An older save's dock lists drop it, the dock it fronted fronting its
  next tab or emptying.
- The network is laid out over the body directly, and shown whether or
  not it fronts a dock.
- The left dock's edge resizes the plate docked there
  (LeftDockResize), where it was the network's own edge.
- A press on the breadcrumb is asked where the breadcrumb is drawn, not
  in the old dock's top strip, where a press in that band of the scene
  focused the network and went nowhere.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

 CLAUDE.md         |  42 ++++++++----
 src/app.rs        | 161 +++++++++++++++++--------------------------
 src/main.rs       | 199 +++++++++++++++++++++++++++++++++++++-----------------
 src/plate_menu.rs |  21 ++++--
 src/project.rs    |  21 +++---
 5 files changed, 250 insertions(+), 194 deletions(-)

diff --git a/CLAUDE.md b/CLAUDE.md
index 3fce28d..ed8f22e 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -249,7 +249,8 @@ gone from cce-ui with the wgpu path).
   (`PLATE_SLOTS` — network, params, spreadsheet, playbar, network 2; NOT the
   viewport, whose plate is the window-spanning lip) — Collapse/Expand, Detach/
   Reattach, Full Width, the selection pins, the dock's tabs, Add Tab, Move To Own
-  Plate, **Move To Left/Right/Bottom** and Close Tab. `plate_menu_rows(idx)` is the
+  Plate, **Move To Left/Right/Bottom** and Close Tab. The network and the params
+  HUD are in no dock, so theirs are Detach (and the HUD's pins) alone. `plate_menu_rows(idx)` is the
   one list. **There is no corner trigger** (since 2026-10-01; it was a small
   circle on each plate's top-right, which opened these rows as a menu of their
   own and, DRAGGED, moved the pane to another dock — the drag, its drop
@@ -2776,18 +2777,33 @@ What only the plate reached went with it: the network's flat focus ring
 params HUD (`plates_over_params`) and over a point number (`under_a_plate`).
 The SECOND network editor has no plate either; it keeps its dock rect.
 
-The pane itself is untouched. It keeps its focus domain, its menus, its clip
-and its keyboard navigation, and it is still a dock occupant (its plate menu
-still lists tabs and Move To, which change nothing that is seen while it
-spans the window).
+The pane keeps its focus domain, its menus, its clip and its keyboard
+navigation.
+
+**The network is in no dock** (since 2026-10-07), as the params HUD is in
+none. The left dock starts EMPTY (`dock_panes` `[NO_PANE, NO_PANE,
+spreadsheet]`), so the spreadsheet runs flush to the window's left, where
+it stopped short of the network's invisible dock until then;
+`TAB_CANDIDATES` is the spreadsheet and the second editor; the network's
+plate menu is Detach alone (no tab, Move To or Collapse rows), and
+`set_pane_collapsed` refuses it; the pane-state load takes it out of an
+older save's tab lists, the dock it fronted fronting its next tab or
+emptying (`the_params_hud_is_under_the_plates_and_stops_above_the_bottom_ones`
+loads one). `plate_at` never answers the network. The left dock's right edge
+resizes whatever plate is docked there (`on_left_dock_resize_edge`,
+`AppDrag::LeftDockResize`, writing `floating_network_layout.2`, which kept
+its name and its place in the save as the left dock's width) — it was the
+network's own edge (`network_resize_edge_at`, `NetworkResize`). And a press
+on the network's breadcrumb is asked where the breadcrumb is drawn: it was
+asked in the old dock's top strip, so a press in that band of the scene
+focused the network and went nowhere. `the_network_is_in_no_dock`.
 
 **The pane spans the whole window** (`network_overlay`, which is true while
-the network is shown, not circular and not a detached window). The dock rect
-is overridden at its source in `rebuild_positions` — one
-`let (px, py, pw, ph)`, so content, panel and breadcrumb all follow — because
-there is no surface to bound the graph, and one confined to a rectangle you
-cannot see is worse than one that spans what it is drawn over. Its right-edge
-resize hotspot is off for the same reason (`network_resize_edge_at`).
+the network is shown, not circular and not a detached window), laid out in
+`rebuild_positions` as one `let (px, py, pw, ph)` — the body, or nothing
+while hidden — so content, panel and breadcrumb all follow: there is no
+surface to bound the graph, and one confined to a rectangle you cannot see
+is worse than one that spans what it is drawn over.
 
 That makes the pane's RECT useless as a hit test, and these route off it:
 
@@ -2899,8 +2915,8 @@ bottom raised by a spreadsheet tucked under it, tabbable and movable.
   The right dock's own edge is `on_right_dock_resize_edge` /
   `AppDrag::RightDockResize`, asked first, its plate being on top.
 - **Out of the docks.** The right dock starts EMPTY (`dock_panes`
-  `[network, NO_PANE, spreadsheet]`); `TAB_CANDIDATES` no longer lists
-  params, its plate menu has no tab, Move To or Collapse rows (Detach and
+  `[NO_PANE, NO_PANE, spreadsheet]` — the network left the left dock the
+  next day); `TAB_CANDIDATES` no longer lists params, its plate menu has no tab, Move To or Collapse rows (Detach and
   the pins stay), `set_pane_collapsed` refuses it, and the pane-state load
   takes it out of an older save's tab lists — the dock it fronted fronts
   its next tab or empties. A plate moved into the right dock is drawn over
diff --git a/src/app.rs b/src/app.rs
index cb3e91d..f08f582 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -2892,7 +2892,8 @@ pub const NO_PANE: usize = usize::MAX;
 
 #[derive(Clone, Copy, Debug, PartialEq)]
 pub enum AppDrag {
-    NetworkResize { dir: ResizeDirection, start_rect: (f32, f32, f32, f32), start_mouse: (f32, f32) },
+    /// The left dock's right edge — the plate docked there, if any.
+    LeftDockResize { start_w: f32, start_mouse_x: f32 },
     /// The right dock's left edge — the plate docked there, if any.
     RightDockResize { start_w: f32, start_mouse_x: f32 },
     /// The params HUD's left edge: its own width, which no plate shares.
@@ -4667,22 +4668,21 @@ impl State {
     // the press handlers arm the matching `AppDrag` off it, and `pane_resize_cursor`
     // shows the resize cursor over it, so the two can't drift apart.
 
-    /// The floating network pane's edge-resize hotspot at (cx, cy) — only the right
-    /// edge resizes. `None` while the pane is circular or hidden.
-    pub fn network_resize_edge_at(&self, cx: f32, cy: f32) -> Option<ResizeDirection> {
-        // The overlay spans the window and has no edge to drag.
-        if self.circular_network_pane || !self.show_network || self.network_overlay() {
-            return None;
+    /// Whether (cx, cy) is on the left dock's right edge-resize hotspot —
+    /// the edge of whatever plate is docked there. The network is not one
+    /// (since 2026-10-06; it spans the window), so the dock is empty unless
+    /// a plate was moved into it.
+    pub fn on_left_dock_resize_edge(&self, cx: f32, cy: f32) -> bool {
+        if self.circular_network_pane || !self.dock_shown(Dock::Left) {
+            return false;
         }
-        let (fx, fy, _, fh) = self.floating_network_layout;
-        let fw = self.left_dock_width();
-        let margin = 8.0_f32;
-        let on_right = cx >= fx + fw - margin && cx <= fx + fw + margin && cy >= fy - margin && cy <= fy + fh + margin;
-        if on_right {
-            Some(ResizeDirection { left: false, right: true, top: false, bottom: false })
-        } else {
-            None
+        let pane = self.pane_in_dock(Dock::Left);
+        if self.pane_is_stubbed(pane) {
+            return false;
         }
+        let (x, y, w, h) = self.positions[pane];
+        let margin = 8.0_f32;
+        w > 0.0 && cx >= x + w - margin && cx <= x + w + margin && cy >= y - margin && cy <= y + h + margin
     }
 
     /// Whether (cx, cy) is on the params HUD's left edge-resize hotspot:
@@ -4998,16 +4998,15 @@ impl State {
         };
         if let Some(drag) = self.app_drag {
             return Some(match drag {
-                AppDrag::NetworkResize { dir, .. } => dir_cursor(dir),
-                AppDrag::RightDockResize { .. } | AppDrag::HudResize { .. } => CursorIcon::EwResize,
+                AppDrag::LeftDockResize { .. } | AppDrag::RightDockResize { .. } | AppDrag::HudResize { .. } => CursorIcon::EwResize,
                 AppDrag::SpreadsheetResize { .. } => CursorIcon::NsResize,
                 AppDrag::SpreadsheetResizeLeft { .. } | AppDrag::SpreadsheetResizeRight { .. } => {
                     CursorIcon::EwResize
                 }
             });
         }
-        if let Some(dir) = self.network_resize_edge_at(cx, cy) {
-            return Some(dir_cursor(dir));
+        if self.on_left_dock_resize_edge(cx, cy) {
+            return Some(CursorIcon::EwResize);
         }
         if self.on_right_dock_resize_edge(cx, cy) || self.on_param_resize_edge(cx, cy) {
             return Some(CursorIcon::EwResize);
@@ -8790,11 +8789,12 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
             plate_menu_slot: None,
             plate_menu_actions: Vec::new(),
             collapsed_panes: [false; WIDGET_COUNT],
-            // The right dock starts empty: the params HUD is not a dock
-            // pane, and a plate moved there is drawn over it.
-            dock_panes: [NETWORK_PANEL_IDX, NO_PANE, SPREADSHEET_IDX],
+            // The left and right docks start empty: neither the network nor
+            // the params HUD is a dock pane (both live on the scene), and a
+            // plate moved into either is drawn over them.
+            dock_panes: [NO_PANE, NO_PANE, SPREADSHEET_IDX],
             dock_tabs: [
-                vec![NETWORK_PANEL_IDX],
+                Vec::new(),
                 Vec::new(),
                 vec![SPREADSHEET_IDX],
             ],
@@ -9786,18 +9786,17 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
                     }
                 };
 
-                let (px, py, pw, ph) = rect_for(NETWORK_PANEL_IDX, self);
-                // The network has no plate (since 2026-10-06): it is an
-                // overlay on the scene, so it takes the whole body instead of
-                // its dock — there is no surface to bound it, and a graph confined to a
-                // rectangle you cannot see is worse than one that spans what
-                // it is drawn over. Everything below derives from these four
-                // numbers — content, panel, breadcrumb — so overriding them
-                // here keeps the pane's parts agreeing with each other.
-                let (px, py, pw, ph) = if self.network_overlay() {
+                // The network is in no dock (since 2026-10-06): it has no
+                // plate, and is an overlay on the scene spanning the whole
+                // body — there is no surface to bound it, and a graph
+                // confined to a rectangle you cannot see is worse than one
+                // that spans what it is drawn over. Everything below derives
+                // from these four numbers — content, panel, breadcrumb — so
+                // the pane's parts agree with each other.
+                let (px, py, pw, ph) = if self.show_network {
                     (0.0, HEADER_H, self.width, self.body_h())
                 } else {
-                    (px, py, pw, ph)
+                    (0.0, 0.0, 0.0, 0.0)
                 };
 
                 if let Some(menubar) = self.slots.left_menubar.as_any_mut().downcast_mut::<cce_ui::widget::MenuBar>() {
@@ -9894,15 +9893,14 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
                 // is hidden regardless of its View flag — a zero rect alone
                 // does not stop the text pass, so a waiting editor's labels
                 // would paint over whichever pane fronted.
-                let net_active = self.dock_of_pane(NETWORK_PANEL_IDX).is_some();
                 let ss_active = self.dock_of_pane(SPREADSHEET_IDX).is_some();
                 self.slots.header.set_visible(false);
                 self.slots.status.set_visible(false);
                 self.slots.playbar.set_visible(self.show_playbar);
-                self.slots.content.set_visible(self.show_network && net_active);
-                self.slots.network_panel.set_visible(self.show_network && net_active);
+                self.slots.content.set_visible(self.show_network);
+                self.slots.network_panel.set_visible(self.show_network);
                 self.slots.left_menubar.set_visible(false);
-                self.slots.breadcrumb.set_visible(self.show_network && net_active);
+                self.slots.breadcrumb.set_visible(self.show_network);
                 self.slots.splitter1.set_visible(false);
                 self.slots.splitter2.set_visible(false);
                 self.slots.viewport.set_visible(viewport_visible);
@@ -11407,33 +11405,9 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
                 } else {
                     if let Some(drag) = self.app_drag {
                         match drag {
-                            AppDrag::NetworkResize { dir, start_rect, start_mouse } => {
-                                let dx = self.cursor_x - start_mouse.0;
-                                let dy = self.cursor_y - start_mouse.1;
-                                let (sx, sy, sw, sh) = start_rect;
-
-                                let mut fx = sx;
-                                let mut fy = sy;
-                                let mut fw = sw;
-                                let mut fh = sh;
-
-                                if dir.left {
-                                    let new_w = (sw - dx).max(150.0);
-                                    fx = sx + sw - new_w;
-                                    fw = new_w;
-                                } else if dir.right {
-                                    fw = (sw + dx).max(150.0);
-                                }
-
-                                if dir.top {
-                                    let new_h = (sh - dy).max(100.0);
-                                    fy = sy + sh - new_h;
-                                    fh = new_h;
-                                } else if dir.bottom {
-                                    fh = (sh + dy).max(100.0);
-                                }
-
-                                self.floating_network_layout = (fx, fy, fw, fh);
+                            AppDrag::LeftDockResize { start_w, start_mouse_x } => {
+                                let dx = self.cursor_x - start_mouse_x;
+                                self.floating_network_layout.2 = (start_w + dx).max(150.0);
                                 // A drag commits the width it SHOWS: storing an
                                 // overshoot past the window's limit would leave
                                 // the edge dead on the way back until the pointer
@@ -11824,40 +11798,29 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
                             }
                         }
 
+                        // The left dock's edge: the plate docked there, if any.
+                        if *button == MouseButton::Left && self.on_left_dock_resize_edge(self.cursor_x, self.cursor_y) {
+                            self.app_drag = Some(AppDrag::LeftDockResize {
+                                start_w: self.left_dock_width(),
+                                start_mouse_x: self.cursor_x,
+                            });
+                            if let Some(old) = self.focused_widget {
+                                self.slots.get_dyn_mut(old).unfocus();
+                                self.focused_widget = None;
+                            }
+                            return true;
+                        }
+                        // The network's breadcrumb hovers over the graph and
+                        // claims its segments only. Asked where it is drawn:
+                        // until 2026-10-07 this tested the old left dock's
+                        // top strip, so a press in that band of the scene
+                        // focused the network and went nowhere.
                         if *button == MouseButton::Left && !self.circular_network_pane && self.show_network {
-                            let (fx, fy, _, _fh) = self.floating_network_layout;
-                            let fw = self.left_dock_width();
-                            let cx = self.cursor_x;
-                            let cy = self.cursor_y;
-
-                            if let Some(dir) = self.network_resize_edge_at(cx, cy) {
-                                self.app_drag = Some(AppDrag::NetworkResize {
-                                    dir,
-                                    start_rect: (fx, fy, fw, self.floating_network_layout.3),
-                                    start_mouse: (cx, cy),
-                                });
-                                self.focused_pane = LEFT_MENUBAR_IDX;
-                                if let Some(old) = self.focused_widget {
-                                    self.slots.get_dyn_mut(old).unfocus();
-                                    self.focused_widget = None;
-                                }
-                                self.slots.param.unfocus();
-                                self.sync_parameters_to_project();
-                                return true;
-                            } else if cx >= fx && cx < fx + fw && cy >= fy && cy < fy + (if self.show_network { breadcrumb_h() } else { 0.0 }) {
-                                if self.slots.breadcrumb.mouse_input(*button, *btn_state, cx, cy, &mut self.ui_context) {
-                                    return true;
-                                }
-                                // A strip press off the crumbs focuses the pane and consumes.
-                                // The NETWORK_PANEL_IDX drag it used to arm was inert (see the
-                                // circular-border note above).
-                                self.focused_pane = LEFT_MENUBAR_IDX;
-                                if let Some(old) = self.focused_widget {
-                                    self.slots.get_dyn_mut(old).unfocus();
-                                    self.focused_widget = None;
-                                }
-                                self.slots.param.unfocus();
-                                self.sync_parameters_to_project();
+                            let (bx, by, bw, bh) = self.positions[BREADCRUMB_IDX];
+                            let (cx, cy) = (self.cursor_x, self.cursor_y);
+                            if cx >= bx && cx < bx + bw && cy >= by && cy < by + bh
+                                && self.slots.breadcrumb.mouse_input(*button, *btn_state, cx, cy, &mut self.ui_context)
+                            {
                                 return true;
                             }
                         }
@@ -12304,7 +12267,7 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
                             // shared teardown for faithfulness — the pane widget never began
                             // a drag in resize mode, so its commit/cancel hook is a no-op.
                             let idx = match drag {
-                                AppDrag::NetworkResize { .. } => NETWORK_PANEL_IDX,
+                                AppDrag::LeftDockResize { .. } => self.pane_in_dock(Dock::Left),
                                 AppDrag::HudResize { .. } => PARAM_IDX,
                                 AppDrag::RightDockResize { .. } => self.pane_in_dock(Dock::Right),
                                 AppDrag::SpreadsheetResize { .. }
@@ -12320,7 +12283,7 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
                             // offsets need the same refresh.
                             if matches!(
                                 drag,
-                                AppDrag::NetworkResize { .. }
+                                AppDrag::LeftDockResize { .. }
                                     | AppDrag::SpreadsheetResizeLeft { .. }
                             ) {
                                 self.read_panel_offsets();
diff --git a/src/main.rs b/src/main.rs
index d15aca3..03178eb 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -464,25 +464,11 @@ mod tests {
         assert_eq!(state.plate_menu_slot, Some(NETWORK_PANEL_IDX), "turned into the plate's rows");
         assert_eq!((context_menu::x(), context_menu::y()), corner, "where the menu stood");
         assert_eq!(context_menu::back_title().as_deref(), Some("Network"));
-        let options = context_menu::options();
-        assert!(options.iter().any(|o| o.starts_with("Move To")), "{options:?}");
-        // Add Tab is a page off the page, and comes back to it; the page's
-        // band goes back to the network's menu.
-        if let Some(add) = state.plate_menu_actions.iter().position(|a| *a == PlateMenuAction::AddTabMenu) {
-            press_at(&mut state, corner.0 + 8.0, context_menu::row_y(add) + 4.0, MouseButton::Left);
-            assert_eq!(context_menu::back_title().as_deref(), Some(crate::plate_menu::plate_title(NETWORK_PANEL_IDX)));
-            press_at(&mut state, corner.0 + 20.0, corner.1 + context_menu::PAD + context_menu::ROW_H * 0.5, MouseButton::Left);
-            assert_eq!(context_menu::back_title().as_deref(), Some("Network"), "back on the plate page");
-        }
+        // The network is in no dock and does not collapse: Detach alone.
+        assert_eq!(context_menu::options(), vec!["Detach".to_string()]);
         press_at(&mut state, corner.0 + 20.0, corner.1 + context_menu::PAD + context_menu::ROW_H * 0.5, MouseButton::Left);
         assert!(state.network_menu_active && state.plate_menu_slot.is_none(), "back on the network's menu");
-        // Picking Collapse on the page collapses the network plate.
-        let plate = context_menu::options().iter().position(|o| o == "Plate").unwrap();
-        press_at(&mut state, corner.0 + 8.0, context_menu::row_y(plate) + 4.0, MouseButton::Left);
-        let row = context_menu::options().iter().position(|o| *o == collapse).unwrap();
-        press_at(&mut state, corner.0 + 8.0, context_menu::row_y(row) + 4.0, MouseButton::Left);
-        assert!(state.pane_is_collapsed(NETWORK_PANEL_IDX));
-        state.set_pane_collapsed(NETWORK_PANEL_IDX, false);
+        press_at(&mut state, 2.0, 2.0, MouseButton::Left);
 
         // The playbar: its transport, then the Plate page row.
         let (x, y, w, h) = state.positions[PLAYBAR_IDX];
@@ -504,21 +490,26 @@ mod tests {
     fn move_to_swaps_a_pane_into_another_dock() {
         use crate::app::Dock;
         use crate::plate_menu::PlateMenuAction;
-        use crate::slots::{NETWORK_PANEL_IDX, PARAM_IDX, SPREADSHEET_IDX};
+        use crate::slots::{NETWORK_PANEL2_IDX, NETWORK_PANEL_IDX, PARAM_IDX, SPREADSHEET_IDX};
         let mut state = State::new(false);
         state.resize(1600.0, 900.0, 1.0);
         state.show_spreadsheet = true;
+        state.add_dock_tab(Dock::Left, NETWORK_PANEL2_IDX);
         state.open_plate_menu(SPREADSHEET_IDX);
         assert!(state.plate_menu_actions.contains(&PlateMenuAction::MoveTo(Dock::Right)));
         assert!(!state.plate_menu_actions.contains(&PlateMenuAction::MoveTo(Dock::Bottom)), "not to its own dock");
         state.close_plate_menu();
         state.run_plate_menu_action(SPREADSHEET_IDX, PlateMenuAction::MoveTo(Dock::Left), (0.0, 0.0));
         assert_eq!(state.dock_of_pane(SPREADSHEET_IDX), Some(Dock::Left));
-        assert_eq!(state.dock_of_pane(NETWORK_PANEL_IDX), Some(Dock::Bottom));
-        // The params HUD is in no dock and moves to none.
-        state.open_plate_menu(PARAM_IDX);
-        assert!(!state.plate_menu_actions.iter().any(|a| matches!(a, PlateMenuAction::MoveTo(_))));
-        assert_eq!(state.dock_of_pane(PARAM_IDX), None);
+        assert_eq!(state.dock_of_pane(NETWORK_PANEL2_IDX), Some(Dock::Bottom));
+        // Neither the params HUD nor the network is in a dock, and neither
+        // moves to one.
+        for idx in [PARAM_IDX, NETWORK_PANEL_IDX] {
+            state.open_plate_menu(idx);
+            assert!(!state.plate_menu_actions.iter().any(|a| matches!(a, PlateMenuAction::MoveTo(_))));
+            assert_eq!(state.dock_of_pane(idx), None);
+            state.close_plate_menu();
+        }
     }
 
     /// Collapse must actually reclaim the plate AND take its body with it, and
@@ -527,9 +518,14 @@ mod tests {
     #[test]
     fn test_collapse_shrinks_the_plate_and_restores_it() {
         use crate::plate_menu::STUB_H;
-        use crate::slots::{CONTENT_IDX, NETWORK_PANEL_IDX};
+        // The second network editor, docked: the network itself is in no
+        // dock and does not collapse.
+        use crate::slots::{CONTENT2_IDX as CONTENT_IDX, NETWORK_PANEL2_IDX as NETWORK_PANEL_IDX};
         let mut state = State::new(false);
         state.resize(1600.0, 900.0, 1.0);
+        state.set_pane_collapsed(crate::slots::NETWORK_PANEL_IDX, true);
+        assert!(!state.pane_is_collapsed(crate::slots::NETWORK_PANEL_IDX), "the network does not collapse");
+        state.add_dock_tab(crate::app::Dock::Left, NETWORK_PANEL_IDX);
 
         let (_, _, _, full_h) = state.slots.get_dyn(NETWORK_PANEL_IDX).rect();
         assert!(full_h > STUB_H, "network plate starts taller than a stub");
@@ -620,7 +616,7 @@ mod tests {
         assert!(state.network_overlay(), "the network overlays the scene");
         // Spanning the window, it has no edge to drag where its dock's was.
         let (fx, fy, _, fh) = state.floating_network_layout;
-        assert!(state.network_resize_edge_at(fx + state.left_dock_width(), fy + fh * 0.5).is_none());
+        assert!(!state.on_left_dock_resize_edge(fx + state.left_dock_width(), fy + fh * 0.5));
         let press = |state: &mut State, x: f32, y: f32, b: MouseButton| {
             state.handle_event(&WindowEvent::CursorMoved { position: LocalPosition { x: x as f64, y: y as f64 } });
             state.handle_event(&WindowEvent::MouseInput { state: ElementState::Pressed, button: b });
@@ -1133,8 +1129,10 @@ mod tests {
         assert!(!state.spreadsheet_tucks_left() && !state.spreadsheet_tucks_right());
 
         state.set_spreadsheet_full_width(true);
-        assert!(state.spreadsheet_tucks_left() && state.spreadsheet_tucks_right(),
-            "full width must tuck under both neighbors");
+        // Under the neighbour there is: the left dock is empty, the
+        // network being in no dock.
+        assert!(state.spreadsheet_tucks_right() && !state.spreadsheet_tucks_left(),
+            "full width must tuck under the right dock's plate");
         let (ss_x, _, ss_w, _) = state.floating_spreadsheet_rect();
         assert!(ss_x <= 18.5 && ss_x + ss_w >= 1600.0 - 18.5, "not actually full width: x={ss_x} w={ss_w}");
 
@@ -1228,8 +1226,12 @@ mod tests {
         use crate::plate_menu::{PlateMenuAction, PLATE_SLOTS};
         let mut state = State::new(false);
         state.resize(1600.0, 900.0, 1.0);
+        state.execute_menu_action("Show Spreadsheet Pane");
 
+        // A plate that collapses: not the network nor the params HUD,
+        // which are on the scene and not plates of a dock.
         let idx = PLATE_SLOTS.iter().copied()
+            .filter(|&i| i != crate::slots::NETWORK_PANEL_IDX && i != crate::slots::PARAM_IDX)
             .find(|&i| state.slots.get_dyn(i).visible())
             .expect("some plate is shown at this size");
 
@@ -1586,45 +1588,116 @@ mod tests {
         assert!(vs.dock_tabs[1].contains(&"spreadsheet".to_string()));
         assert!(vs.dock_tabs[2].is_empty());
 
-        // Splitting moves the active pane to the empty dock; the tab left
-        // behind fronts.
+        // Splitting moves the active pane to the first empty dock — the
+        // left, which the network does not hold; the tab left behind fronts.
         state.split_dock_tab(NETWORK_PANEL2_IDX);
-        assert_eq!(state.pane_in_dock(Dock::Bottom), NETWORK_PANEL2_IDX);
+        assert_eq!(state.pane_in_dock(Dock::Left), NETWORK_PANEL2_IDX);
         assert_eq!(state.pane_in_dock(Dock::Right), SPREADSHEET_IDX);
     }
 
-    /// Move To Own Plate is offered only while a dock is free to take the
-    /// pane. With the params pane out of the docks, three tab candidates
-    /// share three docks, so a dock holding two always leaves one empty —
-    /// the row is offered, and the split lands there.
+    /// The network is in no dock (since 2026-10-07): it spans the body
+    /// whatever the docks hold, the left dock starts empty — so the
+    /// spreadsheet runs flush to the left — and the network neither
+    /// collapses nor moves. The left dock's edge resizes the plate docked
+    /// there, and a press in the band the network's dock had along the
+    /// top, off the breadcrumb's segments, is the scene's.
+    #[test]
+    fn the_network_is_in_no_dock() {
+        use crate::app::{Dock, NO_PANE, HEADER_H};
+        use crate::plate_menu::PlateMenuAction;
+        use crate::slots::{BREADCRUMB_IDX, LEFT_MENUBAR_IDX, NETWORK_PANEL2_IDX, NETWORK_PANEL_IDX, SPREADSHEET_IDX};
+        use crate::window::{LocalPosition, WindowEvent};
+        use cce_ui::widget::{ElementState, MouseButton};
+        let mut state = State::new(false);
+        state.resize(1600.0, 900.0, 1.0);
+        state.execute_menu_action("Show Spreadsheet Pane");
+        assert_eq!(state.tab_dock_of_pane(NETWORK_PANEL_IDX), None);
+        assert_eq!(state.pane_in_dock(Dock::Left), NO_PANE);
+        assert_eq!(state.positions[NETWORK_PANEL_IDX], (0.0, HEADER_H, 1600.0, state.body_h()));
+        assert!(state.slots.get_dyn(crate::slots::CONTENT_IDX).visible(), "shown though it fronts no dock");
+        assert_eq!(state.positions[SPREADSHEET_IDX].0, 18.0, "flush left, a gap in");
+
+        state.set_pane_collapsed(NETWORK_PANEL_IDX, true);
+        assert!(!state.pane_is_collapsed(NETWORK_PANEL_IDX));
+        state.move_pane_to_dock(NETWORK_PANEL_IDX, Dock::Bottom);
+        assert_eq!(state.tab_dock_of_pane(NETWORK_PANEL_IDX), None);
+        state.open_plate_menu(NETWORK_PANEL_IDX);
+        assert_eq!(state.plate_menu_actions, vec![PlateMenuAction::Detach]);
+        state.close_plate_menu();
+
+        let at = |state: &mut State, x: f32, y: f32| {
+            state.handle_event(&WindowEvent::CursorMoved { position: LocalPosition { x: x as f64, y: y as f64 } });
+        };
+        let press = |state: &mut State, s: ElementState| {
+            state.handle_event(&WindowEvent::MouseInput { state: s, button: MouseButton::Left });
+        };
+
+        // The old dock's top band, off the crumbs and off every node: the
+        // scene's, where it used to focus the network and go nowhere.
+        let (bx, by, _, bh) = state.positions[BREADCRUMB_IDX];
+        let y = by + bh * 0.5;
+        let x = (0..40)
+            .map(|k| bx + 200.0 + k as f32 * 5.0)
+            .find(|&x| {
+                !state.slots.get_dyn(BREADCRUMB_IDX).hit_test(x, y, &state.ui_context)
+                    && state.graph().node_at(x, y).is_none()
+                    && !state.over_floating_pane_at(x, y)
+            })
+            .expect("a point in the band off the crumbs");
+        assert!(x < 18.0 + 400.0, "inside the old dock's band");
+        state.focused_pane = crate::slots::RIGHT_MENUBAR_IDX;
+        at(&mut state, x, y);
+        press(&mut state, ElementState::Pressed);
+        assert!(state.orbit_drag.is_some(), "the press orbits");
+        press(&mut state, ElementState::Released);
+        assert_ne!(state.focused_pane, LEFT_MENUBAR_IDX);
+
+        // A plate in the left dock: its right edge resizes the dock.
+        state.add_dock_tab(Dock::Left, NETWORK_PANEL2_IDX);
+        let (qx, qy, qw, qh) = state.positions[NETWORK_PANEL2_IDX];
+        assert!(qw > 0.0);
+        let w0 = state.left_dock_width();
+        let edge = (qx + qw, qy + qh * 0.5);
+        assert!(state.on_left_dock_resize_edge(edge.0, edge.1));
+        at(&mut state, edge.0, edge.1);
+        press(&mut state, ElementState::Pressed);
+        at(&mut state, edge.0 + 40.0, edge.1);
+        press(&mut state, ElementState::Released);
+        assert!((state.left_dock_width() - (w0 + 40.0)).abs() < 0.5, "{} against {}", state.left_dock_width(), w0 + 40.0);
+        assert_eq!(state.positions[NETWORK_PANEL2_IDX].2, state.left_dock_width());
+    }
+
+    /// Move To Own Plate is offered only on a dock holding more than one
+    /// tab. With neither the params pane nor the network in the docks, the
+    /// two tab candidates share three docks, so a dock holding both always
+    /// leaves another empty — the row is offered, and the split lands there.
     #[test]
     fn move_to_own_plate_needs_an_empty_dock() {
         use crate::app::Dock;
         use crate::plate_menu::PlateMenuAction;
-        use crate::slots::{NETWORK_PANEL2_IDX, NETWORK_PANEL_IDX};
+        use crate::slots::{NETWORK_PANEL2_IDX, SPREADSHEET_IDX};
         let mut state = State::new(false);
         state.resize(1600.0, 900.0, 1.0);
+        state.show_spreadsheet = true;
 
-        // Every dock held, none shared: no split to offer.
-        state.add_dock_tab(Dock::Right, NETWORK_PANEL2_IDX);
-        assert_eq!(state.first_empty_dock(), None);
-        state.open_plate_menu(NETWORK_PANEL_IDX);
+        // A dock of its own: no split to offer.
+        state.open_plate_menu(SPREADSHEET_IDX);
         assert!(!state.plate_menu_actions.contains(&PlateMenuAction::SplitTab), "its dock is its own");
         state.close_plate_menu();
 
-        // The second editor tabs in beside the first: the right dock frees,
-        // and the row is offered on the shared dock.
-        state.add_dock_tab(Dock::Left, NETWORK_PANEL2_IDX);
-        state.show_dock_tab(Dock::Left, NETWORK_PANEL_IDX);
-        assert_eq!(state.first_empty_dock(), Some(Dock::Right));
-        state.open_plate_menu(NETWORK_PANEL_IDX);
+        // The second editor tabs in beside the spreadsheet: the row is
+        // offered on the shared dock.
+        state.add_dock_tab(Dock::Bottom, NETWORK_PANEL2_IDX);
+        state.show_dock_tab(Dock::Bottom, SPREADSHEET_IDX);
+        assert_eq!(state.first_empty_dock(), Some(Dock::Left));
+        state.open_plate_menu(SPREADSHEET_IDX);
         assert!(state.plate_menu_actions.contains(&PlateMenuAction::SplitTab));
         assert!(state.plate_menu_actions.contains(&PlateMenuAction::ShowTab(NETWORK_PANEL2_IDX)));
         state.close_plate_menu();
 
         // And the split lands there.
-        state.split_dock_tab(NETWORK_PANEL_IDX);
-        assert_eq!(state.pane_in_dock(Dock::Right), NETWORK_PANEL_IDX);
+        state.split_dock_tab(SPREADSHEET_IDX);
+        assert_eq!(state.pane_in_dock(Dock::Left), SPREADSHEET_IDX);
     }
 
     /// The second network editor: joins a dock from nowhere through the tab
@@ -1639,7 +1712,7 @@ mod tests {
 
         state.add_dock_tab(Dock::Left, NETWORK_PANEL2_IDX);
         assert_eq!(state.pane_in_dock(Dock::Left), NETWORK_PANEL2_IDX);
-        assert_eq!(state.tab_dock_of_pane(NETWORK_PANEL_IDX), Some(Dock::Left));
+        assert_eq!(state.tab_dock_of_pane(NETWORK_PANEL_IDX), None, "the network is in no dock");
 
         let start = state.current_path.clone();
         let sphere = state
@@ -1659,7 +1732,7 @@ mod tests {
 
         state.close_dock_tab(NETWORK_PANEL2_IDX);
         assert_eq!(state.tab_dock_of_pane(NETWORK_PANEL2_IDX), None);
-        assert_eq!(state.pane_in_dock(Dock::Left), NETWORK_PANEL_IDX);
+        assert_eq!(state.pane_in_dock(Dock::Left), crate::app::NO_PANE);
     }
 
 
@@ -1749,8 +1822,8 @@ mod tests {
         assert!(!a.collapsed_panes[PARAM_IDX], "the params HUD does not collapse");
         a.splitter_layout.splitter1_x = 400.0;
         a.splitter_layout.splitter2_x = 1200.0;
-        // Tab state: a second network editor tabbed beside the first (and
-        // fronted), dived one level down its own path.
+        // Tab state: a second network editor in the left dock (which the
+        // network does not hold), dived one level down its own path.
         a.add_dock_tab(crate::app::Dock::Left, crate::slots::NETWORK_PANEL2_IDX);
         let sphere = a
             .current_dir()
@@ -1772,17 +1845,13 @@ mod tests {
             "splitters restore as fractions: 400/1600 of an 800-wide window = 200, got {}",
             b.splitter_layout.splitter1_x);
         // The tab arrangement rides the file: the second editor exists,
-        // fronted in the left dock with the primary waiting, on its own path.
+        // fronted in the left dock, on its own path; the network is in none.
         assert_eq!(
             b.pane_in_dock(crate::app::Dock::Left),
             crate::slots::NETWORK_PANEL2_IDX,
             "the fronted second editor must load fronted"
         );
-        assert_eq!(
-            b.tab_dock_of_pane(crate::slots::NETWORK_PANEL_IDX),
-            Some(crate::app::Dock::Left),
-            "the primary must load as the waiting tab"
-        );
+        assert_eq!(b.tab_dock_of_pane(crate::slots::NETWORK_PANEL_IDX), None, "the network loads in no dock");
         assert_eq!(b.current_path2, [a.current_path.clone(), vec![sphere]].concat(), "the second editor's path must round-trip");
 
         // A detached pane window must ignore the same file's pane state.
@@ -1807,7 +1876,8 @@ mod tests {
         a.resize(1600.0, 900.0, 1.0);
         a.execute_menu_action("Show Spreadsheet Pane");
         assert!(a.show_spreadsheet);
-        a.add_dock_tab(crate::app::Dock::Right, crate::slots::NETWORK_PANEL2_IDX);
+        // A plate in the left dock to tuck under (the network is in none).
+        a.add_dock_tab(crate::app::Dock::Left, crate::slots::NETWORK_PANEL2_IDX);
         a.floating_network_layout.2 = 520.0;
         a.floating_param_width = 360.0;
         a.params_hud_width = 420.0;
@@ -1831,7 +1901,7 @@ mod tests {
         assert!((b.floating_spreadsheet_height - 300.0).abs() < 0.5, "spreadsheet height: {}", b.floating_spreadsheet_height);
         assert!((b.floating_spreadsheet_inset_left - insets.0).abs() < 0.5 && (b.floating_spreadsheet_inset_right - insets.1).abs() < 0.5,
             "tucks: {:?} vs {:?}", (b.floating_spreadsheet_inset_left, b.floating_spreadsheet_inset_right), insets);
-        assert!(b.spreadsheet_tucks_left() && b.spreadsheet_tucks_right(), "the full-width tuck must load tucked");
+        assert!(b.spreadsheet_tucks_left(), "the full-width tuck must load tucked");
 
         // Half the window: the same fractions land at half the pixels.
         let mut c = State::new(false);
@@ -1913,7 +1983,7 @@ mod tests {
         // What was unsaved before the configure still is after it.
         let mut c = State::new(false);
         c.load_from_file(&dir).expect("load");
-        c.set_pane_collapsed(crate::slots::NETWORK_PANEL_IDX, true);
+        c.set_pane_collapsed(crate::slots::SPREADSHEET_IDX, true);
         c.resize(1400.0, 1080.0, 1.0);
         assert!(c.has_unsaved_changes(), "the configure must not hide an edit");
 
@@ -2061,7 +2131,7 @@ mod tests {
         state.update_window_title(); // the event loop's refresh, after the save event
         assert!(!state.title.ends_with('*'), "title: {}", state.title);
 
-        state.set_pane_collapsed(crate::slots::NETWORK_PANEL_IDX, true);
+        state.set_pane_collapsed(crate::slots::SPREADSHEET_IDX, true);
         assert!(state.has_unsaved_changes(), "a collapse is saved state too");
         state.save_to_file(&dir).expect("save");
         state.params_hud_width += 40.0;
@@ -2357,7 +2427,8 @@ mod tests {
         assert!(!state.params_claims(over.0, over.1), "a plate over the HUD takes the pointer");
         assert_eq!(state.plate_at(over.0, over.1), Some(SPREADSHEET_IDX));
 
-        // An older save docked the params pane; it loads out of the dock.
+        // An older save docked the params pane and the network; both load
+        // out of the docks.
         let dir = std::env::temp_dir().join(format!("cce-designer-hud-dock-test-{}", std::process::id()));
         let _ = fs::remove_dir_all(&dir);
         let mut a = State::new(false);
@@ -2369,8 +2440,10 @@ mod tests {
         let mut b = State::new(false);
         b.load_from_file(&dir).expect("load");
         assert_eq!(b.pane_in_dock(Dock::Left), NO_PANE, "the dock the params fronted is empty");
-        assert_eq!(b.pane_in_dock(Dock::Right), crate::slots::NETWORK_PANEL_IDX, "the rest of the arrangement loads");
+        assert_eq!(b.pane_in_dock(Dock::Right), NO_PANE, "the dock the network fronted is empty");
+        assert_eq!(b.pane_in_dock(Dock::Bottom), SPREADSHEET_IDX, "the rest of the arrangement loads");
         assert_eq!(b.tab_dock_of_pane(PARAM_IDX), None);
+        assert_eq!(b.tab_dock_of_pane(crate::slots::NETWORK_PANEL_IDX), None);
         let _ = fs::remove_dir_all(&dir);
     }
 
diff --git a/src/plate_menu.rs b/src/plate_menu.rs
index c9f7e6b..43237d0 100644
--- a/src/plate_menu.rs
+++ b/src/plate_menu.rs
@@ -33,8 +33,7 @@ pub const PLATE_SLOTS: [usize; 5] =
 /// pane: unplaced it simply does not exist. The params pane is not one
 /// (since 2026-10-06): it is a HUD on the scene, under the plates, laid out
 /// from the viewport alone (`State::params_hud_rect`).
-pub const TAB_CANDIDATES: [usize; 3] =
-    [NETWORK_PANEL_IDX, SPREADSHEET_IDX, NETWORK_PANEL2_IDX];
+pub const TAB_CANDIDATES: [usize; 2] = [SPREADSHEET_IDX, NETWORK_PANEL2_IDX];
 
 /// What the plate menu can do to its plate.
 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -105,6 +104,11 @@ impl State {
             if !w.visible() {
                 return false;
             }
+            // The network spans the window and has no plate: it is not the
+            // plate under anything.
+            if idx == NETWORK_PANEL_IDX && !self.pane_is_stubbed(idx) {
+                return false;
+            }
             // The params HUD is under every plate and, without its own,
             // only its rows: it is the plate there only where it claims.
             if idx == PARAM_IDX && !self.pane_is_stubbed(idx) {
@@ -130,9 +134,11 @@ impl State {
         let mut options: Vec<String> = Vec::new();
         let mut actions: Vec<PlateMenuAction> = Vec::new();
         for (label, action) in plate_dock::standard_menu(state, self.plate_can_detach(idx)) {
-            // The HUD does not collapse: it is not a plate with a stub to
-            // shrink to, and hiding it is Show Parameters Pane.
-            if idx == PARAM_IDX && matches!(action, PlateDockAction::Collapse | PlateDockAction::Expand) {
+            // Neither the HUD nor the network collapses: neither is a plate
+            // with a stub to shrink to, and hiding one is its Show … Pane.
+            if (idx == PARAM_IDX || idx == NETWORK_PANEL_IDX)
+                && matches!(action, PlateDockAction::Collapse | PlateDockAction::Expand)
+            {
                 continue;
             }
             options.push(label);
@@ -420,8 +426,9 @@ impl State {
     }
 
     pub fn set_pane_collapsed(&mut self, idx: usize, collapsed: bool) {
-        // The params HUD never collapses (see `plate_menu_rows`).
-        let collapsed = collapsed && idx != PARAM_IDX;
+        // The params HUD and the network never collapse (see
+        // `plate_menu_rows`).
+        let collapsed = collapsed && idx != PARAM_IDX && idx != NETWORK_PANEL_IDX;
         if !PLATE_SLOTS.contains(&idx) || self.collapsed_panes[idx] == collapsed {
             return;
         }
diff --git a/src/project.rs b/src/project.rs
index b669464..a63e67d 100644
--- a/src/project.rs
+++ b/src/project.rs
@@ -374,22 +374,19 @@ impl State {
             let all: Vec<usize> = resolved.iter().flatten().copied().collect();
             let n2 = crate::slots::NETWORK_PANEL2_IDX;
             let n2_count = all.iter().filter(|&&s| s == n2).count();
-            // The params pane is not docked since 2026-10-06 (it is a HUD
-            // on the scene): an older save lists it in a dock, and it is
-            // taken out — the dock it fronted fronts its next tab, or is
+            // Neither the params pane nor the network is docked since
+            // 2026-10-06 (the one a HUD on the scene, the other an overlay
+            // spanning it): an older save lists them in docks, and they are
+            // taken out — a dock one fronted fronts its next tab, or is
             // empty.
+            let undocked = |s: usize| s == crate::slots::PARAM_IDX || s == crate::slots::NETWORK_PANEL_IDX;
             let resolved: Vec<Vec<usize>> = resolved
                 .into_iter()
-                .map(|tabs| tabs.into_iter().filter(|&s| s != crate::slots::PARAM_IDX).collect())
+                .map(|tabs| tabs.into_iter().filter(|&s| !undocked(s)).collect())
                 .collect();
-            let all: Vec<usize> = all.into_iter().filter(|&s| s != crate::slots::PARAM_IDX).collect();
-            let mut core: Vec<usize> = all.iter().copied().filter(|&s| s != n2).collect();
-            core.sort_unstable();
-            let mut expected = vec![
-                crate::slots::NETWORK_PANEL_IDX,
-                crate::slots::SPREADSHEET_IDX,
-            ];
-            expected.sort_unstable();
+            let all: Vec<usize> = all.into_iter().filter(|&s| !undocked(s)).collect();
+            let core: Vec<usize> = all.iter().copied().filter(|&s| s != n2).collect();
+            let expected = vec![crate::slots::SPREADSHEET_IDX];
             if core == expected && n2_count <= 1 {
                 for d in 0..3 {
                     self.dock_tabs[d] = resolved[d].clone();