@@ -123,8 +123,7 @@ where
123123 graph[ node_ix] . register ( reg_ctx) ;
124124
125125 // Position the new node under the pointer, falling back to the center of the
126- // current view. `insert` (not `entry`) so a reused stable-graph index can't
127- // inherit a removed node's stale position.
126+ // current view.
128127 let pos = pos. unwrap_or_else ( || view. scene_rect . center ( ) ) ;
129128 let egui_id = egui_graph:: NodeId :: from_u64 ( node_ix. index ( ) as u64 ) ;
130129 view. layout . insert ( egui_id, pos) ;
@@ -179,8 +178,7 @@ where
179178 let node_ix = graph. add_node ( N :: from ( named_ref) ) ;
180179
181180 // Position the new node under the pointer, falling back to the center of the
182- // current view. `insert` (not `entry`) so a reused stable-graph index can't
183- // inherit a removed node's stale position.
181+ // current view.
184182 let pos = pos. unwrap_or_else ( || view. scene_rect . center ( ) ) ;
185183 let egui_id = egui_graph:: NodeId :: from_u64 ( node_ix. index ( ) as u64 ) ;
186184 view. layout . insert ( egui_id, pos) ;
@@ -194,6 +192,60 @@ where
194192 Some ( node_ix)
195193}
196194
195+ /// Remove `nodes` from `graph`, migrating the per-node state, layout and
196+ /// selection that are keyed by node index.
197+ ///
198+ /// `petgraph::Graph::remove_node` swap-removes: the former-last node adopts the
199+ /// removed index, so exactly one surviving node changes index per removal.
200+ /// Targets are processed highest-index first, so a swap only ever pulls a
201+ /// surviving node down into an already-freed higher slot and never invalidates a
202+ /// pending target. The swapped node's state, layout entry and selection are then
203+ /// moved to its new index. Edge selection is cleared because removing a node
204+ /// drops its incident edges with compounded edge swaps. Returns whether any node
205+ /// was removed.
206+ ///
207+ /// Run this before the next recompile (`vm::sync`): the regenerated code reads
208+ /// state by the new index, so the migration must already be in place.
209+ pub fn remove_nodes < N > (
210+ graph : & mut Graph < N > ,
211+ vm : & mut Engine ,
212+ layout : & mut egui_graph:: Layout ,
213+ selection : & mut crate :: widget:: graph_scene:: Selection ,
214+ nodes : impl IntoIterator < Item = NodeIndex > ,
215+ ) -> bool {
216+ let node_id = |ix : usize | egui_graph:: NodeId :: from_u64 ( ix as u64 ) ;
217+ let mut targets: Vec < NodeIndex > = nodes. into_iter ( ) . collect ( ) ;
218+ targets. sort_unstable_by_key ( |n| std:: cmp:: Reverse ( n. index ( ) ) ) ;
219+ targets. dedup ( ) ;
220+ let mut removed_any = false ;
221+ for t in targets {
222+ if graph. node_weight ( t) . is_none ( ) {
223+ continue ;
224+ }
225+ let last = graph. node_count ( ) - 1 ;
226+ // Drop the removed node's index-keyed data.
227+ let _ = node:: state:: remove_value ( vm, & [ t. index ( ) ] ) ;
228+ layout. remove ( & node_id ( t. index ( ) ) ) ;
229+ selection. nodes . remove ( & t) ;
230+ graph. remove_node ( t) ;
231+ // Migrate the node that swapped into `t` (the former `last`), if any.
232+ if t. index ( ) != last {
233+ let _ = node:: state:: move_value ( vm, & [ last] , & [ t. index ( ) ] ) ;
234+ if let Some ( pos) = layout. remove ( & node_id ( last) ) {
235+ layout. insert ( node_id ( t. index ( ) ) , pos) ;
236+ }
237+ if selection. nodes . remove ( & NodeIndex :: new ( last) ) {
238+ selection. nodes . insert ( t) ;
239+ }
240+ }
241+ removed_any = true ;
242+ }
243+ if removed_any {
244+ selection. edges . clear ( ) ;
245+ }
246+ removed_any
247+ }
248+
197249/// Insert an Inspect node on the given edge, splicing it between the
198250/// endpoints and positioning it at `cmd.pos`.
199251pub fn inspect_edge < N > (
@@ -376,3 +428,56 @@ pub fn commit_layout<G>(
376428 head,
377429 ) )
378430}
431+
432+ #[ cfg( test) ]
433+ mod tests {
434+ use super :: * ;
435+ use crate :: widget:: graph_scene:: Selection ;
436+ use gantz_core:: node:: graph:: NodeIx ;
437+
438+ // Deleting a node swap-removes the former-last node into its slot; the
439+ // swapped node's layout entry and selection must follow it to the new index.
440+ #[ test]
441+ fn remove_nodes_migrates_layout_and_selection ( ) {
442+ let node_id = |i : usize | egui_graph:: NodeId :: from_u64 ( i as u64 ) ;
443+
444+ // Five nodes 0..5 (weights 10..15) each with a distinct layout x.
445+ let mut graph: Graph < u32 > = Graph :: default ( ) ;
446+ for w in 10u32 ..15 {
447+ graph. add_node ( w) ;
448+ }
449+ let mut layout = egui_graph:: Layout :: default ( ) ;
450+ for i in 0 ..5 {
451+ layout. insert ( node_id ( i) , egui:: pos2 ( i as f32 , 0.0 ) ) ;
452+ }
453+ let mut selection = Selection :: default ( ) ;
454+ selection. nodes . insert ( NodeIx :: new ( 4 ) ) ; // select the (to-be-swapped) last
455+
456+ let mut vm = Engine :: new_base ( ) ;
457+
458+ // Delete index 1: node 4 (weight 14) swap-removes into slot 1.
459+ let removed = remove_nodes (
460+ & mut graph,
461+ & mut vm,
462+ & mut layout,
463+ & mut selection,
464+ [ NodeIx :: new ( 1 ) ] ,
465+ ) ;
466+ assert ! ( removed) ;
467+
468+ // The swapped node now sits at index 1.
469+ assert_eq ! ( graph. node_count( ) , 4 ) ;
470+ assert_eq ! ( graph[ NodeIx :: new( 1 ) ] , 14 ) ;
471+
472+ // Layout followed the swap; the deleted and old-last slots are gone.
473+ assert_eq ! ( layout. len( ) , 4 ) ;
474+ assert_eq ! ( layout. get( & node_id( 1 ) ) . copied( ) , Some ( egui:: pos2( 4.0 , 0.0 ) ) ) ;
475+ assert ! ( !layout. contains_key( & node_id( 4 ) ) ) ;
476+
477+ // Selection followed the swap: node 4 -> node 1.
478+ assert_eq ! (
479+ selection. nodes. iter( ) . copied( ) . collect:: <Vec <_>>( ) ,
480+ vec![ NodeIx :: new( 1 ) ] ,
481+ ) ;
482+ }
483+ }
0 commit comments