Skip to content

Commit 86847fd

Browse files
committed
grouping nodes WiP
1 parent 07d937d commit 86847fd

File tree

11 files changed

+328
-10
lines changed

11 files changed

+328
-10
lines changed

apps/vps-web/src/styles.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,10 @@ rect-node:has(.hover) > .shape-circle,
274274
cursor: grab;
275275
z-index: 3;
276276
}
277+
.rect-node.group-node {
278+
z-index: -1;
279+
}
280+
277281
.rect-node.selected {
278282
z-index: 4;
279283
}

libs/visual-programming-system/src/canvas-app/index.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,8 @@ export class FlowCanvas<T extends BaseNodeInfo>
261261
this.compositons,
262262
onEditCompositionName ?? (() => Promise.resolve(false)),
263263
isNodeContainer,
264-
getNodeTaskFactory
264+
getNodeTaskFactory,
265+
() => this.onCanvasUpdated
265266
);
266267

267268
this.onDroppedOnNode = onDroppedOnNode;
@@ -1458,7 +1459,8 @@ export class FlowCanvas<T extends BaseNodeInfo>
14581459
this.rootElement,
14591460
this.theme,
14601461
layoutProperties?.customClassName,
1461-
this
1462+
this,
1463+
nodeInfo
14621464
);
14631465
if (!rectInstance || !rectInstance.nodeComponent) {
14641466
throw new Error('rectInstance is undefined');
@@ -1517,7 +1519,8 @@ export class FlowCanvas<T extends BaseNodeInfo>
15171519
this.rootElement,
15181520
this.theme,
15191521
layoutProperties?.customClassName,
1520-
this
1522+
this,
1523+
nodeInfo
15211524
);
15221525
if (!rectInstance || !rectInstance.nodeComponent) {
15231526
throw new Error('rectInstance is undefined');

libs/visual-programming-system/src/components/node-selector.ts

Lines changed: 120 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
IRectNodeComponent,
1616
IThumb,
1717
ThumbConnectionType,
18+
FlowChangeType,
1819
} from '../interfaces';
1920
import { Composition } from '../interfaces/composition';
2021
import { GetNodeTaskFactory } from '../interfaces/node-task-registry';
@@ -50,6 +51,7 @@ export class NodeSelector<T extends BaseNodeInfo> {
5051
leftBottom: IDOMElement | undefined;
5152
rightBottom: IDOMElement | undefined;
5253
createCompositionButtons: IDOMElement | undefined;
54+
createGroupButton: IDOMElement | undefined;
5355
toolsNodesPanel: IDOMElement | undefined;
5456

5557
resizeMode = 'right-bottom';
@@ -61,6 +63,17 @@ export class NodeSelector<T extends BaseNodeInfo> {
6163
elements: ElementNodeMap<T> = new Map();
6264
orgPositionMoveNodes: { [key: string]: { x: number; y: number } } = {};
6365
compositions: Compositions<T>;
66+
getCanvasUpdated?:
67+
| (() =>
68+
| ((
69+
shouldClearExecutionHistory?: boolean,
70+
_isStoreOnly?: boolean,
71+
_flowChangeType?: FlowChangeType,
72+
_node?: INodeComponent<T> | undefined
73+
) => void)
74+
| undefined)
75+
| undefined;
76+
6477
protected cssClasses: ReturnType<typeof getNodeSelectorCssClasses>;
6578
onAddComposition?: (
6679
composition: Composition<T>,
@@ -83,7 +96,17 @@ export class NodeSelector<T extends BaseNodeInfo> {
8396
compositions: Compositions<T>,
8497
onEditCompositionName: () => Promise<string | false>,
8598
isInContainer = false,
86-
getNodeTaskFactory?: GetNodeTaskFactory<T>
99+
getNodeTaskFactory?: GetNodeTaskFactory<T>,
100+
getCanvasUpdated?:
101+
| (() =>
102+
| ((
103+
shouldClearExecutionHistory?: boolean,
104+
_isStoreOnly?: boolean,
105+
_flowChangeType?: FlowChangeType,
106+
_node?: INodeComponent<T> | undefined
107+
) => void)
108+
| undefined)
109+
| undefined
87110
) {
88111
this.canvasApp = canvasApp;
89112
this.cssClasses = getNodeSelectorCssClasses();
@@ -96,6 +119,7 @@ export class NodeSelector<T extends BaseNodeInfo> {
96119
this.onEditCompositionName = onEditCompositionName;
97120
this.isInContainer = isInContainer;
98121
this.getNodeTaskFactory = getNodeTaskFactory;
122+
this.getCanvasUpdated = getCanvasUpdated;
99123

100124
this.nodeSelectorElement = createElement(
101125
'div',
@@ -177,6 +201,15 @@ export class NodeSelector<T extends BaseNodeInfo> {
177201
return;
178202
}
179203

204+
this.createGroupButton = createElement(
205+
'button',
206+
{
207+
class: this.cssClasses.createCompositionButtonClasses,
208+
click: this.onCreateGroup,
209+
},
210+
this.toolsNodesPanel.domElement,
211+
'Create group'
212+
);
180213
this.createCompositionButtons = createElement(
181214
'button',
182215
{
@@ -499,6 +532,92 @@ export class NodeSelector<T extends BaseNodeInfo> {
499532
this.selectionWasPlacedOrMoved = false;
500533
};
501534

535+
onCreateGroup = () => {
536+
const nodeIdsInGroup: string[] = [];
537+
538+
let minX = Infinity,
539+
minY = Infinity;
540+
let maxX = -Infinity,
541+
maxY = -Infinity;
542+
543+
this.selectedNodes.forEach((node) => {
544+
if (node.nodeType === NodeType.Shape) {
545+
nodeIdsInGroup.push(node.id);
546+
547+
const x = node.x;
548+
const y = node.y;
549+
const width = node.width || 0;
550+
const height = node.height || 0;
551+
552+
minX = Math.min(minX, x);
553+
minY = Math.min(minY, y);
554+
maxX = Math.max(maxX, x + width);
555+
maxY = Math.max(maxY, y + height);
556+
}
557+
});
558+
if (
559+
minX === Infinity ||
560+
minX === Infinity ||
561+
maxX === -Infinity ||
562+
maxY === -Infinity
563+
) {
564+
return;
565+
}
566+
if (!this.getNodeTaskFactory || !this.getCanvasUpdated) {
567+
return;
568+
}
569+
const onCanvasUpdated = this.getCanvasUpdated();
570+
if (!onCanvasUpdated) {
571+
return;
572+
}
573+
const factory = this.getNodeTaskFactory('group');
574+
575+
if (factory) {
576+
const nodeTask = factory(onCanvasUpdated, this.canvasApp.theme);
577+
const nodeInfo = undefined;
578+
const node = nodeTask.createVisualNode(
579+
this.canvasApp,
580+
minX - 50,
581+
minY - 50,
582+
undefined,
583+
undefined,
584+
undefined,
585+
maxX - minX + 100,
586+
maxY - minY + 100,
587+
undefined,
588+
nodeInfo
589+
);
590+
if (node && node.nodeInfo) {
591+
// TODO : IMPROVE THIS
592+
(node.nodeInfo as any).taskType = 'group';
593+
node.nodeInfo.isGroup = true;
594+
node.nodeInfo.groupedNodeIds = nodeIdsInGroup;
595+
}
596+
node.nodesInGroup = this.selectedNodes.filter(
597+
(node) => node.nodeType === NodeType.Shape
598+
) as unknown as IRectNodeComponent<T>[];
599+
this.selectedNodes.forEach((nodeInSelection) => {
600+
if (nodeInSelection.nodeType === NodeType.Shape) {
601+
(nodeInSelection as unknown as IRectNodeComponent<T>).groupNode =
602+
node;
603+
if ((nodeInSelection as unknown as IRectNodeComponent<T>).nodeInfo) {
604+
const nodeInfo = (
605+
nodeInSelection as unknown as IRectNodeComponent<T>
606+
).nodeInfo;
607+
if (nodeInfo) {
608+
nodeInfo.isInGroup = true;
609+
nodeInfo.groupId = node.id;
610+
}
611+
}
612+
}
613+
});
614+
node.updateEnd?.();
615+
this.selectedNodes = [];
616+
this.selectionWasPlacedOrMoved = false;
617+
this.removeSelector();
618+
}
619+
};
620+
502621
/*
503622
TODO : for Flow-canvas when a selection contains certain nodes or combination of nodes a
504623
composition can not be created:

libs/visual-programming-system/src/components/rect-thumb.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,8 @@ export class RectThumb<T extends BaseNodeInfo> extends Rect<T> {
5757
rootElement?: HTMLElement,
5858
theme?: Theme,
5959
customClassName?: string,
60-
canvasApp?: IBaseFlow<T>
60+
canvasApp?: IBaseFlow<T>,
61+
nodeInfo?: BaseNodeInfo
6162
) {
6263
super(
6364
canvas,
@@ -85,7 +86,8 @@ export class RectThumb<T extends BaseNodeInfo> extends Rect<T> {
8586
rootElement,
8687
theme,
8788
customClassName,
88-
canvasApp
89+
canvasApp,
90+
nodeInfo
8991
);
9092
if (!this.nodeComponent) {
9193
throw new Error('nodeComponent not created');

libs/visual-programming-system/src/components/rect.ts

Lines changed: 78 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ export class Rect<T extends BaseNodeInfo> {
7979
protected theme?: Theme;
8080

8181
protected canvasApp?: IBaseFlow<T>;
82+
protected nodeInfo?: BaseNodeInfo;
8283

8384
constructor(
8485
canvas: IElementNode<T>,
@@ -110,13 +111,16 @@ export class Rect<T extends BaseNodeInfo> {
110111
rootElement?: HTMLElement,
111112
theme?: Theme,
112113
customClassName?: string,
113-
canvasApp?: IBaseFlow<T>
114+
canvasApp?: IBaseFlow<T>,
115+
nodeInfo?: BaseNodeInfo
114116
) {
115117
this.cssClasses = getRectNodeCssClasses();
116118
this.canvas = canvas;
117119
this.canvasElements = elements;
118120
this.canvasApp = canvasApp;
119121

122+
this.nodeInfo = nodeInfo;
123+
120124
this.canvasUpdated = canvasUpdated;
121125
this.setCanvasAction = setCanvasAction;
122126
this.nodeTransformer = nodeTransformer;
@@ -1050,7 +1054,8 @@ export class Rect<T extends BaseNodeInfo> {
10501054
},
10511055
canvasElement,
10521056
undefined,
1053-
id
1057+
id,
1058+
this.nodeInfo?.isGroup ?? false
10541059
) as unknown as IRectNodeComponent<T> | undefined;
10551060

10561061
if (!rectContainerElement)
@@ -1306,6 +1311,8 @@ export class Rect<T extends BaseNodeInfo> {
13061311
if (!target || x === undefined || y === undefined || !initiator) {
13071312
return false;
13081313
}
1314+
const orgX = this.points.beginX;
1315+
const orgY = this.points.beginY;
13091316

13101317
if (
13111318
this.nodeComponent &&
@@ -1558,6 +1565,75 @@ export class Rect<T extends BaseNodeInfo> {
15581565
this.nodeComponent
15591566
);
15601567
}
1568+
if (this.nodeInfo?.isGroup) {
1569+
const groupOfInitiator = initiator?.nodeInfo?.groupId;
1570+
if (groupOfInitiator && groupOfInitiator !== this.nodeComponent.id) {
1571+
this.nodeInfo?.groupedNodeIds?.forEach((id) => {
1572+
const groupedNode = this.canvasApp?.elements.get(id);
1573+
if (groupedNode && groupedNode.id !== initiator.id) {
1574+
const groupedNodeInstance =
1575+
groupedNode as unknown as IRectNodeComponent<T>;
1576+
const newX = this.points.beginX - orgX + groupedNodeInstance.x;
1577+
const newY = this.points.beginY - orgY + groupedNodeInstance.y;
1578+
if (groupedNodeInstance.update) {
1579+
groupedNodeInstance.update(
1580+
groupedNodeInstance,
1581+
newX,
1582+
newY,
1583+
this.nodeComponent
1584+
);
1585+
}
1586+
}
1587+
});
1588+
}
1589+
}
1590+
if (this.nodeInfo?.isInGroup && this.nodeInfo?.groupId) {
1591+
const groupNode = this.canvasApp?.elements.get(this.nodeInfo.groupId);
1592+
if (groupNode && groupNode.id !== initiator.id) {
1593+
const groupNodeInstance =
1594+
groupNode as unknown as IRectNodeComponent<T>;
1595+
1596+
let minX = Infinity,
1597+
minY = Infinity;
1598+
let maxX = -Infinity,
1599+
maxY = -Infinity;
1600+
if (groupNode.nodeInfo?.groupedNodeIds) {
1601+
groupNode.nodeInfo.groupedNodeIds.forEach((nodeId) => {
1602+
const groupedNode = this.canvasApp?.elements.get(
1603+
nodeId
1604+
) as unknown as IRectNodeComponent<T>;
1605+
if (groupedNode) {
1606+
const x = groupedNode.x;
1607+
const y = groupedNode.y;
1608+
const width = groupedNode.width || 0;
1609+
const height = groupedNode.height || 0;
1610+
1611+
minX = Math.min(minX, x);
1612+
minY = Math.min(minY, y);
1613+
maxX = Math.max(maxX, x + width);
1614+
maxY = Math.max(maxY, y + height);
1615+
}
1616+
});
1617+
}
1618+
if (
1619+
minX !== Infinity &&
1620+
minX !== Infinity &&
1621+
maxX !== -Infinity &&
1622+
maxY !== -Infinity
1623+
) {
1624+
if (groupNodeInstance.update) {
1625+
groupNodeInstance.setSize(maxX - minX + 100, maxY - minY + 100);
1626+
groupNodeInstance.update(
1627+
groupNodeInstance,
1628+
minX - 50,
1629+
minY - 50,
1630+
this.nodeComponent
1631+
);
1632+
}
1633+
}
1634+
}
1635+
}
1636+
15611637
if ((this.nodeComponent?.nodeInfo as any)?.update) {
15621638
(this.nodeComponent.nodeInfo as any).update();
15631639
}

libs/visual-programming-system/src/interfaces/element.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@ export interface IRectNodeComponent<T extends BaseNodeInfo>
8787
thumbs: IThumb[];
8888
isSettingSize?: boolean;
8989
setSize: (width: number, height: number) => void;
90+
groupNode?: IRectNodeComponent<T>;
91+
nodesInGroup?: IRectNodeComponent<T>[];
9092
}
9193

9294
export interface IConnectionNodeComponent<T extends BaseNodeInfo>

libs/visual-programming-system/src/types/base-node-info.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,11 @@ export interface BaseNodeInfo extends BaseSettingsNodeInfo {
122122
isOCIFNode?: boolean;
123123
compositionId?: string;
124124
isComposition?: boolean;
125+
isGroup?: boolean;
126+
isInGroup?: boolean;
127+
groupedNodeIds?: string[];
128+
groupId?: string;
129+
125130
cancelPreview?: () => void;
126131
outputConnectionInfo?: {
127132
text: string;

libs/visual-programming-system/src/utils/create-element.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,8 @@ export const createNodeElement = <T extends BaseNodeInfo>(
8989
attributes?: Record<string, string | number | object | EventHandler>,
9090
parent?: DOMElementNode,
9191
content?: string | HTMLElement | JSX.Element,
92-
id?: string
92+
id?: string,
93+
shouldAddAsFirstChild?: boolean
9394
): IElementNode<T> | undefined => {
9495
if (typeof document === 'undefined') {
9596
return undefined;
@@ -135,7 +136,11 @@ export const createNodeElement = <T extends BaseNodeInfo>(
135136
});
136137
}
137138
if (parent) {
138-
parent.appendChild(domElement);
139+
if (shouldAddAsFirstChild) {
140+
(parent as HTMLElement).prepend(domElement);
141+
} else {
142+
parent.appendChild(domElement);
143+
}
139144
}
140145
if (content && elementName) {
141146
if (typeof content === 'string') {

0 commit comments

Comments
 (0)