Skip to content

Commit 53583bd

Browse files
committed
feat: add EditorInitApi for unified initialization, improve spatial index diagnostics, and update render layer prioritization
1 parent 05683d2 commit 53583bd

File tree

7 files changed

+562
-240
lines changed

7 files changed

+562
-240
lines changed
Lines changed: 353 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,353 @@
1+
part of '../node_flow_controller.dart';
2+
3+
/// Editor initialization API for [NodeFlowController].
4+
///
5+
/// This extension provides the canonical initialization entry point for the
6+
/// NodeFlow editor. ALL initialization happens here - this is the "Big Bang"
7+
/// moment that sets up the entire universe of the editor.
8+
///
9+
/// ## Design Principles
10+
///
11+
/// 1. **Single Entry Point**: [_initController] is THE ONLY place where
12+
/// initialization occurs. No scattered initialization across multiple files.
13+
///
14+
/// 2. **Predictable Order**: All setup happens in a specific, documented order.
15+
/// This eliminates timing-related bugs where components depend on each other.
16+
///
17+
/// 3. **Private to Editor**: This API is internal - only [NodeFlowEditor] can
18+
/// call it. External code cannot interfere with initialization.
19+
///
20+
/// 4. **Idempotent**: Calling [_initController] multiple times is safe - it
21+
/// only initializes once.
22+
///
23+
/// ## Initialization Order
24+
///
25+
/// 1. Theme setup
26+
/// 2. Node shape builder setup
27+
/// 3. Spatial index callbacks (portSizeResolver, nodeShapeBuilder)
28+
/// 4. Connection painter creation
29+
/// 5. Connection hit tester setup
30+
/// 6. Render order provider setup
31+
/// 7. Connection segment calculator storage
32+
/// 8. Spatial index reactions setup
33+
/// 9. Event handlers setup
34+
/// 10. Initial spatial index rebuild (handles pre-loaded nodes)
35+
///
36+
extension EditorInitApi<T> on NodeFlowController<T> {
37+
// ============================================================================
38+
// Initialization State
39+
// ============================================================================
40+
41+
/// Whether the controller has been initialized for editor use.
42+
///
43+
/// This becomes true after [internalInitController] completes successfully.
44+
/// Operations that require the editor to be initialized should check this
45+
/// flag first.
46+
bool get isEditorInitialized => _editorInitialized;
47+
48+
// ============================================================================
49+
// Primary Initialization Entry Point
50+
// ============================================================================
51+
52+
/// Initializes the controller for use with the NodeFlow editor.
53+
///
54+
/// **THIS IS THE CANONICAL INITIALIZATION POINT.**
55+
///
56+
/// All editor infrastructure is set up here in a specific order. This method
57+
/// must be called from [NodeFlowEditor.initState] before any other operations.
58+
///
59+
/// **@internal** - This method is for internal use by [NodeFlowEditor] only.
60+
/// Do not call directly from application code.
61+
///
62+
/// ## Parameters
63+
///
64+
/// - [theme]: The visual theme for the editor (required)
65+
/// - [portSizeResolver]: Resolves the size of a port for spatial indexing (required)
66+
/// - [nodeShapeBuilder]: Optional builder for custom node shapes
67+
/// - [connectionHitTesterBuilder]: Builder for the connection hit test callback.
68+
/// This receives the [ConnectionPainter] and returns a hit test function.
69+
/// - [connectionSegmentCalculator]: Calculates segment bounds for connection
70+
/// spatial indexing. Required for accurate connection hit testing.
71+
/// - [events]: Optional event handlers for node/connection/viewport events
72+
///
73+
/// ## Example
74+
///
75+
/// ```dart
76+
/// // In NodeFlowEditor.initState():
77+
/// widget.controller.internalInitController(
78+
/// theme: widget.theme,
79+
/// portSizeResolver: (port) => port.size ?? widget.theme.portTheme.size,
80+
/// nodeShapeBuilder: widget.nodeShapeBuilder != null
81+
/// ? (node) => widget.nodeShapeBuilder!(context, node)
82+
/// : null,
83+
/// connectionHitTesterBuilder: (painter) => (connection, point) {
84+
/// // hit test logic using painter
85+
/// },
86+
/// connectionSegmentCalculator: (connection) {
87+
/// // calculate segment bounds
88+
/// },
89+
/// events: widget.events,
90+
/// );
91+
/// ```
92+
void internalInitController({
93+
required NodeFlowTheme theme,
94+
required Size Function(Port port) portSizeResolver,
95+
NodeShape? Function(Node<T> node)? nodeShapeBuilder,
96+
bool Function(Connection connection, Offset point)? Function(
97+
ConnectionPainter painter,
98+
)?
99+
connectionHitTesterBuilder,
100+
List<Rect> Function(Connection connection)? connectionSegmentCalculator,
101+
NodeFlowEvents<T>? events,
102+
}) {
103+
// Idempotent - only initialize once
104+
if (_editorInitialized) return;
105+
_editorInitialized = true;
106+
107+
// =========================================================================
108+
// Step 1: Store the theme
109+
// =========================================================================
110+
runInAction(() => _themeObservable.value = theme);
111+
112+
// =========================================================================
113+
// Step 2: Set up node shape builder
114+
// =========================================================================
115+
// This must happen before spatial index setup and connection painter
116+
// creation since both use the shape builder.
117+
_nodeShapeBuilder = nodeShapeBuilder;
118+
119+
// =========================================================================
120+
// Step 3: Set up spatial index callbacks
121+
// =========================================================================
122+
// These callbacks are used when calculating bounds for nodes and ports.
123+
// They must be set before any spatial index operations.
124+
_spatialIndex.portSizeResolver = portSizeResolver;
125+
_spatialIndex.nodeShapeBuilder = nodeShapeBuilder;
126+
127+
// =========================================================================
128+
// Step 4: Create connection painter
129+
// =========================================================================
130+
// The connection painter handles rendering and hit testing of connections.
131+
// It needs the theme and optionally the node shape builder.
132+
_connectionPainter = ConnectionPainter(
133+
theme: theme,
134+
nodeShape: nodeShapeBuilder != null
135+
? (node) => nodeShapeBuilder(node as Node<T>)
136+
: null,
137+
);
138+
139+
// =========================================================================
140+
// Step 5: Set up connection hit tester
141+
// =========================================================================
142+
// Now that the connection painter exists, we can create the hit tester.
143+
if (connectionHitTesterBuilder != null) {
144+
_spatialIndex.connectionHitTester = connectionHitTesterBuilder(
145+
_connectionPainter!,
146+
);
147+
}
148+
149+
// =========================================================================
150+
// Step 6: Set up render order provider
151+
// =========================================================================
152+
// This enables accurate hit testing based on visual stacking order.
153+
_spatialIndex.renderOrderProvider = () => sortedNodes;
154+
155+
// =========================================================================
156+
// Step 7: Store connection segment calculator for later use
157+
// =========================================================================
158+
_connectionSegmentCalculator = connectionSegmentCalculator;
159+
160+
// =========================================================================
161+
// Step 8: Set up spatial index reactions
162+
// =========================================================================
163+
// These reactions automatically sync the spatial index when nodes,
164+
// connections, or theme properties change.
165+
_setupSpatialIndexReactions();
166+
167+
// =========================================================================
168+
// Step 9: Set up event handlers
169+
// =========================================================================
170+
if (events != null) {
171+
_events = events;
172+
}
173+
174+
// =========================================================================
175+
// Step 10: Initialize spatial indexes and node infrastructure
176+
// =========================================================================
177+
// If nodes were pre-loaded (e.g., from loadDocument before editor mounted),
178+
// we need to set up their infrastructure and rebuild spatial indexes now.
179+
_initializeLoadedNodes();
180+
_rebuildSpatialIndexes();
181+
}
182+
183+
// ============================================================================
184+
// Theme Updates (Post-Initialization)
185+
// ============================================================================
186+
187+
/// Updates the theme on an already-initialized controller.
188+
///
189+
/// **@internal** - This method is for internal use by [NodeFlowEditor] only.
190+
/// Do not call directly from application code.
191+
///
192+
/// This should only be called after [internalInitController] has been called.
193+
/// For initial setup, use [internalInitController] instead.
194+
void internalUpdateTheme(NodeFlowTheme theme) {
195+
if (!_editorInitialized) {
196+
throw StateError(
197+
'Cannot update theme before controller is initialized. '
198+
'Call internalInitController first.',
199+
);
200+
}
201+
202+
// Update the connection painter's theme
203+
_connectionPainter?.updateTheme(theme);
204+
205+
// Update observable theme - this triggers reactions for spatial index rebuild
206+
runInAction(() => _themeObservable.value = theme);
207+
}
208+
209+
// ============================================================================
210+
// Event Updates (Post-Initialization)
211+
// ============================================================================
212+
213+
/// Updates the event handlers on an already-initialized controller.
214+
///
215+
/// **@internal** - This method is for internal use by [NodeFlowEditor] only.
216+
/// Do not call directly from application code.
217+
void internalUpdateEvents(NodeFlowEvents<T> events) {
218+
_events = events;
219+
}
220+
221+
// ============================================================================
222+
// Node Shape Builder Updates (Post-Initialization)
223+
// ============================================================================
224+
225+
/// Updates the node shape builder on an already-initialized controller.
226+
///
227+
/// **@internal** - This method is for internal use by [NodeFlowEditor] only.
228+
/// Do not call directly from application code.
229+
///
230+
/// This also updates the spatial index callback to use the new builder.
231+
void internalUpdateNodeShapeBuilder(
232+
NodeShape? Function(Node<T> node)? builder,
233+
) {
234+
_nodeShapeBuilder = builder;
235+
_spatialIndex.nodeShapeBuilder = builder;
236+
237+
// Update connection painter if it exists
238+
_connectionPainter?.updateNodeShape(
239+
builder != null ? (node) => builder(node as Node<T>) : null,
240+
);
241+
}
242+
243+
// ============================================================================
244+
// Spatial Index Reactions Setup
245+
// ============================================================================
246+
247+
/// Sets up MobX reactions for automatic spatial index synchronization.
248+
///
249+
/// These reactions ensure the spatial index stays in sync with:
250+
/// - Node additions/removals
251+
/// - Connection additions/removals
252+
/// - Theme/style changes that affect path geometry
253+
/// - Node visibility changes
254+
/// - Drag state changes
255+
///
256+
/// This is called once during [_initController] and should not be called
257+
/// directly.
258+
void _setupSpatialIndexReactions() {
259+
// === DRAG STATE FLUSH REACTION ===
260+
// When any drag ends, flush all pending spatial index updates.
261+
// This is the single point where pending updates are committed.
262+
reaction((_) => interaction.draggedNodeId.value, (String? nodeDragId) {
263+
// Only flush when drag has ended
264+
if (nodeDragId == null) {
265+
_flushPendingSpatialUpdates();
266+
}
267+
}, fireImmediately: false);
268+
269+
// === NODE ADD/REMOVE SYNC ===
270+
// When nodes are added/removed, rebuild the node spatial index
271+
reaction((_) => _nodes.keys.toSet(), (Set<String> currentNodeIds) {
272+
_spatialIndex.rebuildFromNodes(_nodes.values);
273+
}, fireImmediately: false);
274+
275+
// === CONNECTION ADD/REMOVE SYNC ===
276+
// When connections are added/removed, rebuild connection spatial index
277+
reaction((_) => _connections.map((c) => c.id).toSet(), (
278+
Set<String> connectionIds,
279+
) {
280+
// Rebuild connection-by-node index
281+
_rebuildConnectionsByNodeIndex();
282+
// Rebuild connection spatial index with proper segments
283+
rebuildAllConnectionSegments();
284+
}, fireImmediately: false);
285+
286+
// === THEME/STYLE CHANGE SYNC ===
287+
// When path-affecting theme properties change, rebuild connection segments
288+
reaction((_) => _getPathAffectingSignature(), (_) {
289+
// Theme properties that affect path geometry have changed
290+
// Rebuild all connection segments in spatial index
291+
rebuildAllConnectionSegments();
292+
}, fireImmediately: false);
293+
294+
// === NODE VISIBILITY CHANGE SYNC ===
295+
// When node visibility changes, rebuild connection segments
296+
// (connections are visible only when both endpoint nodes are visible)
297+
reaction(
298+
(_) {
299+
// Create a signature of all node visibility states
300+
return _nodes.values.map((n) => (n.id, n.isVisible)).toList();
301+
},
302+
(_) {
303+
// Rebuild connection spatial index segments
304+
// Hidden connections will return empty segments from the path cache
305+
rebuildAllConnectionSegments();
306+
},
307+
fireImmediately: false,
308+
);
309+
}
310+
311+
// ============================================================================
312+
// Node Infrastructure Setup
313+
// ============================================================================
314+
315+
/// Sets up infrastructure for nodes that were loaded before initialization.
316+
///
317+
/// This handles:
318+
/// - Setting visual positions with grid snapping
319+
/// - Attaching context for groupable nodes (GroupNode, etc.)
320+
///
321+
/// Called during [_initController] if nodes exist.
322+
void _initializeLoadedNodes() {
323+
for (final node in _nodes.values) {
324+
// Set visual position with snapping
325+
node.setVisualPosition(_config.snapToGridIfEnabled(node.position.value));
326+
327+
// Attach context for nodes with GroupableMixin (e.g., GroupNode)
328+
if (node is GroupableMixin<T>) {
329+
node.attachContext(_createGroupableContext());
330+
}
331+
}
332+
}
333+
334+
// ============================================================================
335+
// Spatial Index Rebuild
336+
// ============================================================================
337+
338+
/// Rebuilds all spatial indexes from current state.
339+
///
340+
/// This is called during initialization and whenever a full rebuild is needed.
341+
void _rebuildSpatialIndexes() {
342+
// Rebuild node and port spatial index
343+
_spatialIndex.rebuildFromNodes(_nodes.values);
344+
345+
// Rebuild connection spatial index if we have a segment calculator
346+
if (_connectionSegmentCalculator != null) {
347+
_spatialIndex.rebuildConnectionsWithSegments(
348+
_connections,
349+
_connectionSegmentCalculator!,
350+
);
351+
}
352+
}
353+
}

0 commit comments

Comments
 (0)