graphic design tool
git clone https://git.lucas.co/cce-designer.git
fix(network): a dragged node always snaps to a cell it could land on
The designer passed config.kdl's graph grid_snap (off on the user's
machine) to its editors, so a dragged node floated freely and landed
somewhere else. Snapping is always on now, and cce-ui's drag snaps only
to cells the node could land on.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CLAUDE.md | 7 +++++++
src/app.rs | 17 +++++++++--------
src/main.rs | 10 +++++++++-
3 files changed, 25 insertions(+), 9 deletions(-)
diff --git a/CLAUDE.md b/CLAUDE.md
index 8e9c49e..6834135 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -1423,6 +1423,13 @@ position write-back carries into the tree, and the release drains
follow by an offset.
- **The swap wins over a splice**: a node's own wires run into its body,
so a ghost on a node always touches one.
+- **A dragged node always snaps to a cell it could land on** (the same
+ day): `State::grid_snap_enabled` is always on, and config.kdl's
+ `style.surface.graph.grid_snap` is not read — off on the user's machine,
+ it let a dragged node float freely and land somewhere else. With swap on
+ every crossing is one it could land on (a free one moves it, a node's
+ swaps), so the ghost goes where the pointer is nearest; in a group drag,
+ where swap is off, it skips taken crossings (cce-ui's `drag_update`).
`dropping_a_node_on_a_node_swaps_their_places` drives it by pointer, undo
included; `swapping_places_trades_wires_port_for_port` is the rule.
diff --git a/src/app.rs b/src/app.rs
index 5ea07ca..53835e2 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -2435,9 +2435,8 @@ pub fn configured_grid_geometry() -> GridGeometry {
/// change to what it checks, made for the sake of a number that is arbitrary
/// either way. What matters is that the number is the suite's own.
///
-/// `graph_grid_snap` is NOT pinned here: it is read inside cce-ui's Graph
-/// widget rather than through this crate, so there is nothing to intercept —
-/// it is off both by cce-ui default and in practice.
+/// `graph_grid_snap` needs no pin: this app does not read it, a dragged node
+/// always snapping to a cell it can land on (`State::grid_snap_enabled`).
#[cfg(test)]
pub fn configured_grid_geometry() -> GridGeometry {
GridGeometry { pitch_x: 140.0, pitch_y: 70.0, node_w: 80.0, node_h: 40.0 }
@@ -3105,6 +3104,9 @@ pub struct State {
pub physical_height: u32,
pub scale: f64,
pub square_viewport: bool,
+ /// Whether a dragged node snaps to cells: always, in this app — to a
+ /// cell it can land on (cce-ui's `Graph::drag_update`). Not read from
+ /// `style.surface.graph.grid_snap`.
pub grid_snap_enabled: bool,
pub network_grid_visible: bool,
/// The network grid's pitch at the current zoom — centre of one grid
@@ -9084,7 +9086,6 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
let opacity = cce_ui::color::graph_opacity();
let node_opacity = cce_ui::color::graph_node_opacity();
let graph_grid_color = cce_ui::color::graph_grid_color();
- let snap_enabled = cce_ui::layout::graph_grid_snap();
let node_color = cce_ui::color::graph_node_color();
let node_compression = cce_ui::config::cached_config()
.pointer("/style/surface/graph/node_compression")
@@ -9114,10 +9115,10 @@ pub(crate) fn geometry_to_spreadsheet_data(geom: &Detail) -> (Vec<String>, Vec<V
colors::set_node_color(node_color);
changed = true;
}
- if self.grid_snap_enabled != snap_enabled {
- self.grid_snap_enabled = snap_enabled;
- changed = true;
- }
+ // `graph_grid_snap` is not read: a dragged node always snaps to a
+ // cell it can land on (`grid_snap_enabled`, always on — since
+ // 2026-10-06; the config's switch, off on the user's machine, had a
+ // dragged node float freely and land somewhere else).
// The grid's spacing and node size: re-applied at the zoom in hand,
// so a config.kdl edit to `spacing_y` shows at once. Until
// 2026-10-06 the live geometry was set at startup and only zoomed
diff --git a/src/main.rs b/src/main.rs
index 6443de3..0c9ef5b 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -16276,9 +16276,17 @@ mod tests {
state.handle_event(&WindowEvent::CursorMoved { position: LocalPosition { x: x as f64, y: y as f64 } });
};
- // Grab b and drop it on a.
+ // Grab b and drop it on a — the pointer between cells, not on a
+ // crossing: the node snaps, whatever the config's grid_snap says.
+ state.update_graph_settings_from_config();
+ assert!(state.grid_snap_enabled, "a dragged node always snaps");
move_to(&mut state, (1, 6));
state.handle_event(&WindowEvent::MouseInput { state: ElementState::Pressed, button: MouseButton::Left });
+ let (ax, ay) = state.cell_center(1, 5);
+ state.handle_event(&WindowEvent::CursorMoved { position: LocalPosition { x: (ax + 23.0) as f64, y: (ay + 11.0) as f64 } });
+ let ghost = state.slots.content.inner().node_rect(slot(&state, "b")).unwrap();
+ let cell = state.cell_rect(1, 5);
+ assert!((ghost.0 - cell.0).abs() < 0.5 && (ghost.1 - cell.1).abs() < 0.5, "the dragged node sits on a's cell: {ghost:?} vs {cell:?}");
move_to(&mut state, (1, 5));
assert_eq!(state.slots.content.inner().swap_target_idx(), Some(slot(&state, "a")), "a is the swap target");
state.handle_event(&WindowEvent::MouseInput { state: ElementState::Released, button: MouseButton::Left });