|
18 | 18 | import {LiveAnnouncer} from '@angular/cdk/a11y'; |
19 | 19 | import {CdkDrag, CdkDragMove, CdkDragStart, DragDropModule} from '@angular/cdk/drag-drop'; |
20 | 20 | import {CommonModule} from '@angular/common'; |
21 | | -import {ChangeDetectionStrategy, ChangeDetectorRef, Component, DoCheck, ElementRef, EventEmitter, Input, KeyValueDiffer, KeyValueDiffers, NgModule, OnChanges, OnDestroy, OnInit, Optional, Output, QueryList, SimpleChanges, TemplateRef, ViewChildren} from '@angular/core'; |
| 21 | +import {ChangeDetectionStrategy, ChangeDetectorRef, Component, DoCheck, ElementRef, EventEmitter, Input, KeyValueDiffer, KeyValueDiffers, NgModule, OnChanges, OnDestroy, OnInit, Optional, Output, QueryList, SimpleChanges, TemplateRef, ViewChild, ViewChildren} from '@angular/core'; |
22 | 22 | import * as dagre from '@dagrejs/dagre'; |
23 | 23 | import {Subscription} from 'rxjs'; |
24 | 24 |
|
@@ -356,6 +356,12 @@ export class DagRaw implements DoCheck, OnInit, OnDestroy { |
356 | 356 | */ |
357 | 357 | @Input() visible = true; |
358 | 358 |
|
| 359 | + @Input() internalEdges: DagEdge[] = []; |
| 360 | + @Input() |
| 361 | + externalTargets: |
| 362 | + Map<string, {nodes: string[], point: Point, type: 'in'|'out'}> = |
| 363 | + new Map(); |
| 364 | + |
359 | 365 | @Input('nodes') |
360 | 366 | set nodes(nodes: DagNode[]) { |
361 | 367 | // Avoid pointer/reference stability, so that angular will pick up the |
@@ -388,6 +394,9 @@ export class DagRaw implements DoCheck, OnInit, OnDestroy { |
388 | 394 | return this.$groups as EnhancedDagGroup[]; |
389 | 395 | } |
390 | 396 |
|
| 397 | + @ViewChild('defaultProxyNodeTemplate', {static: true}) |
| 398 | + defaultProxyNodeTemplate!: TemplateRef<any>; |
| 399 | + |
391 | 400 | @ViewChildren('subDag') subDags?: QueryList<DagRaw>; |
392 | 401 |
|
393 | 402 | @Input() hoveredEdge?: DagEdge; |
@@ -482,7 +491,9 @@ export class DagRaw implements DoCheck, OnInit, OnDestroy { |
482 | 491 | } |
483 | 492 |
|
484 | 493 | ngOnChanges(changes: SimpleChanges) { |
485 | | - if (changes['nodes'] || changes['edges'] || changes['groups']) { |
| 494 | + if (changes['nodes'] || changes['edges'] || changes['groups'] || |
| 495 | + changes['externalTargets']) { |
| 496 | + this.generateRenderableData(); |
486 | 497 | this.updateGraphLayoutAndReselect(); |
487 | 498 | } |
488 | 499 | } |
@@ -533,6 +544,158 @@ export class DagRaw implements DoCheck, OnInit, OnDestroy { |
533 | 544 | return group.hasControlNode && |
534 | 545 | !(group.hideControlNodeOnExpand && this.isGroupExpanded(group)); |
535 | 546 | } |
| 547 | + /** |
| 548 | + * Converts a point in the parent's coordinates to the child's coordinates. |
| 549 | + */ |
| 550 | + convertParentPointToChild(point: Point, groupId: string): Point|undefined { |
| 551 | + const group = this.nodeMap.groups[groupId]?.group as EnhancedDagGroup; |
| 552 | + |
| 553 | + if (!group || group.x === undefined || group.y === undefined) { |
| 554 | + console.error(`Group ${groupId} not found or not yet positioned.`); |
| 555 | + return undefined; |
| 556 | + } |
| 557 | + |
| 558 | + // group.x and group.y are the center of the group in the parent's |
| 559 | + // coordinates. |
| 560 | + const groupOriginX = group.x - group.width / 2; |
| 561 | + const groupOriginY = group.y - group.height / 2; |
| 562 | + |
| 563 | + // The padY is added to the height, and the group content is shifted down |
| 564 | + // by padY So, we need to account for this shift. |
| 565 | + const offsetY = group.padY ? group.padY : 0; |
| 566 | + // const offsetY = 0; |
| 567 | + |
| 568 | + return { |
| 569 | + x: point.x - groupOriginX, |
| 570 | + y: point.y - groupOriginY - offsetY, |
| 571 | + }; |
| 572 | + } |
| 573 | + /** |
| 574 | + * Calculates which nodes inside a given group are targeted by edges |
| 575 | + * originating outside of that group. |
| 576 | + */ |
| 577 | + getExternalTargetsForGroup(group: DagGroup): |
| 578 | + Map<string, {nodes: string[], point: Point, type: 'in'|'out'}> { |
| 579 | + const targetsMap = |
| 580 | + new Map<string, {nodes: string[], point: Point, type: 'in' | 'out'}>(); |
| 581 | + if (!group) return targetsMap; |
| 582 | + |
| 583 | + // Create a Set of all node/group IDs that are inside the target group |
| 584 | + // for |
| 585 | + // fast lookups. |
| 586 | + const internalIds = new Set([ |
| 587 | + ...group.nodes.map(n => n.id), |
| 588 | + ...group.groups.map(g => g.id), |
| 589 | + ]); |
| 590 | + |
| 591 | + // Find edges that cross the boundary into or out of the group. |
| 592 | + for (const edge of this.edges) { |
| 593 | + const sourceIsInternal = internalIds.has(edge.from); |
| 594 | + const targetIsInternal = internalIds.has(edge.to); |
| 595 | + const sourceIsGroup = edge.from === group.id; |
| 596 | + const targetIsGroup = edge.to === group.id; |
| 597 | + |
| 598 | + // An incoming edge has an external source and targets the group. |
| 599 | + if (!sourceIsInternal && targetIsGroup && edge.points?.length) { |
| 600 | + let intersectionPoint = edge.points[edge.points.length - 1]; |
| 601 | + |
| 602 | + const childIntersectionPoint = this.convertParentPointToChild( |
| 603 | + intersectionPoint, |
| 604 | + group.id, |
| 605 | + ); |
| 606 | + if (childIntersectionPoint) { |
| 607 | + intersectionPoint = childIntersectionPoint; |
| 608 | + if (!targetsMap.has(edge.from)) { |
| 609 | + targetsMap.set( |
| 610 | + edge.from, {nodes: [], point: intersectionPoint, type: 'in'}); |
| 611 | + } |
| 612 | + } |
| 613 | + } |
| 614 | + // An outgoing edge has the group as source and targets an external node. |
| 615 | + else if (sourceIsGroup && !targetIsInternal && edge.points?.length) { |
| 616 | + let intersectionPoint = edge.points[0]; |
| 617 | + |
| 618 | + const childIntersectionPoint = this.convertParentPointToChild( |
| 619 | + intersectionPoint, |
| 620 | + group.id, |
| 621 | + ); |
| 622 | + if (childIntersectionPoint) { |
| 623 | + intersectionPoint = childIntersectionPoint; |
| 624 | + if (!targetsMap.has(edge.to)) { |
| 625 | + targetsMap.set( |
| 626 | + edge.to, {nodes: [], point: intersectionPoint, type: 'out'}); |
| 627 | + } |
| 628 | + } |
| 629 | + } |
| 630 | + } |
| 631 | + |
| 632 | + // Find internal nodes connected to the external nodes |
| 633 | + for (const edge of this.internalEdges) { |
| 634 | + const sourceIsExternal = targetsMap.has(edge.from); |
| 635 | + const targetIsInternal = internalIds.has(edge.to); |
| 636 | + const sourceIsInternal = internalIds.has(edge.from); |
| 637 | + const targetIsExternal = targetsMap.has(edge.to); |
| 638 | + |
| 639 | + if (sourceIsExternal && targetIsInternal) { |
| 640 | + targetsMap.get(edge.from)!.nodes.push(edge.to); |
| 641 | + } else if (sourceIsInternal && targetIsExternal) { |
| 642 | + targetsMap.get(edge.to)!.nodes.push(edge.from); |
| 643 | + } |
| 644 | + } |
| 645 | + |
| 646 | + return targetsMap; |
| 647 | + } |
| 648 | + |
| 649 | + generateRenderableData() { |
| 650 | + // Remove existing proxy nodes and edges |
| 651 | + this.nodes = this.nodes.filter(node => !node.id.startsWith('__proxy_in_')); |
| 652 | + this.edges = |
| 653 | + this.edges.filter(edge => !edge.from.startsWith('__proxy_in_')); |
| 654 | + this.edges = this.edges.filter(edge => !edge.to.startsWith('__proxy_in_')) |
| 655 | + |
| 656 | + let proxyNodes: DagNode[] = []; |
| 657 | + let proxyEdges: DagEdge[] = []; |
| 658 | + // Start with the original data |
| 659 | + // If there are external targets, create and inject the proxy node and |
| 660 | + // edges |
| 661 | + if (this.externalTargets && this.externalTargets.size > 0) { |
| 662 | + // Sort the external targets based on the Y coordinate of their |
| 663 | + // intersection point |
| 664 | + const sortedTargetIds = |
| 665 | + Array.from(this.externalTargets.keys()).sort((a, b) => { |
| 666 | + const pointY_A = this.externalTargets.get(a)!.point.y; |
| 667 | + const pointY_B = this.externalTargets.get(b)!.point.y; |
| 668 | + return pointY_A - pointY_B; |
| 669 | + }); |
| 670 | + |
| 671 | + for (const targetId of sortedTargetIds) { |
| 672 | + console.log('targetId', this.externalTargets.get(targetId)!.point.y); |
| 673 | + const targetData = this.externalTargets.get(targetId)!; |
| 674 | + const PROXY_NODE_ID = `__proxy_in_${targetId}`; |
| 675 | + const proxyNode = new CustomNode( |
| 676 | + new DagNode(PROXY_NODE_ID, 'artifact'), |
| 677 | + 'proxyNode', // Or your custom template ref |
| 678 | + 5, // Width |
| 679 | + 5, // Height |
| 680 | + ); |
| 681 | + proxyNodes.push(proxyNode); |
| 682 | + for (let j = 0; j < targetData.nodes.length; j++) { |
| 683 | + const targetNodeId = targetData.nodes[j]; |
| 684 | + const PROXY_EDGE_ID = `${PROXY_NODE_ID}_${targetNodeId}`; |
| 685 | + const isOutgoing = targetData.type === 'out'; |
| 686 | + const proxyEdge = { |
| 687 | + from: isOutgoing ? targetNodeId : PROXY_NODE_ID, |
| 688 | + to: isOutgoing ? PROXY_NODE_ID : targetNodeId, |
| 689 | + style: 'dashed', |
| 690 | + }; |
| 691 | + proxyEdges.push(proxyEdge); |
| 692 | + } |
| 693 | + } |
| 694 | + // Add the new elements to our renderable arrays |
| 695 | + this.nodes = [...proxyNodes, ...this.nodes]; |
| 696 | + this.edges = [...this.edges, ...proxyEdges]; |
| 697 | + } |
| 698 | + } |
536 | 699 |
|
537 | 700 | showGroupLabel(group: DagGroup) { |
538 | 701 | if (!group.groupLabel) return false; |
@@ -598,6 +761,11 @@ export class DagRaw implements DoCheck, OnInit, OnDestroy { |
598 | 761 |
|
599 | 762 | getCustomNodeTemplateFor(node: CustomNode) { |
600 | 763 | const {templateRef} = node; |
| 764 | + if (templateRef === 'proxyNode' && !this.customNodeTemplates['proxyNode']) { |
| 765 | + console.log('proxyNode template not found'); |
| 766 | + console.log(this.defaultProxyNodeTemplate); |
| 767 | + return this.defaultProxyNodeTemplate; |
| 768 | + } |
601 | 769 | return this.customNodeTemplates[templateRef]; |
602 | 770 | } |
603 | 771 |
|
@@ -1084,6 +1252,12 @@ export class DagRaw implements DoCheck, OnInit, OnDestroy { |
1084 | 1252 | ]; |
1085 | 1253 | } |
1086 | 1254 |
|
| 1255 | + buildProxyPath(start: Point, end: Point): string { |
| 1256 | + const [cp1, cp2] = this.getControlPointsForBezierCurve(start, end); |
| 1257 | + return `M${start.x},${start.y} C${cp1.x},${cp1.y} ${cp2.x},${cp2.y} ${ |
| 1258 | + end.x},${end.y}`; |
| 1259 | + } |
| 1260 | + |
1087 | 1261 | /** |
1088 | 1262 | * When the edge is in a reversed direction, in order to avoid overlapping |
1089 | 1263 | * between the edges and nodes, the position of the edges in the reversed |
|
0 commit comments