improve flow centering

This commit is contained in:
Guilhem
2025-11-19 12:36:39 +00:00
parent 0aa5e13229
commit c94f879be9
5 changed files with 74 additions and 68 deletions
@@ -53,7 +53,6 @@
import SubflowBound from './renderers/nodes/SubflowBound.svelte'
import ViewportResizer from './ViewportResizer.svelte'
import ViewportSynchronizer from './ViewportSynchronizer.svelte'
import InitialViewportFitter from './InitialViewportFitter.svelte'
import AssetNode, { computeAssetNodes } from './renderers/nodes/AssetNode.svelte'
import AssetsOverflowedNode from './renderers/nodes/AssetsOverflowedNode.svelte'
import type { FlowGraphAssetContext } from '../flows/types'
@@ -248,12 +247,33 @@
const noteEditorContext = getNoteEditorContext()
// Function to calculate extra gap needed for notes below the lowest flow nodes
function calculateNoteGap(notes: FlowNote[] | undefined): number {
if (!notes || notes.length === 0) {
return 0
}
let maxNoteBelowGap = 0
notes.forEach((note) => {
if (note.position?.y && note.position.y < 0) {
maxNoteBelowGap = Math.max(maxNoteBelowGap, -note.position.y)
}
})
return maxNoteBelowGap
}
// Calculate note gap based on current nodes and notes
const topPadding = editMode ? 100 : 24
const yOffset = calculateNoteGap(notes) + topPadding
setGraphContext({
selectionManager: selectionManager,
useDataflow,
showAssets,
noteManager,
clearFlowSelection
clearFlowSelection,
yOffset
} as any)
if (triggerContext && allowSimplifiedPoll) {
@@ -438,8 +458,20 @@
let height = $state(0)
// Counter to trigger initial viewport fit after first flow build
let initialBuildTrigger = $state(0)
// Derived nodes with yOffset applied to all nodes uniformly
const nodesWithOffset = $derived.by(() => {
return nodes.map((node) => {
if (
node.type !== 'asset' &&
node.type !== 'assetsOverflowed' &&
node.type !== 'newAiTool' &&
node.type !== 'aiTool'
) {
return { ...node, position: { ...node.position, y: node.position.y + yOffset } }
}
return node
})
})
// Note feature state
@@ -562,7 +594,13 @@
]
await tick()
height = Math.max(...nodes.map((n) => n.position.y + NODE.height + 100), minHeight)
if (nodes.length === 0) {
height = minHeight
} else {
const minY = Math.min(...nodes.map((n) => n.position.y))
const maxBottom = Math.max(...nodes.map((n) => n.position.y + NODE.height + 100))
height = Math.max(maxBottom - minY, minHeight)
}
}
const nodeTypes = {
@@ -660,10 +698,6 @@
;[graph, allowSimplifiedPoll, $showAssets, showNotes, noteManager.renderCount]
untrack(async () => {
await updateStores()
// Trigger initial viewport fit after first build
if (initialBuildTrigger === 0 && nodes.length > 0) {
initialBuildTrigger = 1
}
})
})
@@ -809,7 +843,6 @@
bind:this={viewportSynchronizer}
/>
{/if}
<InitialViewportFitter {nodes} triggerCount={initialBuildTrigger} />
<PaneContextMenu {editMode} bind:this={paneContextMenu} />
<SvelteFlow
onpaneclick={() => {
@@ -822,13 +855,17 @@
onnodedragstop={(event) => {
const node = event.targetNode
if (node && node.type === 'note') {
onNotePositionUpdate?.(node.id, node.position)
const positionWithOffset = {
x: node.position.x,
y: node.position.y - yOffset
}
onNotePositionUpdate?.(node.id, positionWithOffset)
}
}}
onmove={(event, viewport) => {
viewportSynchronizer?.handleLocalViewportChange(event, viewport)
}}
{nodes}
nodes={nodesWithOffset}
{edges}
{edgeTypes}
{nodeTypes}
@@ -855,7 +892,7 @@
<div class="absolute inset-0 !bg-surface-secondary h-full" id="flow-graph-v2"></div>
{#if noteMode}
<NoteTool {exitNoteMode} />
<NoteTool {exitNoteMode} {yOffset} />
{/if}
{#if multiSelectEnabled}
@@ -865,7 +902,10 @@
!id.startsWith('Settings') && !id.startsWith('Trigger') && !id.startsWith('Result')
)}
>
<SelectionBoundingBox selectedNodes={selectionManager.selectedIds} allNodes={nodes} />
<SelectionBoundingBox
selectedNodes={selectionManager.selectedIds}
allNodes={nodesWithOffset}
/>
</NodeContextMenu>
{/if}
@@ -1,50 +0,0 @@
<script lang="ts">
import { useSvelteFlow } from '@xyflow/svelte'
import { tick, untrack } from 'svelte'
interface Props {
nodes: any[]
triggerCount: number
}
let { nodes, triggerCount }: Props = $props()
const { fitView } = useSvelteFlow()
let hasRunInitialFit = false
/**
* Smart fitView that fits all nodes and notes with minimal zoom adjustment
* Allows zoom to change by ±0.2 to fit content if needed
* Only runs once after the first flow build
*/
async function smartFitView() {
if (hasRunInitialFit || nodes.length === 0) {
return
}
await tick()
try {
fitView({
minZoom: Math.max(1),
maxZoom: Math.min(1),
padding: 0.2,
duration: 0
})
hasRunInitialFit = true
} catch (e) {
console.debug('smartFitView error:', e)
}
}
// Run when nodes are ready and trigger count changes (after first build)
$effect(() => {
triggerCount
untrack(() => {
if (!hasRunInitialFit && nodes.length > 0) {
smartFitView()
}
})
})
</script>
@@ -7,9 +7,10 @@
interface Props {
exitNoteMode?: () => void
yOffset: number
}
let { exitNoteMode }: Props = $props()
let { exitNoteMode, yOffset }: Props = $props()
// Get NoteEditor context for direct note creation
const noteEditorContext = getNoteEditorContext()
@@ -61,10 +62,14 @@
y: endPosition.y + rect.top
}
const position = screenToFlowPosition({
const flowPosition = screenToFlowPosition({
x: Math.min(absoluteStartPosition.x, absoluteEndPosition.x),
y: Math.min(absoluteStartPosition.y, absoluteEndPosition.y)
})
const position = {
x: flowPosition.x,
y: flowPosition.y - (yOffset || 0)
}
const zoom = getViewport().zoom
const size = {
@@ -155,10 +160,14 @@
onpointerup={onPointerUp}
oncontextmenu={(e) => {
// Capture the position when context menu is triggered
contextMenuPosition = screenToFlowPosition({
const flowPosition = screenToFlowPosition({
x: e.clientX,
y: e.clientY
})
contextMenuPosition = {
x: flowPosition.x,
y: flowPosition.y - yOffset
}
}}
role="button"
tabindex="0"
@@ -9,6 +9,7 @@
CONTEXT_MENU_ITEM_BASE_CLASS,
CONTEXT_MENU_ITEM_HOVER_CLASS
} from '../common/contextmenu/contextMenuStyles'
import { getGraphContext } from './graphContext'
interface Props {
editMode?: boolean
@@ -19,6 +20,8 @@
const { screenToFlowPosition } = useSvelteFlow()
const noteEditorContext = getNoteEditorContext()
const graphContext = getGraphContext()
let contextMenuVisible = $state(false)
let contextMenuPosition = $state<{ x: number; y: number }>({ x: 0, y: 0 })
let pendingFlowPosition = $state<{ x: number; y: number } | null>(null)
@@ -51,7 +54,10 @@
if (noteEditorContext?.noteEditor && pendingFlowPosition) {
noteEditorContext.noteEditor.addNote({
text: '### Free note\nDouble click to edit me',
position: pendingFlowPosition,
position: {
x: pendingFlowPosition.x,
y: pendingFlowPosition.y - (graphContext?.yOffset || 0)
},
size: { width: 300, height: 200 },
color: DEFAULT_NOTE_COLOR,
type: 'free',
@@ -9,6 +9,7 @@ export type GraphContext = {
showAssets: Writable<boolean | undefined>
noteManager?: NoteManager
clearFlowSelection?: () => void
yOffset?: number
}
const graphContextKey = 'FlowGraphContext'