graphic design tool
git clone https://git.lucas.co/cce-designer.git
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;