-
Notifications
You must be signed in to change notification settings - Fork 448
Refactor Link/Reroute layout operations and add cleanup #5345
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -39,8 +39,16 @@ import { | |
| } from '@/renderer/core/layout/types' | ||
| import { SpatialIndexManager } from '@/renderer/core/spatial/SpatialIndex' | ||
|
|
||
| type YEventChange = { | ||
| action: 'add' | 'update' | 'delete' | ||
| oldValue: unknown | ||
| } | ||
|
|
||
| const logger = log.getLogger('LayoutStore') | ||
|
|
||
| // Constants | ||
| const REROUTE_RADIUS = 8 | ||
|
|
||
| class LayoutStoreImpl implements LayoutStore { | ||
| // Yjs document and shared data structures | ||
| private ydoc = new Y.Doc() | ||
|
|
@@ -90,11 +98,11 @@ class LayoutStoreImpl implements LayoutStore { | |
| this.rerouteSpatialIndex = new SpatialIndexManager() | ||
|
|
||
| // Listen for Yjs changes and trigger Vue reactivity | ||
| this.ynodes.observe((event) => { | ||
| this.ynodes.observe((event: Y.YMapEvent<Y.Map<unknown>>) => { | ||
| this.version++ | ||
|
|
||
| // Trigger all affected node refs | ||
| event.changes.keys.forEach((_change, key) => { | ||
| event.changes.keys.forEach((_change: YEventChange, key: string) => { | ||
| const trigger = this.nodeTriggers.get(key) | ||
| if (trigger) { | ||
| trigger() | ||
|
|
@@ -103,61 +111,18 @@ class LayoutStoreImpl implements LayoutStore { | |
| }) | ||
|
|
||
| // Listen for link changes and update spatial indexes | ||
| this.ylinks.observe((event) => { | ||
| this.ylinks.observe((event: Y.YMapEvent<Y.Map<unknown>>) => { | ||
| this.version++ | ||
| event.changes.keys.forEach((change, linkIdStr) => { | ||
| const linkId = Number(linkIdStr) as LinkId | ||
| if (change.action === 'delete') { | ||
| this.linkLayouts.delete(linkId) | ||
| // Clean up any segment layouts for this link | ||
| const keysToDelete: string[] = [] | ||
| for (const [key] of this.linkSegmentLayouts) { | ||
| if (key.startsWith(`${linkId}:`)) { | ||
| keysToDelete.push(key) | ||
| } | ||
| } | ||
| for (const key of keysToDelete) { | ||
| this.linkSegmentLayouts.delete(key) | ||
| this.linkSegmentSpatialIndex.remove(key) | ||
| } | ||
| } else { | ||
| // Link was added or updated - geometry will be computed separately | ||
| // This just tracks that the link exists in CRDT | ||
| } | ||
| this.handleLinkChange(change, linkIdStr) | ||
| }) | ||
| }) | ||
|
|
||
| // Listen for reroute changes and update spatial indexes | ||
| this.yreroutes.observe((event) => { | ||
| this.yreroutes.observe((event: Y.YMapEvent<Y.Map<unknown>>) => { | ||
| this.version++ | ||
| event.changes.keys.forEach((change, rerouteIdStr) => { | ||
| // Yjs Map keys are strings, convert to number for layout operations | ||
| const rerouteId = Number(rerouteIdStr) as RerouteId | ||
|
|
||
| if (change.action === 'delete') { | ||
| this.rerouteLayouts.delete(rerouteId) // Use numeric ID for layout map | ||
| this.rerouteSpatialIndex.remove(rerouteIdStr) // Use string for spatial index | ||
| } else if (change.action === 'update' || change.action === 'add') { | ||
| const rerouteData = this.yreroutes.get(rerouteIdStr) // Use string for Yjs | ||
| if (rerouteData) { | ||
| const pos = rerouteData.get('position') as Point | ||
| if (pos) { | ||
| // Update reroute layout when position changes | ||
| const layout: RerouteLayout = { | ||
| id: rerouteId, // Use numeric ID | ||
| position: pos, | ||
| radius: 8, | ||
| bounds: { | ||
| x: pos.x - 8, | ||
| y: pos.y - 8, | ||
| width: 16, | ||
| height: 16 | ||
| } | ||
| } | ||
| this.updateRerouteLayout(rerouteId, layout) | ||
| } | ||
| } | ||
| } | ||
| this.handleRerouteChange(change, rerouteIdStr) | ||
| }) | ||
| }) | ||
| } | ||
|
|
@@ -938,16 +903,23 @@ class LayoutStoreImpl implements LayoutStore { | |
| } | ||
|
|
||
| const size = ynode.get('size') as { width: number; height: number } | ||
| ynode.set('position', operation.position) | ||
| this.updateNodeBounds(ynode, operation.position, size) | ||
|
|
||
| // Update spatial index | ||
| this.spatialIndex.update(operation.nodeId, { | ||
| const newBounds = { | ||
| x: operation.position.x, | ||
| y: operation.position.y, | ||
| width: size.width, | ||
| height: size.height | ||
| }) | ||
| } | ||
|
|
||
| // Update spatial index FIRST, synchronously to prevent race conditions | ||
| // Hit detection queries can run before CRDT updates complete | ||
| this.spatialIndex.update(operation.nodeId, newBounds) | ||
|
|
||
| // Update associated slot positions synchronously | ||
| this.updateNodeSlotPositions(operation.nodeId, operation.position) | ||
|
|
||
| // Then update CRDT | ||
| ynode.set('position', operation.position) | ||
| this.updateNodeBounds(ynode, operation.position, size) | ||
|
|
||
| change.nodeIds.push(operation.nodeId) | ||
| } | ||
|
|
@@ -960,16 +932,23 @@ class LayoutStoreImpl implements LayoutStore { | |
| if (!ynode) return | ||
|
|
||
| const position = ynode.get('position') as Point | ||
| ynode.set('size', operation.size) | ||
| this.updateNodeBounds(ynode, position, operation.size) | ||
|
|
||
| // Update spatial index | ||
| this.spatialIndex.update(operation.nodeId, { | ||
| const newBounds = { | ||
| x: position.x, | ||
| y: position.y, | ||
| width: operation.size.width, | ||
| height: operation.size.height | ||
| }) | ||
| } | ||
|
|
||
| // Update spatial index FIRST, synchronously to prevent race conditions | ||
| // Hit detection queries can run before CRDT updates complete | ||
| this.spatialIndex.update(operation.nodeId, newBounds) | ||
|
|
||
| // Update associated slot positions synchronously (size changes may affect slot positions) | ||
| this.updateNodeSlotPositions(operation.nodeId, position) | ||
|
|
||
| // Then update CRDT | ||
| ynode.set('size', operation.size) | ||
| this.updateNodeBounds(ynode, position, operation.size) | ||
|
|
||
| change.nodeIds.push(operation.nodeId) | ||
| } | ||
|
|
@@ -1015,6 +994,18 @@ class LayoutStoreImpl implements LayoutStore { | |
| // Clean up associated slot layouts | ||
| this.deleteNodeSlotLayouts(operation.nodeId) | ||
|
|
||
| // Clean up associated links | ||
| const linksToDelete = this.findLinksConnectedToNode(operation.nodeId) | ||
|
|
||
| // Delete the associated links | ||
| for (const linkId of linksToDelete) { | ||
| this.ylinks.delete(String(linkId)) | ||
| this.linkLayouts.delete(linkId) | ||
|
|
||
| // Clean up link segment layouts | ||
| this.cleanupLinkSegments(linkId) | ||
| } | ||
|
|
||
| change.type = 'delete' | ||
| change.nodeIds.push(operation.nodeId) | ||
| } | ||
|
|
@@ -1046,16 +1037,7 @@ class LayoutStoreImpl implements LayoutStore { | |
| this.ylinks.delete(String(operation.linkId)) | ||
| this.linkLayouts.delete(operation.linkId) | ||
| // Clean up any segment layouts for this link | ||
| const keysToDelete: string[] = [] | ||
| for (const [key] of this.linkSegmentLayouts) { | ||
| if (key.startsWith(`${operation.linkId}:`)) { | ||
| keysToDelete.push(key) | ||
| } | ||
| } | ||
| for (const key of keysToDelete) { | ||
| this.linkSegmentLayouts.delete(key) | ||
| this.linkSegmentSpatialIndex.remove(key) | ||
| } | ||
| this.cleanupLinkSegments(operation.linkId) | ||
|
|
||
| change.type = 'delete' | ||
| } | ||
|
|
@@ -1132,6 +1114,147 @@ class LayoutStoreImpl implements LayoutStore { | |
| }) | ||
| } | ||
|
|
||
| /** | ||
| * Find all links connected to a specific node | ||
| */ | ||
| private findLinksConnectedToNode(nodeId: NodeId): LinkId[] { | ||
| const connectedLinks: LinkId[] = [] | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. TODO: switch to adjacency list. |
||
| this.ylinks.forEach((linkData: Y.Map<unknown>, linkIdStr: string) => { | ||
| const linkId = Number(linkIdStr) as LinkId | ||
| const sourceNodeId = linkData.get('sourceNodeId') as NodeId | ||
| const targetNodeId = linkData.get('targetNodeId') as NodeId | ||
|
|
||
| if (sourceNodeId === nodeId || targetNodeId === nodeId) { | ||
| connectedLinks.push(linkId) | ||
| } | ||
| }) | ||
| return connectedLinks | ||
| } | ||
|
|
||
| /** | ||
| * Handle link change events | ||
| */ | ||
| private handleLinkChange(change: YEventChange, linkIdStr: string): void { | ||
| if (change.action === 'delete') { | ||
| const linkId = Number(linkIdStr) as LinkId | ||
| this.cleanupLinkData(linkId) | ||
| } | ||
| // Link was added or updated - geometry will be computed separately | ||
| // This just tracks that the link exists in CRDT | ||
| } | ||
|
|
||
| /** | ||
| * Clean up all data associated with a link | ||
| */ | ||
| private cleanupLinkData(linkId: LinkId): void { | ||
| this.linkLayouts.delete(linkId) | ||
| this.cleanupLinkSegments(linkId) | ||
| } | ||
|
|
||
| /** | ||
| * Clean up all segment layouts for a link | ||
| */ | ||
| private cleanupLinkSegments(linkId: LinkId): void { | ||
| const keysToDelete: string[] = [] | ||
| for (const [key] of this.linkSegmentLayouts) { | ||
| if (key.startsWith(`${linkId}:`)) { | ||
| keysToDelete.push(key) | ||
| } | ||
| } | ||
|
|
||
| for (const key of keysToDelete) { | ||
| this.linkSegmentLayouts.delete(key) | ||
| this.linkSegmentSpatialIndex.remove(key) | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Handle reroute change events | ||
| */ | ||
| private handleRerouteChange( | ||
| change: YEventChange, | ||
| rerouteIdStr: string | ||
| ): void { | ||
| const rerouteId = Number(rerouteIdStr) as RerouteId | ||
|
|
||
| if (change.action === 'delete') { | ||
| this.handleRerouteDelete(rerouteId, rerouteIdStr) | ||
| } else if (change.action === 'update' || change.action === 'add') { | ||
| this.handleRerouteAddOrUpdate(rerouteId, rerouteIdStr) | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Handle reroute deletion | ||
| */ | ||
| private handleRerouteDelete( | ||
| rerouteId: RerouteId, | ||
| rerouteIdStr: string | ||
| ): void { | ||
| this.rerouteLayouts.delete(rerouteId) | ||
| this.rerouteSpatialIndex.remove(rerouteIdStr) | ||
| } | ||
|
|
||
| /** | ||
| * Handle reroute add or update | ||
| */ | ||
| private handleRerouteAddOrUpdate( | ||
christian-byrne marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| rerouteId: RerouteId, | ||
| rerouteIdStr: string | ||
| ): void { | ||
| const rerouteData = this.yreroutes.get(rerouteIdStr) | ||
| if (!rerouteData) return | ||
|
|
||
| const position = rerouteData.get('position') as Point | ||
christian-byrne marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| if (!position) return | ||
|
|
||
| const layout = this.createRerouteLayout(rerouteId, position) | ||
| this.updateRerouteLayout(rerouteId, layout) | ||
| } | ||
|
|
||
| /** | ||
| * Create reroute layout from position | ||
| */ | ||
| private createRerouteLayout( | ||
| rerouteId: RerouteId, | ||
| position: Point | ||
| ): RerouteLayout { | ||
| return { | ||
| id: rerouteId, | ||
| position, | ||
| radius: REROUTE_RADIUS, | ||
| bounds: { | ||
| x: position.x - REROUTE_RADIUS, | ||
| y: position.y - REROUTE_RADIUS, | ||
| width: REROUTE_RADIUS * 2, | ||
| height: REROUTE_RADIUS * 2 | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Update slot positions when a node moves | ||
| * TODO: This should be handled by the layout sync system (useSlotLayoutSync) | ||
| * rather than manually here. For now, we'll mark affected slots as needing recalculation. | ||
christian-byrne marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| */ | ||
| private updateNodeSlotPositions(nodeId: NodeId, _nodePosition: Point): void { | ||
| // Mark all slots for this node as potentially stale | ||
| // The layout sync system will recalculate positions on the next frame | ||
| const slotsToRemove: string[] = [] | ||
|
|
||
| for (const [key, slotLayout] of this.slotLayouts) { | ||
| if (slotLayout.nodeId === nodeId) { | ||
| slotsToRemove.push(key) | ||
| } | ||
| } | ||
|
|
||
| // Remove from spatial index so they'll be recalculated | ||
| for (const key of slotsToRemove) { | ||
| this.slotSpatialIndex.remove(key) | ||
| this.slotLayouts.delete(key) | ||
| } | ||
| } | ||
|
|
||
| // Helper methods | ||
| private layoutToYNode(layout: NodeLayout): Y.Map<unknown> { | ||
| const ynode = new Y.Map<unknown>() | ||
|
|
@@ -1186,7 +1309,7 @@ class LayoutStoreImpl implements LayoutStore { | |
| // CRDT-specific methods | ||
| getOperationsSince(timestamp: number): LayoutOperation[] { | ||
| const operations: LayoutOperation[] = [] | ||
| this.yoperations.forEach((op) => { | ||
| this.yoperations.forEach((op: LayoutOperation) => { | ||
| if (op && op.timestamp > timestamp) { | ||
| operations.push(op) | ||
| } | ||
|
|
@@ -1196,7 +1319,7 @@ class LayoutStoreImpl implements LayoutStore { | |
|
|
||
| getOperationsByActor(actor: string): LayoutOperation[] { | ||
| const operations: LayoutOperation[] = [] | ||
| this.yoperations.forEach((op) => { | ||
| this.yoperations.forEach((op: LayoutOperation) => { | ||
| if (op && op.actor === actor) { | ||
| operations.push(op) | ||
| } | ||
|
|
||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TODO: share this constant with LiteGraph or link rendering service.