improve adaptive layout to group note

This commit is contained in:
Guilhem
2025-11-10 15:43:57 +01:00
parent 1ae0f4822d
commit 5083ba61bd
3 changed files with 205 additions and 80 deletions
@@ -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)
}
}
}