@@ -37,12 +37,26 @@ const GraphControl = ({ disableHoverEffect }: { disableHoverEffect?: boolean })
3737 const selectedEdge = useGraphStore . use . selectedEdge ( )
3838 const focusedEdge = useGraphStore . use . focusedEdge ( )
3939 const sigmaGraph = useGraphStore . use . sigmaGraph ( )
40+ const graphEdgeCount = useGraphStore . use . graphEdgeCount ( )
41+
42+ // Mirror GraphViewer's gating: above EDGE_PERF_LIMIT the sigma instance is
43+ // (re)built without the edge picking buffer, so edge events cannot fire even
44+ // though the user setting may be on. Use this — not the raw setting — for
45+ // event registration and the reducers, so we never run the costly edge-focus
46+ // path against a graph that can't actually pick edges.
47+ const effectiveEdgeEvents = enableEdgeEvents && graphEdgeCount <= Constants . EDGE_PERF_LIMIT
4048
4149 // The graph the initial FA2 layout has already been run for. Used to run the
4250 // layout once PER GRAPH, not once per sigma instance (a theme change rebuilds
4351 // the instance and re-runs the bind effect with the SAME graph).
4452 const laidOutGraphRef = useRef < unknown > ( null )
4553
54+ // Last (sigma instance, curved decision) the edge-type effect applied. A
55+ // rebuild (theme toggle / edge-events gating) creates a fresh sigma whose
56+ // defaultEdgeType reverts to its construction default ('rect'), so we must
57+ // re-apply when the INSTANCE changed too — not only when the decision flips.
58+ const edgeTypeRef = useRef < { sigma : unknown ; curved : boolean } | null > ( null )
59+
4660 /**
4761 * When component mounts or the graph changes
4862 * => bind graph to sigma and run the initial layout
@@ -76,8 +90,13 @@ const GraphControl = ({ disableHoverEffect }: { disableHoverEffect?: boolean })
7690 // effect with the SAME graph, which already carries settled positions —
7791 // re-running FA2 there would replay the relaxation animation on every theme
7892 // switch. Only (re)bind in that case; skip the layout.
93+ //
94+ // The ref is marked "laid out" only when the budget timer fires (layout
95+ // settled), NOT before start: if a rebuild interrupts the layout mid-run
96+ // (e.g. edge-events gating crosses the threshold during the first load),
97+ // the new instance re-runs FA2 from where it was instead of freezing on a
98+ // half-relaxed layout. Rebuilds after settling still match and skip.
7999 if ( laidOutGraphRef . current === sigmaGraph ) return
80- laidOutGraphRef . current = sigmaGraph
81100
82101 let layout : { start : ( ) => void ; stop : ( ) => void ; kill : ( ) => void } | null = null
83102 try {
@@ -100,6 +119,9 @@ const GraphControl = ({ disableHoverEffect }: { disableHoverEffect?: boolean })
100119 const timer = window . setTimeout ( ( ) => {
101120 try {
102121 layout ?. stop ( )
122+ // Mark this graph as laid out only now (settled), so a rebuild during
123+ // the budget window re-runs the layout rather than skipping it.
124+ laidOutGraphRef . current = sigmaGraph
103125 console . log ( 'FA2 worker layout stopped after budget' )
104126 // Release the shared slot so the store invariant "activeLayoutSupervisor
105127 // != null => a layout is running" holds (the budget just stopped this
@@ -129,7 +151,13 @@ const GraphControl = ({ disableHoverEffect }: { disableHoverEffect?: boolean })
129151 useEffect ( ( ) => {
130152 if ( sigma ) {
131153 const currentInstance = useGraphStore . getState ( ) . sigmaInstance
132- if ( ! currentInstance ) {
154+ // Update when the instance CHANGED, not only when it's unset. A theme
155+ // toggle, an effectiveEdgeEvents flip, or crossing the edge threshold
156+ // rebuilds the SigmaContainer (old instance killed, new one created); if
157+ // we only wrote on an empty store the killed instance would linger there
158+ // and consumers (e.g. expand reading sigmaInstance.getCamera()) would act
159+ // on a dead Sigma.
160+ if ( currentInstance !== sigma ) {
133161 console . log ( 'Setting sigma instance from GraphControl' )
134162 useGraphStore . getState ( ) . setSigmaInstance ( sigma )
135163 }
@@ -182,6 +210,45 @@ const GraphControl = ({ disableHoverEffect }: { disableHoverEffect?: boolean })
182210 }
183211 } , [ sigma ] )
184212
213+ /**
214+ * Adapt edge geometry to graph size: curves for small graphs (nicer to read),
215+ * straight rectangles above EDGE_PERF_LIMIT (curve tessellation is costly).
216+ *
217+ * Edges carry no per-edge `type`, so switching `defaultEdgeType` + a full
218+ * `refresh()` (which re-adds edges through applyEdgeDefaults) re-selects the
219+ * program for the whole graph without touching attributes or rebuilding.
220+ *
221+ * The ref tracks BOTH the sigma instance and the decision: re-apply when the
222+ * instance changed (a rebuild resets defaultEdgeType to 'rect') OR when the
223+ * curved/straight decision flipped; skip otherwise so routine expand/prune
224+ * within one band don't trigger a full refresh.
225+ */
226+ useEffect ( ( ) => {
227+ if ( ! sigma ) return
228+ const curved = graphEdgeCount > 0 && graphEdgeCount <= Constants . EDGE_PERF_LIMIT
229+ const prev = edgeTypeRef . current
230+ if ( prev && prev . sigma === sigma && prev . curved === curved ) return
231+ edgeTypeRef . current = { sigma, curved }
232+ setSettings ( { defaultEdgeType : curved ? 'curvedNoArrow' : 'rect' } )
233+ try {
234+ sigma . refresh ( )
235+ } catch {
236+ /* sigma instance already killed */
237+ }
238+ } , [ sigma , graphEdgeCount , setSettings ] )
239+
240+ /**
241+ * When edge events become gated off (count crossed above EDGE_PERF_LIMIT),
242+ * drop any residual edge focus/selection so the UI (property panel, reducers)
243+ * doesn't keep a now-unpickable edge highlighted. Node selection is untouched.
244+ */
245+ useEffect ( ( ) => {
246+ if ( effectiveEdgeEvents ) return
247+ const { selectedEdge, focusedEdge, setSelectedEdge, setFocusedEdge } = useGraphStore . getState ( )
248+ if ( selectedEdge !== null ) setSelectedEdge ( null )
249+ if ( focusedEdge !== null ) setFocusedEdge ( null )
250+ } , [ effectiveEdgeEvents ] )
251+
185252 /**
186253 * When component mounts
187254 * => register events
@@ -217,7 +284,7 @@ const GraphControl = ({ disableHoverEffect }: { disableHoverEffect?: boolean })
217284 clickStage : ( ) => clearSelection ( )
218285 }
219286
220- if ( enableEdgeEvents ) {
287+ if ( effectiveEdgeEvents ) {
221288 events . clickEdge = ( event : EdgeEvent ) => {
222289 setSelectedEdge ( event . edge )
223290 setSelectedNode ( null )
@@ -235,7 +302,7 @@ const GraphControl = ({ disableHoverEffect }: { disableHoverEffect?: boolean })
235302 }
236303
237304 registerEvents ( events )
238- } , [ registerEvents , enableEdgeEvents , sigma ] )
305+ } , [ registerEvents , effectiveEdgeEvents , sigma ] )
239306
240307 /**
241308 * When edge size settings change, recalculate edge sizes.
@@ -297,11 +364,16 @@ const GraphControl = ({ disableHoverEffect }: { disableHoverEffect?: boolean })
297364 const effectiveRenderLabels = renderLabels && graphOrder <= Constants . LABEL_RENDER_LIMIT
298365
299366 const _focusedNode = focusedNode || selectedNode
300- const _focusedEdge = focusedEdge || selectedEdge
367+ // Ignore any residual edge focus/selection when edge events are gated off
368+ // (e.g. an edge was selected on a small graph, then expand pushed it past
369+ // EDGE_PERF_LIMIT): otherwise the edge-focused reducer branch below would
370+ // still run the per-edge highlight pass on a large graph — exactly the cost
371+ // we disabled edge events to avoid.
372+ const _focusedEdge = effectiveEdgeEvents ? focusedEdge || selectedEdge : null
301373
302374 if ( disableHoverEffect || ( ! _focusedNode && ! _focusedEdge ) ) {
303375 setSettings ( {
304- enableEdgeEvents,
376+ enableEdgeEvents : effectiveEdgeEvents ,
305377 renderEdgeLabels,
306378 renderLabels : effectiveRenderLabels ,
307379 nodeReducer : null ,
@@ -331,7 +403,7 @@ const GraphControl = ({ disableHoverEffect }: { disableHoverEffect?: boolean })
331403 : Constants . edgeColorHighlightedLightTheme
332404
333405 setSettings ( {
334- enableEdgeEvents,
406+ enableEdgeEvents : effectiveEdgeEvents ,
335407 renderEdgeLabels,
336408 renderLabels : effectiveRenderLabels ,
337409
@@ -403,7 +475,7 @@ const GraphControl = ({ disableHoverEffect }: { disableHoverEffect?: boolean })
403475 disableHoverEffect ,
404476 isDarkTheme ,
405477 hideUnselectedEdges ,
406- enableEdgeEvents ,
478+ effectiveEdgeEvents ,
407479 renderEdgeLabels ,
408480 renderLabels
409481 ] )
0 commit comments