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

commit4e72b7091aba2628d7a8642377559d00fb956527
parent37627adcd6
authorLucas Galante <lsgalante12@gmail.com>
date2026-09-30 12:49
feat: node wires have a style, a Settings row; a config edit repaints

The network's wires are drawn by cce-ui's Graph::paint_wires, after the
grid, in one of four styles - Orthogonal, Rounded, Bezier, Straight - and in
config.kdl's wire_color and wire_size. Node Wire Style is a dialog Settings
row that sets both network editors; it persists in the viewport block (so
state.kdl and the project's display block), and empty follows config.kdl's
style.surface.graph.node.wire_style.

tick_frame's config poll now asks for a repaint when config.kdl changed,
where a changed key used to show only when something else drew.

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

 CLAUDE.md     | 20 ++++++++++++++++++++
 src/app.rs    | 17 ++++++++++++++++-
 src/dialog.rs | 16 ++++++++++++++++
 src/main.rs   | 34 ++++++++++++++++++++++++++++++++++
 src/render.rs |  4 +++-
 5 files changed, 89 insertions(+), 2 deletions(-)

diff --git a/CLAUDE.md b/CLAUDE.md
index 00615d8..af7796e 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -2408,6 +2408,26 @@ cell-and-gap setters (`set_grid_sizes` / `set_skipped_sizes`) survive as a
 description of the same lattice for cce-files and cce-graph, which still
 speak it; this app sets the pitch.
 
+### Node wires have a style
+
+The network's wires are drawn by cce-ui's `Graph::paint_wires` (see its
+CLAUDE.md, "A graph's wires are strokes in a style"), called in
+`render.rs` right after `paint_grid`, and come in four styles: Orthogonal,
+Rounded, Bezier, Straight. **Node Wire Style** is a dialog Settings row
+(Alt+D, "wire"), a `Ctl::Choice` whose value lives on the two Graph widgets
+themselves (`State::set_node_wire_style` sets both); it persists as
+`ViewportSettings::node_wire_style`, so in state.kdl and with the project's
+display block. Empty — every file from before the row — hands the choice
+to config.kdl's `style.surface.graph.node.wire_style`, and the row then
+reads what the config says. Not to be confused with the wireframe's
+"Wire" rows beside it, which are the 3D edges.
+
+**A config.kdl edit repaints** (the same day): `tick_frame`'s config poll
+reloaded the style registry and returned nothing, so a changed key showed
+only when something else drew — a wire style set in the config appeared on
+the next hover. `the_node_wire_style_is_a_setting_the_project_keeps` is the
+test for the row.
+
 ### The cursor is a region, and dragging the grid grows it
 
 A left press on EMPTY grid puts the cursor on the pressed cell — on the press,
diff --git a/src/app.rs b/src/app.rs
index df63b89..e2ee10d 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -1331,6 +1331,11 @@ pub struct ViewportSettings {
     /// the project, but whether its surface is drawn is how you like to work.
     #[serde(default = "default_network_plate")]
     pub network_plate: bool,
+    /// How the network's node wires run (`cce_ui::widget::display::WireStyle`
+    /// by name). Empty follows `style.surface.graph.node.wire_style` in
+    /// config.kdl, which is every file from before the row.
+    #[serde(default)]
+    pub node_wire_style: String,
     /// The three point overlays. Display settings like the guide toggles
     /// above, and persisted in the same place: they were per-node `meta`
     /// child preferences until 2026-09-23, which made a view choice into a
@@ -1551,6 +1556,7 @@ impl Default for ViewportSettings {
             show_grid_enabled: true,
             show_origin_enabled: true,
             network_plate: true,
+            node_wire_style: String::new(),
             show_point_markers: false,
             show_point_numbers: false,
             show_point_normals: false,
@@ -2760,6 +2766,7 @@ impl State {
                 grid_thickness: self.grid_thickness,
                 grid_color: self.viewport().grid_color,
                 network_plate: self.network_plate,
+                node_wire_style: self.slots.content.inner().chosen_wire_style().map(|w| w.name().to_string()).unwrap_or_default(),
                 show_point_markers: self.show_point_markers,
                 show_point_numbers: self.show_point_numbers,
                 show_point_normals: self.show_point_normals,
@@ -2838,6 +2845,7 @@ impl State {
         self.origin_size = v.origin_size;
         self.grid_thickness = v.grid_thickness;
         self.network_plate = v.network_plate;
+        self.set_node_wire_style(cce_ui::widget::display::WireStyle::parse(&v.node_wire_style));
         self.circular_network_pane = self.is_detached_network || v.circular_pane;
         self.show_point_markers = v.show_point_markers;
         self.show_point_numbers = v.show_point_numbers;
@@ -7157,6 +7165,9 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
         });
 
         slots.playbar.inner_mut().repeat = settings.playbar_repeat;
+        let wire_style = cce_ui::widget::display::WireStyle::parse(&settings.viewport.node_wire_style);
+        slots.content.inner_mut().set_wire_style(wire_style);
+        slots.content2.inner_mut().set_wire_style(wire_style);
         slots.playbar.inner_mut().fps = settings.playbar_fps.clamp(1.0, 120.0);
         if let Some(viewport) = slots.viewport.as_any_mut().downcast_mut::<Viewport3D>() {
             viewport.show_grid = settings.viewport.show_grid_enabled;
@@ -11154,6 +11165,9 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
         // closed window cannot strand the pane as a stub nothing can revive.
         let reclaimed = self.poll_detached_children();
 
+        // A config.kdl edit repaints: until 2026-09-30 it waited for whatever
+        // drew next, so an edit to the wire style showed on the next hover.
+        let mut config_changed = false;
         if now.duration_since(self.last_config_read).as_secs_f32() > 2.0 {
             self.last_config_read = now;
             let config_paths = [
@@ -11171,6 +11185,7 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
             }
             if current_mod_time != self.last_config_mod_time {
                 self.last_config_mod_time = current_mod_time;
+                config_changed = true;
                 cce_ui::layout::reload_config();
                 self.update_inertial_settings();
                 self.update_graph_settings_from_config();
@@ -11409,7 +11424,7 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
             self.read_panel_offsets();
         }
 
-        tick_changed || panned || reclaimed || glow_animating || frame_moved
+        tick_changed || panned || reclaimed || glow_animating || frame_moved || config_changed
     }
 
     /// Flush CPU-staged mesh updates to the renderer's persistent meshes.
diff --git a/src/dialog.rs b/src/dialog.rs
index 55852fe..9e9d834 100644
--- a/src/dialog.rs
+++ b/src/dialog.rs
@@ -1361,6 +1361,8 @@ impl Setting {
 pub const SETTINGS: &[Setting] = &[
     Setting::field("Background Color", "bg_color", Ctl::Color),
     Setting::field("World Unit", "world_unit", Ctl::Choice(&["mm", "cm", "m", "in"])),
+    // How the network's node wires run — not the wireframe's wires below.
+    Setting::field("Node Wire Style", "node_wire_style", Ctl::Choice(&["Orthogonal", "Rounded", "Bezier", "Straight"])),
     // Takes effect at the next launch — the renderer's device is chosen
     // once, when it is created (`app::apply_gpu_preference`).
     Setting::field("GPU", "gpu", Ctl::Choice(crate::app::GPU_CHOICES)),
@@ -1945,6 +1947,7 @@ impl State {
         match key {
             "world_unit" => self.world_unit.suffix().to_string(),
             "gpu" => self.gpu_preference.clone(),
+            "node_wire_style" => self.slots.content.inner().wire_style().label().to_string(),
             _ => String::new(),
         }
     }
@@ -2020,11 +2023,24 @@ impl State {
                     let Some(v) = options.iter().find(|o| o.eq_ignore_ascii_case(value)) else { return };
                     self.gpu_preference = v.to_string();
                 }
+                "node_wire_style" => {
+                    if let Some(w) = cce_ui::widget::display::WireStyle::parse(value) {
+                        self.set_node_wire_style(Some(w));
+                    }
+                }
                 _ => {}
             },
         }
     }
 
+    /// The node wires' style on both network editors — `None` follows the
+    /// config's `wire_style`. The widgets hold it; the save reads it back
+    /// off the first.
+    pub(crate) fn set_node_wire_style(&mut self, style: Option<cce_ui::widget::display::WireStyle>) {
+        self.slots.content.inner_mut().set_wire_style(style);
+        self.slots.content2.inner_mut().set_wire_style(style);
+    }
+
     /// Write one setting's value to whatever owns it.
     fn setting_write(&mut self, s: &Setting, value: &str) {
         match s.owner {
diff --git a/src/main.rs b/src/main.rs
index 9afc815..30710b8 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1686,6 +1686,40 @@ mod tests {
         let _ = fs::remove_dir_all(dir.parent().unwrap());
     }
 
+    /// The node wires' style is a Settings row: choosing one sets it on both
+    /// network editors, the project file carries it, and a file that names
+    /// none — every one from before the row — follows the config.
+    #[test]
+    fn the_node_wire_style_is_a_setting_the_project_keeps() {
+        use cce_ui::widget::display::WireStyle;
+        let dir = std::env::temp_dir()
+            .join(format!("cce-designer-wires-{}", std::process::id()))
+            .join("look");
+        let _ = fs::remove_dir_all(&dir);
+        let mut state = State::new(false);
+        state.apply_setting("Node Wire Style", "Bezier");
+        assert_eq!(state.slots.content.inner().wire_style(), WireStyle::Bezier);
+        assert_eq!(state.slots.content2.inner().wire_style(), WireStyle::Bezier, "both editors");
+        assert_eq!(state.display_settings().viewport.node_wire_style, "bezier");
+        state.save_to_file(&dir).expect("save");
+
+        state.apply_setting("Node Wire Style", "Straight");
+        assert!(state.has_unsaved_changes(), "a wire style is an edit to the file");
+        state.load_from_file(&dir).expect("load");
+        assert_eq!(state.slots.content.inner().wire_style(), WireStyle::Bezier, "the file's style comes back");
+
+        // A file that names no style hands the choice back to the config.
+        let state_json = dir.join("state.json");
+        let mut v: serde_json::Value = serde_json::from_str(&fs::read_to_string(&state_json).unwrap()).unwrap();
+        v["view_state"]["display"]["viewport"].as_object_mut().unwrap().remove("node_wire_style");
+        fs::write(&state_json, serde_json::to_string(&v).unwrap()).unwrap();
+        state.load_from_file(&dir).expect("load an older save");
+        assert_eq!(state.slots.content.inner().chosen_wire_style(), None);
+        assert_eq!(state.slots.content.inner().wire_style(), WireStyle::configured());
+
+        let _ = fs::remove_dir_all(dir.parent().unwrap());
+    }
+
     /// A number under a plate is not drawn: the engine lays text out after
     /// all geometry, so it would stand sharp over a plate that frosts
     /// everything else behind it.
diff --git a/src/render.rs b/src/render.rs
index 824ba73..4dee6ba 100644
--- a/src/render.rs
+++ b/src/render.rs
@@ -451,8 +451,10 @@ impl State {
                 let mut seen_node = false;
                 // The grid lines (flat, gap colour at the network opacity)
                 // and the origin axes, under the wires and nodes; the cells
-                // are the pane plate itself.
+                // are the pane plate itself. Then the wires, which are
+                // strokes in the node wire style and not among the quads.
                 g.paint_grid(clip, pc);
+                g.paint_wires(clip, pc);
                 for (qx, qy, qw, qh, qc, cell) in g.geometry_quads_tagged(clip) {
                     if g.is_node_rect(qx, qy, qw, qh) {
                         seen_node = true;