diff --git a/src/app/directed_acyclic_graph.spec.ts b/src/app/directed_acyclic_graph.spec.ts index 620384e..e7f2ad5 100644 --- a/src/app/directed_acyclic_graph.spec.ts +++ b/src/app/directed_acyclic_graph.spec.ts @@ -26,11 +26,11 @@ import {ScreenshotTest} from '../screenshot_test'; import {ColorThemeLoader} from './color_theme_loader'; import {DagStateService} from './dag-state.service'; import {STATE_SERVICE_PROVIDER} from './dag-state.service.provider'; -import {DirectedAcyclicGraph, DirectedAcyclicGraphModule} from './directed_acyclic_graph'; +import {DirectedAcyclicGraph, DirectedAcyclicGraphModule, generateTheme} from './directed_acyclic_graph'; import {DagNode as Node, type GraphSpec, type NodeRef} from './node_spec'; import {TEST_IMPORTS, TEST_PROVIDERS} from './test_providers'; import {DirectedAcyclicGraphHarness} from './test_resources/directed_acyclic_graph_harness'; -import {createDagSkeletonWithCustomGroups, createDagSkeletonWithGroups, fakeGraph} from './test_resources/fake_data'; +import {createDagSkeletonWithCustomGroups, createDagSkeletonWithGroups, fakeGraph, fakeGraphWithEdgeOffsets} from './test_resources/fake_data'; import {initTestBed} from './test_resources/test_utils'; const FAKE_DATA: GraphSpec = @@ -212,6 +212,24 @@ describe('Directed Acyclic Graph Renderer', () => { await screenShot.expectMatch(`graph_loading`); }); }); + + describe('with edge offsets', () => { + let fixture: ComponentFixture; + + beforeEach(() => { + fixture = TestBed.createComponent(TestComponent); + const graphSpec = Node.createFromSkeleton( + fakeGraphWithEdgeOffsets.skeleton, fakeGraphWithEdgeOffsets.state); + fixture.componentRef.setInput('graph', graphSpec); + fixture.componentRef.setInput( + 'theme', generateTheme({edgeStyle: 'snapped'})); + fixture.detectChanges(); + }); + + it('renders correctly', async () => { + await screenShot.expectMatch(`graph_with_edge_offsets`); + }); + }); }); }); @@ -226,6 +244,7 @@ describe('Directed Acyclic Graph Renderer', () => { [followNode]="followNode" [loading]="loading" [customNodeTemplates]="{'outlineBasic': outlineBasic}" + [theme]="theme" > @@ -267,4 +286,5 @@ class TestComponent { @Input() graph: GraphSpec = FAKE_DATA; @Input() followNode: NodeRef|null = null; @Input() loading = false; + @Input() theme = generateTheme({}); } diff --git a/src/app/directed_acyclic_graph_raw.ts b/src/app/directed_acyclic_graph_raw.ts index cf9bcf2..3ed2266 100644 --- a/src/app/directed_acyclic_graph_raw.ts +++ b/src/app/directed_acyclic_graph_raw.ts @@ -29,7 +29,7 @@ import {fetchIcon, generateFullIconFor} from './icon_util'; import {WorkflowGraphIconModule} from './icon_wrapper'; import {DagNodeModule} from './node'; import {NodeRefBadgeModule} from './node_ref_badge'; -import {CustomNode, type DagEdge, DagGroup, DagNode, GroupIterationRecord, GroupToggleEvent, isDagreInit, isSamePath, NodeMap, NodeRef, NodeType, Point, type SelectedNode} from './node_spec'; +import {CustomNode, type DagEdge, DagGroup, DagNode, GroupIterationRecord, GroupToggleEvent, isDagreInit, isSamePath, NodeMap, NodeRef, NodeType, Point, type SelectedNode, SnapPoint} from './node_spec'; import {UserConfigService} from './user_config.service'; import {debounce, isPinch} from './util_functions'; @@ -735,17 +735,8 @@ export class DagRaw implements DoCheck, OnInit, OnDestroy { this.cdr.detectChanges(); } - getTopCenterPoint(node: DagNode|DagGroup): Point { - return {x: node.x, y: node.y - node.height / 2}; - } - getBottomCenterPoint(node: DagNode|DagGroup): Point { - return {x: node.x, y: node.y + node.height / 2}; - } - getLeftCenterPoint(node: DagNode|DagGroup): Point { - return {x: node.x - node.width / 2, y: node.y}; - } - getRightCenterPoint(node: DagNode|DagGroup): Point { - return {x: node.x + node.width / 2, y: node.y}; + anchorPoint(length: number, percent: number, offset: number) { + return length * (percent - 0.5) + offset; } /** @@ -768,27 +759,64 @@ export class DagRaw implements DoCheck, OnInit, OnDestroy { const toTarget = this.ensureTargetEntity(edge.to); const layoutDirection = this.layout.rankDirection; + + let startHorizontalAnchor = edge.startSnapPoint?.horizontalPercent; + let startVerticalAnchor = edge.startSnapPoint?.verticalPercent; + let endHorizontalAnchor = edge.endSnapPoint?.horizontalPercent; + let endVerticalAnchor = edge.endSnapPoint?.verticalPercent; + if (layoutDirection === 'LR') { - edge.points = [ - this.getRightCenterPoint(fromTarget), - this.getLeftCenterPoint(toTarget), - ]; + startHorizontalAnchor ??= 1; + startVerticalAnchor ??= 0.5; + endHorizontalAnchor ??= 0; + endVerticalAnchor ??= 0.5; } else if (layoutDirection === 'RL') { - edge.points = [ - this.getLeftCenterPoint(fromTarget), - this.getRightCenterPoint(toTarget), - ]; + startHorizontalAnchor ??= 0; + startVerticalAnchor ??= 0.5; + endHorizontalAnchor ??= 1; + endVerticalAnchor ??= 0.5; } else if (layoutDirection === 'BT') { - edge.points = [ - this.getTopCenterPoint(fromTarget), - this.getBottomCenterPoint(toTarget), - ]; + startHorizontalAnchor ??= 0.5; + startVerticalAnchor ??= 0; + endHorizontalAnchor ??= 0.5; + endVerticalAnchor ??= 1; } else { - edge.points = [ - this.getBottomCenterPoint(fromTarget), - this.getTopCenterPoint(toTarget), - ]; + startHorizontalAnchor ??= 0.5; + startVerticalAnchor ??= 1; + endHorizontalAnchor ??= 0.5; + endVerticalAnchor ??= 0; } + + const startPoint = { + x: fromTarget.x + + this.anchorPoint( + fromTarget.width, + startHorizontalAnchor, + edge.startSnapPoint?.horizontalOffset ?? 0, + ), + y: fromTarget.y + + this.anchorPoint( + fromTarget.height, + startVerticalAnchor, + edge.startSnapPoint?.verticalOffset ?? 0, + ), + }; + const endPoint = { + x: toTarget.x + + this.anchorPoint( + toTarget.width, + endHorizontalAnchor, + edge.endSnapPoint?.horizontalOffset ?? 0, + ), + y: toTarget.y + + this.anchorPoint( + toTarget.height, + endVerticalAnchor, + edge.endSnapPoint?.verticalOffset ?? 0, + ), + }; + + edge.points = [startPoint, endPoint]; } /** diff --git a/src/app/node_spec.ts b/src/app/node_spec.ts index 48395d8..4bf9dc6 100644 --- a/src/app/node_spec.ts +++ b/src/app/node_spec.ts @@ -412,6 +412,16 @@ export class DagGroup implements } } +// Snap point of the edge +export interface SnapPoint { + // Defaults to how snap points are now calculated + horizontalPercent?: number; + verticalPercent?: number; + // Defaults to 0, 0 + horizontalOffset?: number; + verticalOffset?: number; +} + /** * Dag Edge relationship * @@ -436,6 +446,9 @@ export interface DagEdge { weight?: number; /** The number of ranks to keep between the source and target of the edge. */ minlen?: number; + // Only relevant for snapped edges + startSnapPoint?: SnapPoint; + endSnapPoint?: SnapPoint; } /** diff --git a/src/app/scuba_goldens/directed_acyclic_graph/chrome-linux/graph_with_edge_offsets.png b/src/app/scuba_goldens/directed_acyclic_graph/chrome-linux/graph_with_edge_offsets.png new file mode 100644 index 0000000..72b0609 Binary files /dev/null and b/src/app/scuba_goldens/directed_acyclic_graph/chrome-linux/graph_with_edge_offsets.png differ diff --git a/src/app/test_resources/fake_data.ts b/src/app/test_resources/fake_data.ts index 2022c1d..25a06ca 100644 --- a/src/app/test_resources/fake_data.ts +++ b/src/app/test_resources/fake_data.ts @@ -20,7 +20,7 @@ * various testing scenarios */ -import {baseColors, CustomNode, DagNode, DagNodeSkeleton, DagSkeleton, repeatedMetaNodes, StateTable} from '../node_spec'; +import {baseColors, CustomNode, DagNode, DagNodeSkeleton, DagSkeleton, repeatedMetaNodes, SnapPoint, StateTable} from '../node_spec'; /** * The standard Fake Graph that can be used by `DagNode.createFromSkeleton()` to @@ -657,3 +657,69 @@ export function createDagSkeletonWithGroups(treatAsLoop: boolean): DagSkeleton { ] as DagNodeSkeleton[], }; } + +export const fakeGraphWithEdgeOffsets: DagSkeleton = { + skeleton: [ + { + id: 'node1', + type: 'execution', + next: [ + { + id: 'node2', + type: 'execution', + edgeOpts: { + startSnapPoint: { + horizontalOffset: 5, + verticalOffset: 15, + horizontalPercent: 0.4, + verticalPercent: 0.6, + }, + endSnapPoint: { + horizontalPercent: 0.5, + verticalPercent: 0.5, + }, + }, + }, + ], + }, + { + id: 'node2', + type: 'execution', + next: [ + { + id: 'node3', + type: 'execution', + edgeOpts: { + startSnapPoint: { + horizontalOffset: 15, + verticalOffset: -5, + }, + endSnapPoint: { + horizontalOffset: 30, + verticalPercent: 0.2, + }, + }, + }, + { + id: 'node4', + type: 'execution', + edgeOpts: { + startSnapPoint: {}, + endSnapPoint: { + horizontalAnchor: -0.1, + verticalAnchor: -0.1, + }, + }, + }, + ], + }, + { + id: 'node3', + type: 'execution', + }, + { + id: 'node4', + type: 'execution', + }, + ] as DagNodeSkeleton[], +};