Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 68 additions & 10 deletions src/renderer/core/layout/store/layoutStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import * as Y from 'yjs'

import { ACTOR_CONFIG } from '@/renderer/core/layout/constants'
import type {
BatchUpdateBoundsOperation,
CreateLinkOperation,
CreateNodeOperation,
CreateRerouteOperation,
Expand Down Expand Up @@ -864,6 +865,12 @@ class LayoutStoreImpl implements LayoutStore {
case 'deleteNode':
this.handleDeleteNode(operation as DeleteNodeOperation, change)
break
case 'batchUpdateBounds':
this.handleBatchUpdateBounds(
operation as BatchUpdateBoundsOperation,
change
)
break
case 'createLink':
this.handleCreateLink(operation as CreateLinkOperation, change)
break
Expand Down Expand Up @@ -1092,6 +1099,38 @@ class LayoutStoreImpl implements LayoutStore {
change.nodeIds.push(operation.nodeId)
}

private handleBatchUpdateBounds(
operation: BatchUpdateBoundsOperation,
change: LayoutChange
): void {
const spatialUpdates: Array<{ nodeId: NodeId; bounds: Bounds }> = []

for (const nodeId of operation.nodeIds) {
const data = operation.bounds[nodeId]
const ynode = this.ynodes.get(nodeId)
if (!ynode || !data) continue

ynode.set('position', { x: data.bounds.x, y: data.bounds.y })
ynode.set('size', {
width: data.bounds.width,
height: data.bounds.height
})
ynode.set('bounds', data.bounds)

spatialUpdates.push({ nodeId, bounds: data.bounds })
change.nodeIds.push(nodeId)
}

// Batch update spatial index for better performance
if (spatialUpdates.length > 0) {
this.spatialIndex.batchUpdate(spatialUpdates)
}

if (change.nodeIds.length) {
change.type = 'update'
}
}

private handleCreateLink(
operation: CreateLinkOperation,
change: LayoutChange
Expand Down Expand Up @@ -1372,19 +1411,38 @@ class LayoutStoreImpl implements LayoutStore {
const originalSource = this.currentSource
this.currentSource = LayoutSource.Vue

this.ydoc.transact(() => {
for (const { nodeId, bounds } of updates) {
const ynode = this.ynodes.get(nodeId)
if (!ynode) continue
const nodeIds: NodeId[] = []
const boundsRecord: BatchUpdateBoundsOperation['bounds'] = {}

this.spatialIndex.update(nodeId, bounds)
ynode.set('bounds', bounds)
ynode.set('position', { x: bounds.x, y: bounds.y })
ynode.set('size', { width: bounds.width, height: bounds.height })
for (const { nodeId, bounds } of updates) {
const ynode = this.ynodes.get(nodeId)
if (!ynode) continue
const currentLayout = yNodeToLayout(ynode)

boundsRecord[nodeId] = {
bounds,
previousBounds: currentLayout.bounds
}
}, this.currentActor)
nodeIds.push(nodeId)
}

if (!nodeIds.length) {
this.currentSource = originalSource
return
}

const operation: BatchUpdateBoundsOperation = {
type: 'batchUpdateBounds',
entity: 'node',
nodeIds,
bounds: boundsRecord,
timestamp: Date.now(),
source: this.currentSource,
actor: this.currentActor
}

this.applyOperation(operation)

// Restore original source
this.currentSource = originalSource
}
}
Expand Down
5 changes: 5 additions & 0 deletions src/renderer/core/layout/sync/useLinkLayoutSync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,11 @@ export function useLinkLayoutSync() {
case 'resizeNode':
recomputeLinksForNode(parseInt(change.operation.nodeId))
break
case 'batchUpdateBounds':
for (const nodeId of change.operation.nodeIds) {
recomputeLinksForNode(parseInt(nodeId))
}
break
case 'createLink':
recomputeLinkById(change.operation.linkId)
break
Expand Down
13 changes: 7 additions & 6 deletions src/renderer/core/layout/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ type OperationType =
| 'createNode'
| 'deleteNode'
| 'setNodeVisibility'
| 'batchUpdate'
| 'batchUpdateBounds'
| 'createLink'
| 'deleteLink'
| 'createReroute'
Expand Down Expand Up @@ -184,10 +184,11 @@ interface SetNodeVisibilityOperation extends NodeOpBase {
/**
* Batch update operation for atomic multi-property changes
*/
interface BatchUpdateOperation extends NodeOpBase {
type: 'batchUpdate'
updates: Partial<NodeLayout>
previousValues: Partial<NodeLayout>
export interface BatchUpdateBoundsOperation extends OperationMeta {
entity: 'node'
type: 'batchUpdateBounds'
nodeIds: NodeId[]
bounds: Record<NodeId, { bounds: Bounds; previousBounds: Bounds }>
}

/**
Expand Down Expand Up @@ -244,7 +245,7 @@ export type LayoutOperation =
| CreateNodeOperation
| DeleteNodeOperation
| SetNodeVisibilityOperation
| BatchUpdateOperation
| BatchUpdateBoundsOperation
| CreateLinkOperation
| DeleteLinkOperation
| CreateRerouteOperation
Expand Down
11 changes: 11 additions & 0 deletions src/renderer/core/spatial/SpatialIndex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,17 @@ export class SpatialIndexManager {
this.invalidateCache()
}

/**
* Batch update multiple nodes' bounds in the spatial index
* More efficient than calling update() multiple times as it only invalidates cache once
*/
batchUpdate(updates: Array<{ nodeId: NodeId; bounds: Bounds }>): void {
for (const { nodeId, bounds } of updates) {
this.quadTree.update(nodeId, bounds)
}
this.invalidateCache()
}

/**
* Remove a node from the spatial index
*/
Expand Down
23 changes: 15 additions & 8 deletions src/renderer/extensions/vueNodes/components/LGraphNode.vue
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ import {
import { cn } from '@/utils/tailwindUtil'

import { useNodeResize } from '../composables/useNodeResize'
import { calculateIntrinsicSize } from '../utils/calculateIntrinsicSize'
import NodeContent from './NodeContent.vue'
import NodeHeader from './NodeHeader.vue'
import NodeSlots from './NodeSlots.vue'
Expand Down Expand Up @@ -245,7 +246,7 @@ onErrorCaptured((error) => {
})

// Use layout system for node position and dragging
const { position, size, zIndex, resize } = useNodeLayout(() => nodeData.id)
const { position, size, zIndex } = useNodeLayout(() => nodeData.id)
const { pointerHandlers, isDragging, dragStyle } = useNodePointerInteractions(
() => nodeData,
handleNodeSelect
Expand All @@ -267,13 +268,19 @@ const handleContextMenu = (event: MouseEvent) => {
}

onMounted(() => {
if (size.value && transformState?.camera) {
const scale = transformState.camera.z
const screenSize = {
width: size.value.width * scale,
height: size.value.height * scale
}
resize(screenSize)
// Set initial DOM size from layout store, but respect intrinsic content minimum
if (size.value && nodeContainerRef.value && transformState) {
const intrinsicMin = calculateIntrinsicSize(
nodeContainerRef.value,
transformState.camera.z
)

// Use the larger of stored size or intrinsic minimum
const finalWidth = Math.max(size.value.width, intrinsicMin.width)
const finalHeight = Math.max(size.value.height, intrinsicMin.height)

nodeContainerRef.value.style.width = `${finalWidth}px`
nodeContainerRef.value.style.height = `${finalHeight}px`
}
})

Expand Down
24 changes: 6 additions & 18 deletions src/renderer/extensions/vueNodes/composables/useNodeResize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useEventListener } from '@vueuse/core'
import { ref } from 'vue'

import type { TransformState } from '@/renderer/core/layout/injectionKeys'
import { calculateIntrinsicSize } from '@/renderer/extensions/vueNodes/utils/calculateIntrinsicSize'

interface Size {
width: number
Expand Down Expand Up @@ -53,29 +54,16 @@ export function useNodeResize(
if (!(nodeElement instanceof HTMLElement)) return

const rect = nodeElement.getBoundingClientRect()

// Calculate intrinsic content size once at start
const originalWidth = nodeElement.style.width
const originalHeight = nodeElement.style.height
nodeElement.style.width = 'auto'
nodeElement.style.height = 'auto'

const intrinsicRect = nodeElement.getBoundingClientRect()

// Restore original size
nodeElement.style.width = originalWidth
nodeElement.style.height = originalHeight

// Convert to canvas coordinates using transform state
const scale = transformState.camera.z

// Calculate current size in canvas coordinates
resizeStartSize.value = {
width: rect.width / scale,
height: rect.height / scale
}
intrinsicMinSize.value = {
width: intrinsicRect.width / scale,
height: intrinsicRect.height / scale
}

// Calculate intrinsic content size (minimum based on content)
intrinsicMinSize.value = calculateIntrinsicSize(nodeElement, scale)

const handlePointerMove = (moveEvent: PointerEvent) => {
if (
Expand Down
9 changes: 0 additions & 9 deletions src/renderer/extensions/vueNodes/layout/useNodeLayout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,14 +167,6 @@ export function useNodeLayout(nodeIdMaybe: MaybeRefOrGetter<string>) {
mutations.moveNode(nodeId, position)
}

/**
* Update node size
*/
function resize(newSize: { width: number; height: number }) {
mutations.setSource(LayoutSource.Vue)
mutations.resizeNode(nodeId, newSize)
}

return {
// Reactive state (via customRef)
layoutRef,
Expand All @@ -187,7 +179,6 @@ export function useNodeLayout(nodeIdMaybe: MaybeRefOrGetter<string>) {

// Mutations
moveTo,
resize,

// Drag handlers
startDrag,
Expand Down
Loading
Loading