diff --git a/frontend/src/lib/components/graph/DragGhost.svelte b/frontend/src/lib/components/graph/DragGhost.svelte index 6d2e26e10e..0aefc5c9e8 100644 --- a/frontend/src/lib/components/graph/DragGhost.svelte +++ b/frontend/src/lib/components/graph/DragGhost.svelte @@ -18,10 +18,6 @@ /** Offset so the cursor indicator icon doesn't overlap the cursor tip */ const CURSOR_INDICATOR_OFFSET = 8 - function nodeOffset(n: Node): number { - return ((n.data as Record)?.offset as number) ?? 0 - } - function getSubflowNodesAndEdges( moduleId: string, allNodes: Node[], @@ -68,7 +64,7 @@ maxY = -Infinity for (const n of sfNodes) { const abs = absolutePosition(n, allNodes) - const x = abs.x + nodeOffset(n) + const x = abs.x const y = abs.y const w = n.measured?.width ?? NODE.width const h = n.measured?.height ?? NODE.height @@ -90,7 +86,7 @@ let offsetY = containerHeight / 2 if (mainNode) { const mainAbs = absolutePosition(mainNode, allNodes) - const mx = mainAbs.x + nodeOffset(mainNode) - minX + PADDING + const mx = mainAbs.x - minX + PADDING const my = mainAbs.y - minY + PADDING const mw = mainNode.measured?.width ?? NODE.width const mh = mainNode.measured?.height ?? NODE.height diff --git a/frontend/src/lib/components/graph/FlowGraphV2.svelte b/frontend/src/lib/components/graph/FlowGraphV2.svelte index 96b50204ba..853dbf5c54 100644 --- a/frontend/src/lib/components/graph/FlowGraphV2.svelte +++ b/frontend/src/lib/components/graph/FlowGraphV2.svelte @@ -20,7 +20,6 @@ import { graphBuilder, isTriggerStep, - topologicalSort, type InlineScript, type InsertKind, type NodeLayout, @@ -36,7 +35,6 @@ import ResultNode from './renderers/nodes/ResultNode.svelte' import BaseEdge from './renderers/edges/BaseEdge.svelte' import EmptyEdge from './renderers/edges/EmptyEdge.svelte' - import { sugiyama, dagStratify, coordCenter, decrossTwoLayer, decrossOpt } from 'd3-dag' import { Expand, MousePointer, Hand } from 'lucide-svelte' import Toggle from '../Toggle.svelte' import DataflowEdge from './renderers/edges/DataflowEdge.svelte' @@ -71,6 +69,7 @@ import type { MoveManager } from './moveManager.svelte' import DragCoordinator from './DragCoordinator.svelte' import type { ModulesTestStates } from '../modulesTest.svelte' + import { compoundLayout } from './compoundLayout' import { deepEqual } from 'fast-equals' import type { AssetWithAltAccessType } from '../assets/lib' import type { ModuleActionInfo } from '$lib/components/flows/flowDiff' @@ -333,7 +332,6 @@ type NodeDep = { id: string parentIds?: string[] - offset?: number data?: { assets?: AssetWithAltAccessType[] } } type NodePos = { position: { x: number; y: number } } @@ -354,59 +352,21 @@ seenId.push(n.id) } - let nodeWidths: Record = {} - const nodes2: (NodeDep & NodePos)[] = nodes.map((n) => { - return { ...n, position: { x: 0, y: 0 } } + // Run recursive compound layout + const { positions, bbox } = compoundLayout(nodes, { + nodeWidth: NODE.width, + nodeHeight: NODE.height, + gapH: NODE.gap.horizontal, + gapV: NODE.gap.vertical }) - for (const n of topologicalSort(nodes)) { - const endId = n.id + '-end' - if (nodeWidths[endId] != undefined) { - nodeWidths[n.id] = Math.max(nodeWidths[n.id] ?? 0, nodeWidths[endId]) - } - if (n.parentIds && n.parentIds?.length == 1) { - const parent = n.parentIds[0] - const nodeWidth = nodeWidths[n.id] ?? 1 - nodeWidths[parent] = (nodeWidths[parent] ?? 0) + nodeWidth - } - } - - const dag = dagStratify().id(({ id }: NodeDep & NodePos) => id)(nodes2) - - let boxSize: any - try { - const layout = sugiyama() - .decross(nodes.length > 20 ? decrossTwoLayer() : decrossOpt()) - .coord(coordCenter()) - .nodeSize((d) => { - return [ - (nodeWidths[d?.data?.['id'] ?? ''] ?? 1) * (NODE.width + NODE.gap.horizontal * 1), - NODE.height + NODE.gap.vertical - ] as readonly [number, number] - }) - boxSize = layout(dag as any) - } catch { - const layout = sugiyama() - .decross(decrossTwoLayer()) - .coord(coordCenter()) - .nodeSize(() => [NODE.width + NODE.gap.horizontal, NODE.height + NODE.gap.vertical]) - boxSize = layout(dag as any) - } - - const newNodes = dag.descendants().map((des) => ({ - id: des.data.id, + // Center horizontally + const xCenter = (fullSize ? fullWidth : width) / 2 - bbox.width / 2 - (width - fullWidth) / 2 + const newNodes = nodes.map((n) => ({ + id: n.id, position: { - x: des.x - ? // @ts-ignore - (des.data.offset ?? 0) + - // @ts-ignore - des.x + - (fullSize ? fullWidth : width) / 2 - - boxSize.width / 2 - - NODE.width / 2 - - (width - fullWidth) / 2 - : 0, - y: des.y || 0 + x: (positions.get(n.id)?.x ?? 0) + xCenter - NODE.width / 2, + y: positions.get(n.id)?.y ?? 0 } })) @@ -631,7 +591,6 @@ Object.values(graph.nodes).map((n) => ({ id: n.id, parentIds: n.parentIds, - offset: n.data.offset ?? 0, data: { assets: (n.data as any).assets } })) ) @@ -640,10 +599,7 @@ let assetNodesResult = $showAssets ? computeAssetNodes( newNodes.map((n) => ({ - data: { - assets: n.data?.assets as AssetWithAltAccessType[], - offset: n.data?.offset as number - }, + data: { assets: n.data?.assets as AssetWithAltAccessType[] }, id: n.id, position: n.position })) @@ -674,7 +630,6 @@ id: n.id, position: n.position, parentIds: n.parentIds, - offset: n.data?.offset ?? 0, data: { assets: (n.data as any)?.assets }, type: n.type })), @@ -1039,8 +994,8 @@ {#if multiSelectEnabled} - nodesWithOffset.some(n => n.id === id) + selectedNodes={selectionManager.selectedIds.filter((id) => + nodesWithOffset.some((n) => n.id === id) )} allNodes={nodesWithOffset as (Node & { type: string })[]} onDeleteSelected={() => onDeleteMultiple?.(resolvedModuleIds)} @@ -1061,7 +1016,12 @@ {@render leftHeader()} {:else} - + n.type !== 'note') }} + > {#if multiSelectEnabled}
diff --git a/frontend/src/lib/components/graph/compoundLayout.ts b/frontend/src/lib/components/graph/compoundLayout.ts new file mode 100644 index 0000000000..4bf3ddebda --- /dev/null +++ b/frontend/src/lib/components/graph/compoundLayout.ts @@ -0,0 +1,555 @@ +import { sugiyama, dagStratify, coordCenter, decrossTwoLayer, decrossOpt } from 'd3-dag' +import { NODE } from './util' + +type LayoutNode = { + id: string + parentIds?: string[] +} + +type LayoutConstants = { + nodeWidth: number + nodeHeight: number + gapH: number + gapV: number +} + +type CompoundGroup = { + type: 'branch' | 'loop' + headId: string + endId: string + branches: { + labelId: string + innerIds: string[] + }[] +} + +type LayoutResult = { + positions: Map + bbox: { width: number; height: number } + contentMinX: number +} + +const LOOP_INDENT = 25 + +/** + * Detect compound groups from a flat list of node IDs. + * Uses ID naming conventions from graphBuilder: + * - BranchAll/BranchOne: node X has children X-branch-N and X-end + * - ForLoop/WhileLoop: node X has child X-start and X-end + */ +function detectGroups( + nodeIds: Set, + allNodes: Map, + childrenMap: Map +): CompoundGroup[] { + const groups: CompoundGroup[] = [] + + for (const id of nodeIds) { + if (!id.endsWith('-end')) continue + + // Extract base ID (everything before -end) + const baseId = id.slice(0, -4) + if (!nodeIds.has(baseId)) continue + + const baseNode = allNodes.get(baseId) + if (!baseNode) continue + + // Check for branch pattern: probe for baseId-branch-N nodes directly + const branchLabelIds: string[] = [] + if (nodeIds.has(`${baseId}-branch-default`)) { + branchLabelIds.push(`${baseId}-branch-default`) + } + for (let i = 0; nodeIds.has(`${baseId}-branch-${i}`); i++) { + branchLabelIds.push(`${baseId}-branch-${i}`) + } + + // Check for loop pattern: baseId-start node + const hasStart = nodeIds.has(`${baseId}-start`) + + if (branchLabelIds.length > 0) { + // Branches are already in correct order: default first, then 0, 1, 2... + const branches = branchLabelIds.map((labelId) => ({ + labelId, + innerIds: findInnerIds(labelId, id, nodeIds, childrenMap) + })) + + groups.push({ type: 'branch', headId: baseId, endId: id, branches }) + } else if (hasStart) { + const innerIds = findInnerIds(`${baseId}-start`, id, nodeIds, childrenMap) + + groups.push({ + type: 'loop', + headId: baseId, + endId: id, + branches: [{ labelId: `${baseId}-start`, innerIds }] + }) + } + } + + return groups +} + +/** + * Find inner node IDs between a label/start node and an end node. + * These are nodes that are reachable from the label node but not including + * the label or end node themselves. + */ +function findInnerIds( + labelId: string, + endId: string, + nodeIds: Set, + childrenMap: Map +): string[] { + const inner: string[] = [] + const visited = new Set() + + // BFS from label to find all reachable nodes before end + const queue = [labelId] + visited.add(labelId) + visited.add(endId) // Don't traverse past end + + let qi = 0 + while (qi < queue.length) { + const current = queue[qi++] + const kids = childrenMap.get(current) ?? [] + for (const kid of kids) { + if (visited.has(kid)) continue + if (!nodeIds.has(kid)) continue + visited.add(kid) + inner.push(kid) + queue.push(kid) + } + } + + return inner +} + +/** + * Run sugiyama layout on a set of nodes with parent relationships. + * Returns x,y positions for each node, centered at x=0. + */ +function runSugiyama( + nodes: { id: string; parentIds?: string[] }[], + constants: LayoutConstants, + nodeSizes?: Map +): { positions: Map; width: number; height: number } { + if (nodes.length === 0) { + return { positions: new Map(), width: 0, height: 0 } + } + + if (nodes.length === 1) { + const pos = new Map() + pos.set(nodes[0].id, { x: 0, y: 0 }) + const w = nodeSizes?.get(nodes[0].id)?.width ?? constants.nodeWidth + const h = nodeSizes?.get(nodes[0].id)?.height ?? constants.nodeHeight + return { positions: pos, width: w, height: h } + } + + const nodeIdSet = new Set(nodes.map((n) => n.id)) + const dagNodes = nodes.map((n) => ({ + id: n.id, + parentIds: (n.parentIds ?? []).filter((pid) => nodeIdSet.has(pid)) + })) + + const dag = dagStratify().id(({ id }: { id: string }) => id)(dagNodes) + + let boxSize: { width: number; height: number } + try { + const layout = sugiyama() + .decross(nodes.length > 20 ? decrossTwoLayer() : decrossOpt()) + .coord(coordCenter()) + .nodeSize((d: any) => { + const nodeId = d?.data?.id ?? '' + const size = nodeSizes?.get(nodeId) + const w = size?.width ?? constants.nodeWidth + const h = size?.height ?? constants.nodeHeight + return [w + constants.gapH, h + constants.gapV] as readonly [number, number] + }) + boxSize = layout(dag as any) as any + } catch (e) { + console.debug('[compoundLayout] decrossOpt failed, falling back to decrossTwoLayer:', e) + const layout = sugiyama() + .decross(decrossTwoLayer()) + .coord(coordCenter()) + .nodeSize((d: any) => { + const nodeId = d?.data?.id ?? '' + const size = nodeSizes?.get(nodeId) + const h = size?.height ?? constants.nodeHeight + return [constants.nodeWidth + constants.gapH, h + constants.gapV] + }) + boxSize = layout(dag as any) as any + } + + const positions = new Map() + for (const desc of dag.descendants()) { + const nodeId = desc.data.id + // sugiyama returns CENTER positions; convert to TOP by subtracting half the node's allocated height + const h = nodeSizes?.get(nodeId)?.height ?? constants.nodeHeight + const rawY = (desc as any).y ?? 0 + positions.set(nodeId, { + x: (desc as any).x ?? 0, + y: rawY - (h + constants.gapV) / 2 + }) + } + + // Normalize y so minimum = 0 + let minY = Infinity + for (const pos of positions.values()) { + minY = Math.min(minY, pos.y) + } + if (minY !== Infinity && minY !== 0) { + for (const pos of positions.values()) { + pos.y -= minY + } + } + + // Normalize x so center of bbox = 0 (important for nested branch placement) + let minX = Infinity + let maxX = -Infinity + for (const pos of positions.values()) { + minX = Math.min(minX, pos.x) + maxX = Math.max(maxX, pos.x) + } + if (minX !== Infinity) { + const centerX = (minX + maxX) / 2 + for (const pos of positions.values()) { + pos.x -= centerX + } + } + + return { positions, width: boxSize.width, height: boxSize.height } +} + +/** + * Recursive compound layout. + * + * 1. Detect compound groups at this level + * 2. For each group, recursively lay out each branch + * 3. Compute wrapper bbox for each group + * 4. Replace group nodes with a single wrapper pseudo-node + * 5. Run sugiyama on the simplified graph + * 6. Expand wrapper positions back to absolute positions + */ +const MAX_RECURSION_DEPTH = 50 + +function layoutLevel( + nodeIds: string[], + allNodes: Map, + constants: LayoutConstants, + childrenMap: Map, + depth: number = 0 +): LayoutResult { + const positions = new Map() + const nodeIdSet = new Set(nodeIds) + + if (nodeIds.length === 0) { + return { + positions, + bbox: { width: constants.nodeWidth, height: 0 }, + contentMinX: 0 + } + } + + if (depth >= MAX_RECURSION_DEPTH) { + console.warn('[compoundLayout] Max recursion depth reached, falling back to flat layout') + const flatNodes = nodeIds.map((id) => { + const n = allNodes.get(id)! + return { id, parentIds: (n.parentIds ?? []).filter((pid) => nodeIdSet.has(pid)) } + }) + const result = runSugiyama(flatNodes, constants) + for (const [id, pos] of result.positions) { + positions.set(id, pos) + } + return { positions, bbox: { width: result.width, height: result.height }, contentMinX: 0 } + } + + // Step 1: detect compound groups at this level + const groups = detectGroups(nodeIdSet, allNodes, childrenMap) + + // First pass: quick set of ALL group-owned inner IDs (just for filtering nested heads) + const allGroupOwnedIds = new Set() + for (const group of groups) { + if (!nodeIdSet.has(group.headId)) continue + for (const branch of group.branches) { + for (const innerId of branch.innerIds) { + allGroupOwnedIds.add(innerId) + } + } + } + + // Filter to top-level groups (head not owned by another group) + const topLevelGroups = groups.filter( + (g) => nodeIdSet.has(g.headId) && !allGroupOwnedIds.has(g.headId) + ) + + // Build final groupOwnedIds and groupByHeadId from top-level only + const groupOwnedIds = new Set() + const groupByHeadId = new Map() + for (const group of topLevelGroups) { + groupByHeadId.set(group.headId, group) + groupOwnedIds.add(group.endId) + for (const branch of group.branches) { + groupOwnedIds.add(branch.labelId) + for (const innerId of branch.innerIds) { + groupOwnedIds.add(innerId) + } + } + } + + // Step 2-3: Recursively lay out each group and compute wrapper sizes + type GroupLayout = { + group: CompoundGroup + branchLayouts: { + labelId: string + result: LayoutResult + bbox: { width: number; height: number } + }[] + branchWidths: number[] + totalWidth: number + wrapperWidth: number + wrapperHeight: number + maxBranchHeight: number + } + + const groupLayouts = new Map() + const wrapperSizes = new Map() + + for (const group of topLevelGroups) { + const branchLayouts: GroupLayout['branchLayouts'] = [] + const isBranch = group.type === 'branch' + + for (const branch of group.branches) { + const branchNodeIds = [branch.labelId, ...branch.innerIds] + + // Find sub-groups within this branch + const result = layoutLevel(branchNodeIds, allNodes, constants, childrenMap, depth + 1) + + branchLayouts.push({ + labelId: branch.labelId, + result, + bbox: result.bbox + }) + } + + // Compute wrapper dimensions + let wrapperWidth: number + let wrapperHeight: number + let branchWidths: number[] = [] + let totalWidth = 0 + let maxBranchHeight = 0 + const rowHeight = constants.nodeHeight + constants.gapV + + if (isBranch) { + // Place branches side by side horizontally + branchWidths = branchLayouts.map((bl) => Math.max(bl.bbox.width, constants.nodeWidth)) + const gaps = Math.max(0, branchWidths.length - 1) * constants.gapH + totalWidth = branchWidths.reduce((s, w) => s + w, 0) + gaps + wrapperWidth = Math.max(totalWidth, constants.nodeWidth) + + maxBranchHeight = Math.max(0, ...branchLayouts.map((bl) => bl.bbox.height)) + // head row + branch content + end row + wrapperHeight = rowHeight + maxBranchHeight + rowHeight + } else { + // Loop: body is indented + const bodyWidth = branchLayouts[0]?.bbox.width ?? constants.nodeWidth + const bodyHeight = branchLayouts[0]?.bbox.height ?? 0 + wrapperWidth = Math.max(bodyWidth + LOOP_INDENT * 2, constants.nodeWidth) + // head row + start row + body + end row + wrapperHeight = rowHeight + bodyHeight + rowHeight + } + groupLayouts.set(group.headId, { + group, + branchLayouts, + branchWidths, + totalWidth, + wrapperWidth, + wrapperHeight, + maxBranchHeight + }) + wrapperSizes.set(group.headId, { width: wrapperWidth, height: wrapperHeight }) + } + + // Step 4: Build flattened node list for sugiyama + // Merge into a single pass: create flatNode and compute final parentIds with end→head redirection + const endToHead = new Map() + for (const group of topLevelGroups) { + endToHead.set(group.endId, group.headId) + } + + const flatNodes: { id: string; parentIds?: string[] }[] = [] + for (const nid of nodeIds) { + if (groupOwnedIds.has(nid)) continue + + const originalNode = allNodes.get(nid)! + const seen = new Set() + const newParents: string[] = [] + for (const pid of originalNode.parentIds ?? []) { + if (!nodeIdSet.has(pid)) continue + const resolved = endToHead.get(pid) ?? (groupOwnedIds.has(pid) ? undefined : pid) + if (resolved && !seen.has(resolved)) { + seen.add(resolved) + newParents.push(resolved) + } + } + flatNodes.push({ id: nid, parentIds: newParents }) + } + + // Step 5: Run sugiyama on flattened nodes + const sugResult = runSugiyama(flatNodes, constants, wrapperSizes) + + // Step 6: Resolve absolute positions + // First, set positions for regular (non-group) nodes + for (const [nid, pos] of sugResult.positions) { + if (groupByHeadId.has(nid)) continue // Handle groups separately + positions.set(nid, { x: pos.x, y: pos.y }) + } + + // Now expand group wrappers into absolute positions + for (const [headId, gl] of groupLayouts) { + const wrapperPos = sugResult.positions.get(headId) + if (!wrapperPos) continue + + const rowHeight = constants.nodeHeight + constants.gapV + const isBranch = gl.group.type === 'branch' + + // Position the head node at the top-center of the wrapper + positions.set(headId, { x: wrapperPos.x, y: wrapperPos.y }) + + if (isBranch) { + // Reuse cached branchWidths and totalWidth + let currentX = wrapperPos.x - gl.totalWidth / 2 + + for (let bi = 0; bi < gl.branchLayouts.length; bi++) { + const bl = gl.branchLayouts[bi] + const bw = gl.branchWidths[bi] + const branchCenterX = currentX + bw / 2 + + // Offset all branch positions relative to the branch center + for (const [innerNodeId, innerPos] of bl.result.positions) { + positions.set(innerNodeId, { + x: branchCenterX + innerPos.x, + y: wrapperPos.y + rowHeight + innerPos.y + }) + } + + currentX += bw + constants.gapH + } + + // Position end node below all branches + const maxBranchHeight = gl.maxBranchHeight + positions.set(gl.group.endId, { + x: wrapperPos.x, + y: wrapperPos.y + rowHeight + maxBranchHeight + constants.gapV + }) + } else { + // Loop: position start, body, and end + const bl = gl.branchLayouts[0] + if (bl) { + // Position body nodes with indent + for (const [innerNodeId, innerPos] of bl.result.positions) { + positions.set(innerNodeId, { + x: wrapperPos.x + LOOP_INDENT + innerPos.x, + y: wrapperPos.y + rowHeight + innerPos.y + }) + } + } + + // Position end node below body + const bodyHeight = bl?.bbox.height ?? 0 + positions.set(gl.group.endId, { + x: wrapperPos.x, + y: wrapperPos.y + rowHeight + bodyHeight + constants.gapV + }) + } + } + + // Compute overall bbox (nodes + group wrapper extents) + let minX = Infinity + let maxX = -Infinity + let minY = Infinity + let maxY = -Infinity + for (const pos of positions.values()) { + minX = Math.min(minX, pos.x - constants.nodeWidth / 2) + maxX = Math.max(maxX, pos.x + constants.nodeWidth / 2) + minY = Math.min(minY, pos.y) + maxY = Math.max(maxY, pos.y + constants.nodeHeight) + } + // Account for group wrapper extents in bbox (e.g. LOOP_INDENT makes wrappers wider than nodes) + for (const [headId, gl] of groupLayouts) { + const pos = sugResult.positions.get(headId) + if (!pos) continue + minX = Math.min(minX, pos.x - gl.wrapperWidth / 2) + maxX = Math.max(maxX, pos.x + gl.wrapperWidth / 2) + maxY = Math.max(maxY, pos.y + gl.wrapperHeight) + } + + const contentMinX = minX === Infinity ? 0 : minX + + const bboxWidth = maxX - minX + const bboxHeight = maxY - (positions.size > 0 ? minY : 0) + + const finalBbox = { + width: Math.max(bboxWidth, constants.nodeWidth), + height: Math.max(bboxHeight, 0) + } + return { positions, bbox: finalBbox, contentMinX } +} + +/** + * Main entry point for compound layout. + * + * Takes the flat list of nodes and edges from graphBuilder and produces + * absolute positions that account for compound structure (branches, loops). + */ +export function compoundLayout( + nodes: { id: string; parentIds?: string[] }[], + constants?: Partial +): LayoutResult { + const c: LayoutConstants = { + nodeWidth: constants?.nodeWidth ?? NODE.width, + nodeHeight: constants?.nodeHeight ?? NODE.height, + gapH: constants?.gapH ?? NODE.gap.horizontal, + gapV: constants?.gapV ?? NODE.gap.vertical + } + + // Build node map + const allNodes = new Map() + for (const n of nodes) { + allNodes.set(n.id, n) + } + + // Build children map once (reverse of parentIds), shared across all recursion levels + const childrenMap = new Map() + for (const [nid, node] of allNodes) { + for (const pid of node.parentIds ?? []) { + if (!childrenMap.has(pid)) childrenMap.set(pid, []) + childrenMap.get(pid)!.push(nid) + } + } + + const nodeIds = nodes.map((n) => n.id) + const result = layoutLevel(nodeIds, allNodes, c, childrenMap) + + // Shift positions so minX=0 (left-aligned). + // FlowGraphV2 centers with: xCenter = viewport/2 - bbox.width/2 + // which assumes positions start at x=0. + if (result.positions.size > 0) { + const minX = result.contentMinX + if (minX !== 0 && minX !== Infinity) { + for (const pos of result.positions.values()) { + pos.x -= minX + } + } + } + + // Check for missing nodes + const missing = nodes.filter((n) => !result.positions.has(n.id)) + if (missing.length > 0) { + console.warn( + '[compoundLayout] MISSING positions for:', + missing.map((n) => n.id) + ) + } + + return result +} diff --git a/frontend/src/lib/components/graph/graphBuilder.svelte.ts b/frontend/src/lib/components/graph/graphBuilder.svelte.ts index 37b3103f5f..1d01b2e252 100644 --- a/frontend/src/lib/components/graph/graphBuilder.svelte.ts +++ b/frontend/src/lib/components/graph/graphBuilder.svelte.ts @@ -88,9 +88,7 @@ export function buildPrefix(prefix: string | undefined, id: string): string { export type NodeLayout = { id: string parentIds?: string[] - data: { - offset?: number - } + data: {} selectable?: boolean } & FlowNode @@ -138,7 +136,6 @@ export type InputN = { export type ModuleN = { type: 'module' data: { - offset: number module: FlowModule id: string parentIds: string[] @@ -157,7 +154,6 @@ export type ModuleN = { export type BranchAllStartN = { type: 'branchAllStart' data: { - offset: number label: string id: string branchIndex: number @@ -171,7 +167,6 @@ export type BranchAllStartN = { export type BranchAllEndN = { type: 'branchAllEnd' data: { - offset: number id: string eventHandlers: GraphEventHandlers flowModuleState: GraphModuleState | undefined @@ -181,7 +176,6 @@ export type BranchAllEndN = { export type ForLoopEndN = { type: 'forLoopEnd' data: { - offset: number id: string eventHandlers: GraphEventHandlers simplifiedTriggerView: boolean @@ -192,7 +186,6 @@ export type ForLoopEndN = { export type ForLoopStartN = { type: 'forLoopStart' data: { - offset: number id: string eventHandlers: GraphEventHandlers flowModuleState: GraphModuleState | undefined @@ -217,22 +210,18 @@ export type ResultN = { export type WhileLoopStartN = { type: 'whileLoopStart' data: { - offset: number eventHandlers: GraphEventHandlers } } export type WhileLoopEndN = { type: 'whileLoopEnd' - data: { - offset: number - } + data: {} } export type BranchOneStartN = { type: 'branchOneStart' data: { - offset: number id: string eventHandlers: GraphEventHandlers flowModuleState: GraphModuleState | undefined @@ -248,7 +237,6 @@ export type BranchOneStartN = { export type BranchOneEndN = { type: 'branchOneEnd' data: { - offset: number id: string eventHandlers: GraphEventHandlers flowModuleState: GraphModuleState | undefined @@ -258,7 +246,6 @@ export type BranchOneEndN = { export type SubflowBoundN = { type: 'subflowBound' data: { - offset: number id: string eventHandlers: GraphEventHandlers label: string @@ -271,7 +258,6 @@ export type SubflowBoundN = { export type NoBranchN = { type: 'noBranch' data: { - offset: number id: string eventHandlers: GraphEventHandlers flowModuleState: GraphModuleState | undefined @@ -416,7 +402,7 @@ export function graphBuilder( const nodes: NodeLayout[] = [] const edges: Edge[] = [] - function addNode(module: FlowModule, offset: number) { + function addNode(module: FlowModule) { const duplicated = nodes.find((n) => n.id === module.id) if (duplicated) { console.log('Duplicated node detected: ', module, duplicated) @@ -426,7 +412,6 @@ export function graphBuilder( nodes.push({ id: module.id, data: { - offset: offset, module: module, id: module.id, parentIds: [], @@ -611,7 +596,6 @@ export function graphBuilder( nextNode: NodeLayout | undefined, simplifiedTriggerView: boolean, prefix: string | undefined, - currentOffset = 0, disableMoveIds: string[] = [], parentIndex?: string ) { @@ -646,13 +630,12 @@ export function graphBuilder( if (module.value.type === 'branchall') { // Start - addNode(module, currentOffset) + addNode(module) // "Collect result of each branch" node const endNode: NodeLayout = { id: `${module.id}-end`, data: { - offset: currentOffset, id: module.id, eventHandlers: eventHandlers, flowModuleState: extra.flowModuleStates?.[module.id] @@ -667,7 +650,6 @@ export function graphBuilder( const startNode: NodeLayout = { id: `${module.id}-branch-0`, data: { - offset: currentOffset, id: module.id, branchIndex: -1, eventHandlers: eventHandlers, @@ -693,7 +675,6 @@ export function graphBuilder( const startNode: NodeLayout = { id: `${module.id}-branch-${branchIndex}`, data: { - offset: currentOffset, label: defaultIfEmptyString(branch.summary, `Branch ${branchIndex + 1}`), id: module.id, branchIndex: branchIndex, @@ -724,7 +705,6 @@ export function graphBuilder( endNode, false, prefix, - currentOffset, localDisableMoveIds, parentIndex ? `${parentIndex}-${index}-${branchIndex}` : `${index}-${branchIndex}` ) @@ -734,13 +714,12 @@ export function graphBuilder( previousId = endNode.id } else if (module.value.type === 'forloopflow') { if (!simplifiedTriggerView) { - addNode(module, currentOffset) + addNode(module) } const startNode: NodeLayout = { id: `${module.id}-start`, data: { - offset: currentOffset + 25, id: module.id, module: module, simplifiedTriggerView, @@ -765,7 +744,6 @@ export function graphBuilder( const endNode: NodeLayout = { id: `${module.id}-end`, data: { - offset: currentOffset, id: module.id, eventHandlers: eventHandlers, simplifiedTriggerView, @@ -786,7 +764,6 @@ export function graphBuilder( endNode, false, prefix, - currentOffset + 25, localDisableMoveIds, parentIndex ? `${parentIndex}-${index}-${selectedIterIndex ?? '?'}` @@ -795,12 +772,11 @@ export function graphBuilder( previousId = endNode.id } else if (module.value.type === 'whileloopflow') { - addNode(module, currentOffset) + addNode(module) const startNode: NodeLayout = { id: `${module.id}-start`, data: { - offset: currentOffset + 25, eventHandlers: eventHandlers }, type: 'whileLoopStart' @@ -811,7 +787,7 @@ export function graphBuilder( const endNode: NodeLayout = { id: `${module.id}-end`, - data: { offset: currentOffset, ...extra }, + data: { ...extra }, type: 'whileLoopEnd' } @@ -827,7 +803,6 @@ export function graphBuilder( endNode, false, prefix, - currentOffset + 25, localDisableMoveIds, parentIndex ? `${parentIndex}-${index}-${selectedIterIndex ?? '?'}` @@ -836,12 +811,11 @@ export function graphBuilder( previousId = endNode.id } else if (module.value.type === 'branchone') { - addNode(module, currentOffset) + addNode(module) const endNode: NodeLayout = { id: `${module.id}-end`, data: { - offset: currentOffset, eventHandlers: eventHandlers, flowModuleState: extra.flowModuleStates?.[module.id], id: module.id @@ -854,7 +828,7 @@ export function graphBuilder( // const defaultBranch: NodeLayout = { // id: `${module.id}-default`, // data: { - // offset: currentOffset, + // offset: 0, // label: 'Default', // id: module.id, // branchIndex: -1, @@ -868,7 +842,6 @@ export function graphBuilder( const defaultBranch: NodeLayout = { id: `${module.id}-branch-default`, data: { - offset: currentOffset, label: 'Default', id: module.id, branchIndex: -1, @@ -895,7 +868,6 @@ export function graphBuilder( endNode, false, prefix, - currentOffset, localDisableMoveIds, parentIndex ? `${parentIndex}-${index}` : index.toString() ) @@ -906,7 +878,6 @@ export function graphBuilder( const startNode: NodeLayout = { id: `${module.id}-branch-${branchIndex}`, data: { - offset: currentOffset, label: defaultIfEmptyString(branch.summary, 'Branch ' + (branchIndex + 1)), preLabel: branch.summary ? '' : branch.expr, id: module.id, @@ -933,7 +904,6 @@ export function graphBuilder( endNode, false, prefix, - currentOffset, localDisableMoveIds, parentIndex ? `${parentIndex}-${index}` : index.toString() ) @@ -951,7 +921,6 @@ export function graphBuilder( const startNode: NodeLayout = { id: startId, data: { - offset: currentOffset, label: `Start of subflow ${idWithoutPrefix}`, id: startId, subflowId: module.id, @@ -980,7 +949,6 @@ export function graphBuilder( const endNode: NodeLayout = { id: endId, data: { - offset: currentOffset, label: `End of subflow ${idWithoutPrefix}`, id: endId, subflowId: module.id, @@ -1000,13 +968,12 @@ export function graphBuilder( endNode, false, buildPrefix(prefix, module['oid'] ?? module.id), - currentOffset, localDisableMoveIds ) previousId = endNode.id } else { - addNode(module, currentOffset) + addNode(module) previousId = module.id } } @@ -1047,19 +1014,19 @@ export function graphBuilder( }) Object.entries(toAdd).forEach((x) => { - addNode({ ...failureModule, id: x[1] }, 0) + addNode({ ...failureModule, id: x[1] }) addEdge(x[0], x[1], undefined, undefined, { type: 'empty' }) }) } if (preprocessorModule) { - addNode(preprocessorModule, 0) + addNode(preprocessorModule) const id = JSON.parse(JSON.stringify(preprocessorModule.id)) addEdge(id, 'Input', undefined, undefined, { type: 'empty' }) } if (failureModule && !extra.flowModuleStates) { - addNode(failureModule, 0) + addNode(failureModule) } Object.keys(parents).forEach((key) => { diff --git a/frontend/src/lib/components/graph/noteEditor.svelte.ts b/frontend/src/lib/components/graph/noteEditor.svelte.ts index 4d59cfa480..e59e3be4f5 100644 --- a/frontend/src/lib/components/graph/noteEditor.svelte.ts +++ b/frontend/src/lib/components/graph/noteEditor.svelte.ts @@ -219,7 +219,7 @@ export class NoteEditor { /** * Clean up group notes using DAG path completion */ - cleanupGroupNotes(flowNodes: { id: string; parentIds?: string[]; offset?: number }[]): void { + cleanupGroupNotes(flowNodes: { id: string; parentIds?: string[] }[]): void { if (!this.isAvailable()) { return } diff --git a/frontend/src/lib/components/graph/noteUtils.svelte.ts b/frontend/src/lib/components/graph/noteUtils.svelte.ts index d84817a693..3350e07ce8 100644 --- a/frontend/src/lib/components/graph/noteUtils.svelte.ts +++ b/frontend/src/lib/components/graph/noteUtils.svelte.ts @@ -14,7 +14,6 @@ export type NodeDep = { position: { x: number; y: number } data?: { assets?: AssetWithAltAccessType[] } parentIds?: string[] - offset?: number type?: string } @@ -207,7 +206,6 @@ function calculateGroupNoteLayout( nodes.map((n) => ({ id: n.id, position: n.position, - data: { offset: n.offset ?? 0 }, type: n.type ?? '' })) ) @@ -337,7 +335,6 @@ export function computeNoteNodes( return { ...n, data: origNode?.data, - offset: origNode?.offset, type: origNode?.type } }) diff --git a/frontend/src/lib/components/graph/renderers/nodes/AIToolNode.svelte b/frontend/src/lib/components/graph/renderers/nodes/AIToolNode.svelte index 242f9fac61..28e9d58fa4 100644 --- a/frontend/src/lib/components/graph/renderers/nodes/AIToolNode.svelte +++ b/frontend/src/lib/components/graph/renderers/nodes/AIToolNode.svelte @@ -175,7 +175,7 @@ ? inputToolWidth + inputToolXGap : isLastRow && tools.length % 2 === 1 ? (ROW_WIDTH - inputToolWidth) / 2 - : 0) + node.data.offset, + : 0), y: baseOffset + rowOffset * @@ -207,7 +207,7 @@ parentId: node.id, width: NEW_TOOL_NODE_WIDTH, position: { - x: (ROW_WIDTH - NEW_TOOL_NODE_WIDTH) / 2 + node.data.offset, + x: (ROW_WIDTH - NEW_TOOL_NODE_WIDTH) / 2, y: baseOffset + rowOffset }, selectable: false diff --git a/frontend/src/lib/components/graph/renderers/nodes/AssetNode.svelte b/frontend/src/lib/components/graph/renderers/nodes/AssetNode.svelte index 0db4e92f3a..aec1fb03f8 100644 --- a/frontend/src/lib/components/graph/renderers/nodes/AssetNode.svelte +++ b/frontend/src/lib/components/graph/renderers/nodes/AssetNode.svelte @@ -11,7 +11,7 @@ let computeAssetNodesCache: [NodeDep[], ReturnType] | undefined type NodeDep = { - data: object & { assets?: AssetWithAltAccessType[] | undefined; offset?: number } + data: object & { assets?: AssetWithAltAccessType[] | undefined } id: string position: { x: number; y: number } } @@ -78,14 +78,13 @@ width: inputAssetWidth, position: { x: - (node.data.offset ?? 0) + - (displayedInputAssets.length === 1 + displayedInputAssets.length === 1 ? (NODE.width - inputAssetWidth) / 2 - 10 // Ensure we see the edge : (inputAssetWidth + inputAssetXGap) * (i - displayedInputAssets.length / 2) + (NODE.width + inputAssetXGap) / 2 + (overflowedInputAssets.length ? (-ASSETS_OVERFLOWED_NODE_WIDTH - inputAssetXGap) / 2 - : 0)), + : 0), y: READ_ASSET_Y_OFFSET }, selectable: false @@ -116,14 +115,13 @@ width: outputAssetWidth, position: { x: - (node.data.offset ?? 0) + - (displayedOutputAssets.length === 1 + displayedOutputAssets.length === 1 ? (NODE.width - outputAssetWidth) / 2 - 10 // Ensure we see the edge : (outputAssetWidth + outputAssetXGap) * (i - displayedOutputAssets.length / 2) + (NODE.width + outputAssetXGap) / 2 + (overflowedOutputAssets.length ? (-ASSETS_OVERFLOWED_NODE_WIDTH - outputAssetXGap) / 2 - : 0)), + : 0), y: WRITE_ASSET_Y_OFFSET }, selectable: false @@ -157,7 +155,7 @@ parentId: node.id, width: ASSETS_OVERFLOWED_NODE_WIDTH, position: { - x: (node.data.offset ?? 0) + MAX_ASSET_ROW_WIDTH - ASSETS_OVERFLOWED_NODE_WIDTH - 14, + x: MAX_ASSET_ROW_WIDTH - ASSETS_OVERFLOWED_NODE_WIDTH - 14, y: READ_ASSET_Y_OFFSET } } satisfies Node & AssetsOverflowedN) @@ -176,7 +174,7 @@ parentId: node.id, width: ASSETS_OVERFLOWED_NODE_WIDTH, position: { - x: (node.data.offset ?? 0) + MAX_ASSET_ROW_WIDTH - ASSETS_OVERFLOWED_NODE_WIDTH - 14, + x: MAX_ASSET_ROW_WIDTH - ASSETS_OVERFLOWED_NODE_WIDTH - 14, y: WRITE_ASSET_Y_OFFSET } } satisfies Node & AssetsOverflowedN) diff --git a/frontend/src/lib/components/graph/renderers/nodes/BranchAllEndNode.svelte b/frontend/src/lib/components/graph/renderers/nodes/BranchAllEndNode.svelte index 27058af860..3b7dc461b8 100644 --- a/frontend/src/lib/components/graph/renderers/nodes/BranchAllEndNode.svelte +++ b/frontend/src/lib/components/graph/renderers/nodes/BranchAllEndNode.svelte @@ -13,7 +13,7 @@ const { selectionManager } = getGraphContext() - + {#snippet children({ darkMode })} - + {#snippet children({ darkMode })} - + {#snippet children({ darkMode })} - + {#snippet children({ darkMode })} {#if data.simplifiedTriggerView} - + {#snippet children({ darkMode })} - + {#snippet children({ darkMode })} = 0 ? (state?.selectedForloopIndex ?? 0) + 1 @@ -136,15 +136,18 @@ onEditInput={data.eventHandlers.editInput} flowJob={data.flowJob} isOwner={data.isOwner} - maximizeSubflow={data.module.value.type == 'flow' && 'path' in data.module.value + maximizeSubflow={data.module?.value?.type == 'flow' && 'path' in data.module.value ? () => { - data.eventHandlers.expandSubflow(data.id, data.module.value['path']) + const path = data.module?.value && 'path' in data.module.value ? data.module.value['path'] as string : undefined + if (path) { + data.eventHandlers.expandSubflow(data.id, path) + } } : undefined} />
- {#if (data.module.value.type === 'branchall' || data.module.value.type === 'branchone') && data.insertable} + {#if (data.module?.value?.type === 'branchall' || data.module?.value?.type === 'branchone') && data.insertable}