Added block height to state and made loop relative to block height and position

This commit is contained in:
Emir Karabeg
2025-02-11 21:36:52 -08:00
parent 611ed7da15
commit ddfa86641b
4 changed files with 103 additions and 25 deletions
@@ -30,6 +30,7 @@ export function WorkflowBlock({ id, data, selected }: NodeProps<WorkflowBlockPro
// Refs
const blockRef = useRef<HTMLDivElement>(null)
const contentRef = useRef<HTMLDivElement>(null)
const updateNodeInternals = useUpdateNodeInternals()
// Store selectors
@@ -38,16 +39,65 @@ export function WorkflowBlock({ id, data, selected }: NodeProps<WorkflowBlockPro
(state) => state.blocks[id]?.horizontalHandles ?? false
)
const isWide = useWorkflowStore((state) => state.blocks[id]?.isWide ?? false)
const blockHeight = useWorkflowStore((state) => state.blocks[id]?.height ?? 0)
// Store actions
const updateBlockName = useWorkflowStore((state) => state.updateBlockName)
const toggleBlockWide = useWorkflowStore((state) => state.toggleBlockWide)
const updateBlockHeight = useWorkflowStore((state) => state.updateBlockHeight)
// Update node internals when handles change
useEffect(() => {
updateNodeInternals(id)
}, [id, horizontalHandles, updateNodeInternals])
// Add debounce helper
const debounce = (func: Function, wait: number) => {
let timeout: NodeJS.Timeout
return (...args: any[]) => {
clearTimeout(timeout)
timeout = setTimeout(() => func(...args), wait)
}
}
// Add effect to observe size changes with debounced updates
useEffect(() => {
if (!contentRef.current) return
let rafId: number
const debouncedUpdate = debounce((height: number) => {
if (height !== blockHeight) {
updateBlockHeight(id, height)
updateNodeInternals(id)
}
}, 100)
const resizeObserver = new ResizeObserver((entries) => {
// Cancel any pending animation frame
if (rafId) {
cancelAnimationFrame(rafId)
}
// Schedule the update on the next animation frame
rafId = requestAnimationFrame(() => {
for (const entry of entries) {
const height =
entry.borderBoxSize[0]?.blockSize ?? entry.target.getBoundingClientRect().height
debouncedUpdate(height)
}
})
})
resizeObserver.observe(contentRef.current)
return () => {
resizeObserver.disconnect()
if (rafId) {
cancelAnimationFrame(rafId)
}
}
}, [id, blockHeight, updateBlockHeight, updateNodeInternals])
// SubBlock layout management
function groupSubBlocks(subBlocks: SubBlockConfig[]) {
const visibleSubBlocks = subBlocks.filter((block) => !block.hidden)
@@ -185,7 +235,7 @@ export function WorkflowBlock({ id, data, selected }: NodeProps<WorkflowBlockPro
</div>
{/* Block Content */}
<div className="px-4 pt-3 pb-4 space-y-4 cursor-pointer">
<div ref={contentRef} className="px-4 pt-3 pb-4 space-y-4 cursor-pointer">
{subBlockRows.map((row, rowIndex) => (
<div key={`row-${rowIndex}`} className="flex gap-4">
{row.map((subBlock, blockIndex) => (
@@ -34,22 +34,33 @@ function calculateLoopBounds(loop: Loop, blocks: Record<string, any>) {
// Calculate bounds of all blocks in loop
const bound = loopBlocks.reduce(
(acc, block) => {
// Calculate block dimensions
const blockWidth = block.isWide ? 480 : 320
const blockHeight = block.height || 200 // Fallback height if not set
// Update bounds
acc.minX = Math.min(acc.minX, block.position.x)
acc.minY = Math.min(acc.minY, block.position.y)
acc.maxX = Math.max(acc.maxX, block.position.x + (block.isWide ? 480 : 320))
acc.maxY = Math.max(acc.maxY, block.position.y + 200)
acc.maxX = Math.max(acc.maxX, block.position.x + blockWidth)
acc.maxY = Math.max(acc.maxY, block.position.y + blockHeight)
return acc
},
{ minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity }
)
// Add padding around the group
const PADDING = 50
// Add padding around the group with extra bottom padding
const PADDING = {
TOP: 50,
RIGHT: 50,
BOTTOM: 110,
LEFT: 50,
}
return {
x: bound.minX - PADDING,
y: bound.minY - PADDING,
width: bound.maxX - bound.minX + PADDING * 2,
height: bound.maxY - bound.minY + PADDING * 2,
x: bound.minX - PADDING.LEFT,
y: bound.minY - PADDING.TOP,
width: bound.maxX - bound.minX + PADDING.LEFT + PADDING.RIGHT,
height: bound.maxY - bound.minY + PADDING.TOP + PADDING.BOTTOM,
}
}
+31 -16
View File
@@ -4,7 +4,7 @@ import { devtools } from 'zustand/middleware'
import { getBlock } from '@/blocks'
import { resolveOutputType } from '@/blocks/utils'
import { WorkflowStoreWithHistory, pushHistory, withHistory } from './middleware'
import { Position, SubBlockState, Loop } from './types'
import { Loop, Position, SubBlockState } from './types'
import { detectCycle } from './utils'
const initialState = {
@@ -144,6 +144,8 @@ export const useWorkflowStore = create<WorkflowStoreWithHistory>()(
outputs,
enabled: true,
horizontalHandles: true,
isWide: false,
height: 0,
},
},
edges: [...get().edges],
@@ -186,7 +188,7 @@ export const useWorkflowStore = create<WorkflowStoreWithHistory>()(
// Otherwise, just remove the node from the loop
newState.loops[loopId] = {
...loop,
nodes: loop.nodes.filter((nodeId) => nodeId !== id)
nodes: loop.nodes.filter((nodeId) => nodeId !== id),
}
}
}
@@ -208,31 +210,31 @@ export const useWorkflowStore = create<WorkflowStoreWithHistory>()(
sourceHandle: edge.sourceHandle,
targetHandle: edge.targetHandle,
}
const newEdges = [...get().edges, newEdge]
// Recalculate all loops after adding the edge
const newLoops: Record<string, Loop> = {}
const processedPaths = new Set<string>()
// Check for cycles from each node
const nodes = new Set(newEdges.map(e => e.source))
nodes.forEach(node => {
const nodes = new Set(newEdges.map((e) => e.source))
nodes.forEach((node) => {
const { paths } = detectCycle(newEdges, node)
paths.forEach(path => {
paths.forEach((path) => {
// Create a canonical path representation for deduplication
const canonicalPath = [...path].sort().join(',')
if (!processedPaths.has(canonicalPath)) {
const loopId = crypto.randomUUID()
newLoops[loopId] = {
id: loopId,
nodes: path
nodes: path,
}
processedPaths.add(canonicalPath)
}
})
})
const newState = {
blocks: { ...get().blocks },
edges: newEdges,
@@ -246,23 +248,23 @@ export const useWorkflowStore = create<WorkflowStoreWithHistory>()(
removeEdge: (edgeId: string) => {
const newEdges = get().edges.filter((edge) => edge.id !== edgeId)
// Recalculate all loops after edge removal
const newLoops: Record<string, Loop> = {}
const processedPaths = new Set<string>()
// Check for cycles from each node
const nodes = new Set(newEdges.map(e => e.source))
nodes.forEach(node => {
const nodes = new Set(newEdges.map((e) => e.source))
nodes.forEach((node) => {
const { paths } = detectCycle(newEdges, node)
paths.forEach(path => {
paths.forEach((path) => {
// Create a canonical path representation for deduplication
const canonicalPath = [...path].sort().join(',')
if (!processedPaths.has(canonicalPath)) {
const loopId = crypto.randomUUID()
newLoops[loopId] = {
id: loopId,
nodes: path
nodes: path,
}
processedPaths.add(canonicalPath)
}
@@ -413,6 +415,19 @@ export const useWorkflowStore = create<WorkflowStoreWithHistory>()(
loops: { ...get().loops },
}))
},
updateBlockHeight: (id: string, height: number) => {
set((state) => ({
blocks: {
...state.blocks,
[id]: {
...state.blocks[id],
height,
},
},
edges: [...state.edges],
}))
},
})),
{ name: 'workflow-store' }
)
+2
View File
@@ -16,6 +16,7 @@ export interface BlockState {
enabled: boolean
horizontalHandles?: boolean
isWide?: boolean
height?: number
}
export interface SubBlockState {
@@ -50,6 +51,7 @@ export interface WorkflowActions {
toggleBlockHandles: (id: string) => void
updateBlockName: (id: string, name: string) => void
toggleBlockWide: (id: string) => void
updateBlockHeight: (id: string, height: number) => void
}
export type WorkflowStore = WorkflowState & WorkflowActions