mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-09 16:05:42 +00:00
improve adaptive layout to group note
This commit is contained in:
@@ -71,6 +71,7 @@
|
||||
import type { AssetWithAltAccessType } from '../assets/lib'
|
||||
import type { AIModuleAction } from '../copilot/chat/flow/core'
|
||||
import { setGraphContext } from './graphContext'
|
||||
import { computeGroupNoteSpacing } from './groupNoteSpacing'
|
||||
|
||||
let useDataflow: Writable<boolean | undefined> = writable<boolean | undefined>(false)
|
||||
let showAssets: Writable<boolean | undefined> = writable<boolean | undefined>(true)
|
||||
@@ -318,7 +319,7 @@
|
||||
}
|
||||
|
||||
const yOffset = insertable ? 100 : 0
|
||||
const initialNodes = dag.descendants().map((des) => ({
|
||||
return dag.descendants().map((des) => ({
|
||||
id: des.data.id,
|
||||
position: {
|
||||
x: des.x
|
||||
@@ -334,45 +335,6 @@
|
||||
y: (des.y || 0) + yOffset
|
||||
}
|
||||
}))
|
||||
|
||||
// Apply group note spacing adjustments
|
||||
// First, collect all nodes that need spacing above them
|
||||
const spacingMap = new Map<string, number>()
|
||||
for (const node of initialNodes) {
|
||||
const groupNoteHeight = noteManager.getGroupNoteHeightForNode(
|
||||
notes ?? [],
|
||||
node.id,
|
||||
initialNodes,
|
||||
noteTextHeights
|
||||
)
|
||||
if (groupNoteHeight > 0) {
|
||||
spacingMap.set(node.id, groupNoteHeight)
|
||||
}
|
||||
}
|
||||
|
||||
// Apply spacing - move nodes down by the cumulative spacing above them
|
||||
const adjustedNodes = initialNodes.map((node) => {
|
||||
let totalSpacingAbove = 0
|
||||
|
||||
// Calculate total spacing needed above this node from all group notes above it
|
||||
for (const [spacingNodeId, spacing] of spacingMap) {
|
||||
const spacingNode = initialNodes.find((n) => n.id === spacingNodeId)
|
||||
if (spacingNode && spacingNode.position.y <= node.position.y) {
|
||||
totalSpacingAbove += spacing
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...node,
|
||||
position: {
|
||||
...node.position,
|
||||
y: node.position.y + totalSpacingAbove
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
lastNodes = [nodes, adjustedNodes]
|
||||
return adjustedNodes
|
||||
}
|
||||
|
||||
let eventHandler = {
|
||||
@@ -468,7 +430,7 @@
|
||||
actualSelectionManager.handleKeyDown(event, nodes)
|
||||
}
|
||||
|
||||
function handleKeyUp(event: KeyboardEvent) {
|
||||
function handleKeyUp(_event: KeyboardEvent) {
|
||||
// Keep for potential future use
|
||||
}
|
||||
|
||||
@@ -507,6 +469,17 @@
|
||||
)
|
||||
let newNodes: (Node & NodeLayout)[] = layoutedNodes.map((n) => ({ ...n, ...graph.nodes[n.id] }))
|
||||
|
||||
// Apply group note spacing adjustments
|
||||
const groupNoteSpacingResult = computeGroupNoteSpacing(
|
||||
newNodes.map((n) => ({ id: n.id, position: n.position })),
|
||||
notes ?? [],
|
||||
noteTextHeights
|
||||
)
|
||||
newNodes = newNodes.map((n) => ({
|
||||
...n,
|
||||
position: groupNoteSpacingResult.newNodePositions[n.id] || n.position
|
||||
}))
|
||||
|
||||
let assetNodesResult = $showAssets
|
||||
? computeAssetNodes(
|
||||
newNodes.map((n) => ({
|
||||
@@ -720,7 +693,6 @@
|
||||
}
|
||||
|
||||
$inspect('dbg notes & nodes', notes, nodes)
|
||||
$inspect('dbg noteTextHeights', noteTextHeights)
|
||||
</script>
|
||||
|
||||
{#if insertable}
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
import type { FlowNote } from '../../gen'
|
||||
import { deepEqual } from 'fast-equals'
|
||||
import { clone } from '../../utils'
|
||||
|
||||
export type NodePosition = {
|
||||
id: string
|
||||
position: { x: number; y: number }
|
||||
}
|
||||
|
||||
export type GroupNoteSpacingResult = {
|
||||
// Nodes need to be offset on the y axis to make space for group notes
|
||||
newNodePositions: Record<string, { x: number; y: number }>
|
||||
}
|
||||
|
||||
export type TextHeightCache = {
|
||||
content: string
|
||||
height: number
|
||||
}
|
||||
|
||||
// Cache for expensive group note spacing calculations
|
||||
let computeGroupNoteSpacingCache:
|
||||
| [NodePosition[], FlowNote[], Record<string, number>, GroupNoteSpacingResult]
|
||||
| undefined
|
||||
|
||||
// Text height cache to avoid remeasuring unchanged note text
|
||||
let textHeightCache: Record<string, TextHeightCache> = {}
|
||||
|
||||
const GROUP_NOTE_PADDING = 20
|
||||
|
||||
/**
|
||||
* Caches text height measurements based on content hash
|
||||
*/
|
||||
export function cacheTextHeight(noteId: string, content: string, height: number): void {
|
||||
textHeightCache[noteId] = { content, height }
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets cached text height if content hasn't changed, otherwise returns undefined
|
||||
*/
|
||||
export function getCachedTextHeight(noteId: string, content: string): number | undefined {
|
||||
const cached = textHeightCache[noteId]
|
||||
if (cached && cached.content === content) {
|
||||
return cached.height
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the text height for a note, using cache if available or fallback
|
||||
*/
|
||||
function getTextHeight(note: FlowNote, noteTextHeights: Record<string, number>): number {
|
||||
// Try cached height first
|
||||
const cachedHeight = getCachedTextHeight(note.id, note.text)
|
||||
if (cachedHeight !== undefined) {
|
||||
return cachedHeight
|
||||
}
|
||||
|
||||
// Fall back to runtime heights
|
||||
const runtimeHeight = noteTextHeights[note.id]
|
||||
if (runtimeHeight !== undefined) {
|
||||
// Update cache for future use
|
||||
cacheTextHeight(note.id, note.text, runtimeHeight)
|
||||
return runtimeHeight
|
||||
}
|
||||
|
||||
// Default fallback
|
||||
return 60
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the topmost node in a group by Y position
|
||||
*/
|
||||
function findTopmostNodeInGroup(
|
||||
groupNote: FlowNote,
|
||||
nodes: NodePosition[]
|
||||
): NodePosition | undefined {
|
||||
if (!groupNote.contained_node_ids?.length) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const containedNodes = nodes.filter((node) => groupNote.contained_node_ids?.includes(node.id))
|
||||
|
||||
if (containedNodes.length === 0) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return containedNodes.reduce((topMost, node) =>
|
||||
node.position.y < topMost.position.y ? node : topMost
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes vertical spacing adjustments for nodes to accommodate group notes.
|
||||
*/
|
||||
export function computeGroupNoteSpacing(
|
||||
nodes: NodePosition[],
|
||||
notes: FlowNote[],
|
||||
noteTextHeights: Record<string, number>
|
||||
): GroupNoteSpacingResult {
|
||||
// Check cache first
|
||||
if (
|
||||
computeGroupNoteSpacingCache &&
|
||||
deepEqual(nodes, computeGroupNoteSpacingCache[0]) &&
|
||||
deepEqual(notes, computeGroupNoteSpacingCache[1]) &&
|
||||
deepEqual(noteTextHeights, computeGroupNoteSpacingCache[2])
|
||||
) {
|
||||
return computeGroupNoteSpacingCache[3]
|
||||
}
|
||||
|
||||
// Filter to only group notes
|
||||
const groupNotes = notes.filter((note) => note.type === 'group')
|
||||
|
||||
if (groupNotes.length === 0) {
|
||||
const result: GroupNoteSpacingResult = {
|
||||
newNodePositions: Object.fromEntries(nodes.map((n) => [n.id, n.position]))
|
||||
}
|
||||
computeGroupNoteSpacingCache = [clone(nodes), clone(notes), clone(noteTextHeights), result]
|
||||
return result
|
||||
}
|
||||
|
||||
// Map Y positions to required spacing
|
||||
const ySpacingMap = new Map<number, number>()
|
||||
|
||||
// For each group note, determine the spacing needed at the topmost node's Y position
|
||||
for (const groupNote of groupNotes) {
|
||||
const topmostNode = findTopmostNodeInGroup(groupNote, nodes)
|
||||
|
||||
if (topmostNode) {
|
||||
const textHeight = getTextHeight(groupNote, noteTextHeights)
|
||||
const requiredSpacing = textHeight + GROUP_NOTE_PADDING
|
||||
|
||||
// If multiple group notes affect the same Y position, take the maximum spacing needed
|
||||
const currentSpacing = ySpacingMap.get(topmostNode.position.y) || 0
|
||||
ySpacingMap.set(topmostNode.position.y, Math.max(currentSpacing, requiredSpacing))
|
||||
}
|
||||
}
|
||||
|
||||
// Sort nodes by Y position to apply cumulative spacing
|
||||
const sortedNodes = [...nodes].sort((a, b) => a.position.y - b.position.y)
|
||||
|
||||
// Apply cumulative spacing
|
||||
let cumulativeOffset = 0
|
||||
let lastYPosition = -Infinity
|
||||
const adjustedNodes = sortedNodes.map((node) => {
|
||||
// When we encounter a new Y position, check if it needs additional spacing
|
||||
if (node.position.y > lastYPosition) {
|
||||
const spacingAtThisY = ySpacingMap.get(node.position.y)
|
||||
if (spacingAtThisY !== undefined) {
|
||||
cumulativeOffset += spacingAtThisY
|
||||
}
|
||||
lastYPosition = node.position.y
|
||||
}
|
||||
|
||||
return {
|
||||
...node,
|
||||
position: {
|
||||
...node.position,
|
||||
y: node.position.y + cumulativeOffset
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const result: GroupNoteSpacingResult = {
|
||||
newNodePositions: Object.fromEntries(adjustedNodes.map((n) => [n.id, n.position]))
|
||||
}
|
||||
|
||||
// Cache the result
|
||||
computeGroupNoteSpacingCache = [clone(nodes), clone(notes), clone(noteTextHeights), result]
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the text height cache (useful for testing or when needed)
|
||||
*/
|
||||
export function clearTextHeightCache(): void {
|
||||
textHeightCache = {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the group note spacing cache (useful when nodes structure changes dramatically)
|
||||
*/
|
||||
export function clearGroupNoteSpacingCache(): void {
|
||||
computeGroupNoteSpacingCache = undefined
|
||||
}
|
||||
@@ -2,9 +2,12 @@ import type { FlowNote } from '$lib/gen'
|
||||
import type { Node } from '@xyflow/svelte'
|
||||
import type { NoteColor } from './noteColors'
|
||||
import { calculateNodesBounds } from './util'
|
||||
import { cacheTextHeight } from './groupNoteSpacing'
|
||||
|
||||
type NodeDep = { id: string; parentIds?: string[]; offset?: number }
|
||||
type NodePos = { position: { x: number; y: number } }
|
||||
export type NodePosition = {
|
||||
id: string
|
||||
position: { x: number; y: number }
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility class for managing flow notes including regular and group notes
|
||||
@@ -136,42 +139,6 @@ export class NoteManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to determine if a node needs additional spacing above it for group notes.
|
||||
* Returns the height needed above the node (text height + padding).
|
||||
*/
|
||||
getGroupNoteHeightForNode(
|
||||
notes: FlowNote[],
|
||||
nodeId: string,
|
||||
layoutedNodes: (NodeDep & NodePos)[],
|
||||
noteTextHeights: Record<string, number>
|
||||
): number {
|
||||
const PADDING = 20 // Fixed padding above and below the note text
|
||||
|
||||
for (const note of notes) {
|
||||
if (note.type === 'group' && note.contained_node_ids?.includes(nodeId)) {
|
||||
// Find the topmost node in this group by Y position
|
||||
const containedNodes = layoutedNodes.filter((node) =>
|
||||
note.contained_node_ids?.includes(node.id)
|
||||
)
|
||||
|
||||
if (containedNodes.length > 0) {
|
||||
const topmostNode = containedNodes.reduce((topMost, node) =>
|
||||
node.position.y < topMost.position.y ? node : topMost
|
||||
)
|
||||
|
||||
// If this is the topmost node in the group, return the needed height
|
||||
if (topmostNode.id === nodeId) {
|
||||
// Use actual text height if available, otherwise default to 60
|
||||
const textHeight = noteTextHeights[note.id] || 60
|
||||
return textHeight + PADDING
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Create common data object for note nodes
|
||||
*/
|
||||
@@ -210,6 +177,8 @@ export class NoteManager {
|
||||
},
|
||||
onTextHeightChange: (textHeight: number) => {
|
||||
onTextHeightChange(note.id, textHeight)
|
||||
// Cache the text height for improved performance
|
||||
cacheTextHeight(note.id, note.text, textHeight)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user