@luxonis/depthai-pipeline-lib 1.13.0 → 1.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/panda.css +24 -8
- package/dist/src/components/BridgeEdge.d.ts +7 -0
- package/dist/src/components/BridgeEdge.js +58 -0
- package/dist/src/components/Node.js +21 -14
- package/dist/src/components/PipelineCanvas.d.ts +3 -5
- package/dist/src/components/PipelineCanvas.js +10 -213
- package/dist/src/components/PipelineLegend.d.ts +6 -0
- package/dist/src/components/PipelineLegend.js +181 -0
- package/dist/src/services/pipeline.d.ts +0 -26
- package/dist/src/services/pipeline.js +2 -34
- package/dist/src/services/utils.d.ts +36 -0
- package/dist/src/services/utils.js +234 -0
- package/package.json +1 -1
package/dist/panda.css
CHANGED
|
@@ -1252,6 +1252,22 @@
|
|
|
1252
1252
|
|
|
1253
1253
|
@layer utilities{
|
|
1254
1254
|
|
|
1255
|
+
.w_16px {
|
|
1256
|
+
width: 16px;
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
.h_16px {
|
|
1260
|
+
height: 16px;
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
.d_flex {
|
|
1264
|
+
display: flex;
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
.pointer-events_none {
|
|
1268
|
+
pointer-events: none;
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1255
1271
|
.min-w_container\.smaller\.xxs {
|
|
1256
1272
|
min-width: var(--sizes-container-smaller-xxs);
|
|
1257
1273
|
}
|
|
@@ -1305,10 +1321,6 @@
|
|
|
1305
1321
|
height: 20px;
|
|
1306
1322
|
}
|
|
1307
1323
|
|
|
1308
|
-
.d_flex {
|
|
1309
|
-
display: flex;
|
|
1310
|
-
}
|
|
1311
|
-
|
|
1312
1324
|
.w_auto {
|
|
1313
1325
|
width: auto;
|
|
1314
1326
|
}
|
|
@@ -1317,6 +1329,14 @@
|
|
|
1317
1329
|
height: auto;
|
|
1318
1330
|
}
|
|
1319
1331
|
|
|
1332
|
+
.ai_center {
|
|
1333
|
+
align-items: center;
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
.jc_center {
|
|
1337
|
+
justify-content: center;
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1320
1340
|
.border-style_dashed {
|
|
1321
1341
|
border-style: dashed;
|
|
1322
1342
|
}
|
|
@@ -1357,10 +1377,6 @@
|
|
|
1357
1377
|
background-color: var(--colors-light-gray);
|
|
1358
1378
|
}
|
|
1359
1379
|
|
|
1360
|
-
.ai_center {
|
|
1361
|
-
align-items: center;
|
|
1362
|
-
}
|
|
1363
|
-
|
|
1364
1380
|
.right_xs {
|
|
1365
1381
|
right: var(--spacing-xs);
|
|
1366
1382
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type EdgeProps } from '@xyflow/react';
|
|
2
|
+
type BridgeEdgeProps = EdgeProps & {
|
|
3
|
+
data: any;
|
|
4
|
+
type: any;
|
|
5
|
+
};
|
|
6
|
+
export declare const BridgeEdge: ({ id, sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition, style, markerStart, markerEnd, interactionWidth, pathOptions, }: BridgeEdgeProps) => import("react/jsx-runtime").JSX.Element;
|
|
7
|
+
export {};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { ArrowRightSideIcon, Icon } from '@luxonis/common-fe-components/icons';
|
|
3
|
+
import { BaseEdge, EdgeLabelRenderer, getSmoothStepPath, } from '@xyflow/react';
|
|
4
|
+
import React from 'react';
|
|
5
|
+
import { css } from '../styled-system/css/css.mjs';
|
|
6
|
+
const bridgeArrowClassName = css({
|
|
7
|
+
width: '16px',
|
|
8
|
+
height: '16px',
|
|
9
|
+
display: 'flex',
|
|
10
|
+
alignItems: 'center',
|
|
11
|
+
justifyContent: 'center',
|
|
12
|
+
pointerEvents: 'none',
|
|
13
|
+
});
|
|
14
|
+
export const BridgeEdge = ({ id, sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition, style, markerStart, markerEnd, interactionWidth, pathOptions, }) => {
|
|
15
|
+
const pathRef = React.useRef(null);
|
|
16
|
+
const [path, labelX, labelY] = getSmoothStepPath({
|
|
17
|
+
sourceX,
|
|
18
|
+
sourceY,
|
|
19
|
+
targetX,
|
|
20
|
+
targetY,
|
|
21
|
+
sourcePosition,
|
|
22
|
+
targetPosition,
|
|
23
|
+
borderRadius: pathOptions?.borderRadius,
|
|
24
|
+
offset: pathOptions?.offset,
|
|
25
|
+
});
|
|
26
|
+
const [arrowState, setArrowState] = React.useState({
|
|
27
|
+
x: labelX,
|
|
28
|
+
y: labelY,
|
|
29
|
+
angle: 0,
|
|
30
|
+
});
|
|
31
|
+
React.useEffect(() => {
|
|
32
|
+
const pathElement = pathRef.current;
|
|
33
|
+
if (!pathElement) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const totalLength = pathElement.getTotalLength();
|
|
37
|
+
const halfLength = totalLength / 2;
|
|
38
|
+
const sampleOffset = Math.min(12, totalLength / 4);
|
|
39
|
+
const startPoint = pathElement.getPointAtLength(Math.max(0, halfLength - sampleOffset));
|
|
40
|
+
const endPoint = pathElement.getPointAtLength(Math.min(totalLength, halfLength + sampleOffset));
|
|
41
|
+
const nextArrowState = {
|
|
42
|
+
x: (startPoint.x + endPoint.x) / 2,
|
|
43
|
+
y: (startPoint.y + endPoint.y) / 2,
|
|
44
|
+
angle: Math.atan2(endPoint.y - startPoint.y, endPoint.x - startPoint.x) *
|
|
45
|
+
(180 / Math.PI),
|
|
46
|
+
};
|
|
47
|
+
setArrowState((currentArrowState) => {
|
|
48
|
+
const didChange = Math.abs(currentArrowState.x - nextArrowState.x) > 0.5 ||
|
|
49
|
+
Math.abs(currentArrowState.y - nextArrowState.y) > 0.5 ||
|
|
50
|
+
Math.abs(currentArrowState.angle - nextArrowState.angle) > 0.5;
|
|
51
|
+
return didChange ? nextArrowState : currentArrowState;
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
return (_jsxs(_Fragment, { children: [_jsx("path", { ref: pathRef, d: path, fill: "none", stroke: "transparent" }), _jsx(BaseEdge, { id: id, path: path, style: style, markerStart: markerStart, markerEnd: markerEnd, interactionWidth: interactionWidth }), _jsx(EdgeLabelRenderer, { children: _jsx("div", { className: bridgeArrowClassName, style: {
|
|
55
|
+
position: 'absolute',
|
|
56
|
+
transform: `translate(-50%, -50%) translate(${arrowState.x}px, ${arrowState.y}px) rotate(${arrowState.angle}deg)`,
|
|
57
|
+
}, children: _jsx(Icon, { icon: ArrowRightSideIcon, size: "sm", color: "gray", pointerEvents: "none" }) }) })] }));
|
|
58
|
+
};
|
|
@@ -2,10 +2,9 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { Badge, Button, Flex, HelpIcon, Label, SubLabel, } from '@luxonis/common-fe-components';
|
|
3
3
|
import { clsx } from '@luxonis/common-fe-components/helpers';
|
|
4
4
|
import { Handle, Position } from '@xyflow/react';
|
|
5
|
-
import { DOCS_BASE_URL, NodesWithLinks, } from '../services/pipeline.js';
|
|
6
5
|
import { formatTiming } from '../services/pipeline-state.js';
|
|
6
|
+
import { DOCS_BASE_URL, MIN_NODE_WIDTH, NodesWithLinks, } from '../services/utils.js';
|
|
7
7
|
import { css } from '../styled-system/css/css.mjs';
|
|
8
|
-
import { MIN_NODE_WIDTH } from './PipelineCanvas.js';
|
|
9
8
|
const NodeHandles = (props) => {
|
|
10
9
|
const { handles, type } = props;
|
|
11
10
|
return (_jsx(Flex, { full: true, direction: "column", align: type === 'input' ? 'start' : 'end', minWidth: "120px", children: handles.map(({ type: handleType, id, blocking, queueSize, name, maxQueueSize, fps, connected, dotColor, }) => {
|
|
@@ -15,20 +14,28 @@ const NodeHandles = (props) => {
|
|
|
15
14
|
return (_jsxs(Flex, { position: "relative", align: "end", direction: handleType === 'input' ? 'row' : 'row-reverse', children: [_jsx(Handle, { type: handleType === 'input' ? 'target' : 'source', position: handleType === 'input' ? Position.Left : Position.Right, id: name, isConnectable: false, className: css({
|
|
16
15
|
width: 'custom.handle.dot!',
|
|
17
16
|
height: 'custom.handle.dot!',
|
|
18
|
-
backgroundColor:
|
|
19
|
-
? 'dark.
|
|
20
|
-
: dotColor === '
|
|
21
|
-
? 'dark.
|
|
22
|
-
: dotColor === '
|
|
23
|
-
? 'dark.
|
|
24
|
-
: dotColor === '
|
|
25
|
-
? 'dark.
|
|
26
|
-
:
|
|
27
|
-
? 'dark.warning!'
|
|
28
|
-
: 'dark.success!',
|
|
17
|
+
backgroundColor: dotColor === 'gray'
|
|
18
|
+
? 'dark.gray!'
|
|
19
|
+
: dotColor === 'green'
|
|
20
|
+
? 'dark.success!'
|
|
21
|
+
: dotColor === 'red'
|
|
22
|
+
? 'dark.error!'
|
|
23
|
+
: dotColor === 'yellow'
|
|
24
|
+
? 'dark.warning!'
|
|
25
|
+
: 'dark.success!',
|
|
29
26
|
border: 'none!',
|
|
30
27
|
borderRadius: blocking ? '0% !important' : '100% !important',
|
|
31
|
-
})
|
|
28
|
+
}), style: {
|
|
29
|
+
background: dotColor === 'gray'
|
|
30
|
+
? 'dark.gray!'
|
|
31
|
+
: dotColor === 'green'
|
|
32
|
+
? 'dark.success!'
|
|
33
|
+
: dotColor === 'red'
|
|
34
|
+
? 'dark.error!'
|
|
35
|
+
: dotColor === 'yellow'
|
|
36
|
+
? 'dark.warning!'
|
|
37
|
+
: 'dark.gray!',
|
|
38
|
+
} }), _jsx(NodeHandlesSubLabel, { type: type, name: name, queueSize: queueSize, maxQueueSize: maxQueueSize, fps: fps })] }, id));
|
|
32
39
|
}) }));
|
|
33
40
|
};
|
|
34
41
|
const NodeHandlesSubLabel = (props) => {
|
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
import type { FlexProps } from '@luxonis/common-fe-components';
|
|
2
2
|
import React from 'react';
|
|
3
|
-
import type {
|
|
3
|
+
import type { Pipeline } from '../services/pipeline.js';
|
|
4
4
|
import type { PipelineState } from '../services/pipeline-state.js';
|
|
5
5
|
export type PipelineCanvasProps = FlexProps & {
|
|
6
6
|
pipeline: Pipeline | null;
|
|
7
7
|
pipelineState: PipelineState[] | null;
|
|
8
|
-
|
|
8
|
+
header?: React.ReactNode;
|
|
9
|
+
isDebugging?: boolean;
|
|
9
10
|
};
|
|
10
|
-
export declare const MIN_NODE_WIDTH = 376;
|
|
11
|
-
export declare const MIN_NODE_HEIGHT = 150;
|
|
12
|
-
export declare const updateNodesOnPipelineStateChange: (nodes: ParsedNode[], pipelineState: PipelineState[]) => ParsedNode[];
|
|
13
11
|
export declare const PipelineCanvas: React.FC<PipelineCanvasProps>;
|
|
@@ -1,218 +1,14 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import { Panel, ReactFlow, ReactFlowProvider, useEdgesState, useNodesState, useReactFlow, useStore, } from '@xyflow/react';
|
|
2
|
+
import { Button, Flex, Header } from '@luxonis/common-fe-components';
|
|
3
|
+
import { BezierEdge, Panel, ReactFlow, ReactFlowProvider, useEdgesState, useNodesState, useReactFlow, useStore, } from '@xyflow/react';
|
|
5
4
|
import React from 'react';
|
|
6
|
-
import {
|
|
5
|
+
import { adjustNodes, getLayoutedElements, updateNodesOnPipelineStateChange, } from '../services/utils.js';
|
|
6
|
+
import { BridgeEdge } from './BridgeEdge.js';
|
|
7
7
|
import { PipelineNode } from './Node.js';
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
graph.setGraph({ rankdir: 'LR' });
|
|
13
|
-
for (const edge of edges) {
|
|
14
|
-
graph.setEdge(edge.source, edge.target);
|
|
15
|
-
}
|
|
16
|
-
for (const node of nodes) {
|
|
17
|
-
graph.setNode(node.id, {
|
|
18
|
-
...node,
|
|
19
|
-
width: MIN_NODE_WIDTH,
|
|
20
|
-
height: MIN_NODE_HEIGHT,
|
|
21
|
-
});
|
|
22
|
-
}
|
|
23
|
-
Dagre.layout(graph);
|
|
24
|
-
return {
|
|
25
|
-
nodes: nodes.map((node) => {
|
|
26
|
-
const position = graph.node(node.id);
|
|
27
|
-
const x = position.x - MIN_NODE_WIDTH / 2;
|
|
28
|
-
const y = position.y - MIN_NODE_HEIGHT / 2;
|
|
29
|
-
return { ...node, position: { x, y } };
|
|
30
|
-
}),
|
|
31
|
-
edges,
|
|
32
|
-
};
|
|
33
|
-
};
|
|
34
|
-
const adjustNodes = (nodes, edges) => {
|
|
35
|
-
const PADDING = 16;
|
|
36
|
-
const HEADER_HEIGHT = 24;
|
|
37
|
-
// Build a tree: find all group nodes and their depth
|
|
38
|
-
const groupNodes = nodes.filter((node) => node.type === 'group');
|
|
39
|
-
const childNodes = nodes.filter((node) => node.type !== 'group');
|
|
40
|
-
// Sort groups by depth (deepest first) so children are resolved before parents
|
|
41
|
-
const getDepth = (nodeId, depth = 0) => {
|
|
42
|
-
const node = nodes.find((n) => n.id === nodeId);
|
|
43
|
-
if (!node?.parentId)
|
|
44
|
-
return depth;
|
|
45
|
-
return getDepth(node.parentId, depth + 1);
|
|
46
|
-
};
|
|
47
|
-
const sortedGroups = [...groupNodes].sort((a, b) => getDepth(b.id) - getDepth(a.id));
|
|
48
|
-
// Track resolved sizes for groups as we process them
|
|
49
|
-
const resolvedSizes = {};
|
|
50
|
-
// Start with all non-group nodes at their measured sizes
|
|
51
|
-
for (const node of childNodes) {
|
|
52
|
-
resolvedSizes[node.id] = {
|
|
53
|
-
width: Math.min(node.measured?.width ?? MIN_NODE_WIDTH),
|
|
54
|
-
height: Math.min(node.measured?.height ?? MIN_NODE_HEIGHT),
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
const updatedNodes = {};
|
|
58
|
-
for (const node of nodes)
|
|
59
|
-
updatedNodes[node.id] = { ...node };
|
|
60
|
-
// Process groups deepest-first
|
|
61
|
-
for (const group of sortedGroups) {
|
|
62
|
-
const children = nodes.filter((n) => n.parentId === group.id);
|
|
63
|
-
if (children.length === 0) {
|
|
64
|
-
resolvedSizes[group.id] = {
|
|
65
|
-
width: MIN_NODE_WIDTH,
|
|
66
|
-
height: MIN_NODE_HEIGHT,
|
|
67
|
-
};
|
|
68
|
-
continue;
|
|
69
|
-
}
|
|
70
|
-
const g = new Dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({}));
|
|
71
|
-
g.setGraph({ rankdir: 'LR' });
|
|
72
|
-
for (const child of children) {
|
|
73
|
-
// Use resolved size (important for nested groups!)
|
|
74
|
-
const size = resolvedSizes[child.id] ?? {
|
|
75
|
-
width: MIN_NODE_WIDTH,
|
|
76
|
-
height: MIN_NODE_HEIGHT,
|
|
77
|
-
};
|
|
78
|
-
g.setNode(child.id, { width: size.width, height: size.height });
|
|
79
|
-
}
|
|
80
|
-
const groupEdges = edges.filter((e) => children.some((c) => c.id === e.source) &&
|
|
81
|
-
children.some((c) => c.id === e.target));
|
|
82
|
-
for (const edge of groupEdges) {
|
|
83
|
-
g.setEdge(edge.source, edge.target);
|
|
84
|
-
}
|
|
85
|
-
Dagre.layout(g);
|
|
86
|
-
let minX = 9999;
|
|
87
|
-
let minY = 9999;
|
|
88
|
-
let maxX = -9999;
|
|
89
|
-
let maxY = -9999;
|
|
90
|
-
// Replace both child loops with this single pass:
|
|
91
|
-
for (const child of children) {
|
|
92
|
-
const pos = g.node(child.id);
|
|
93
|
-
const size = resolvedSizes[child.id] ?? {
|
|
94
|
-
width: MIN_NODE_WIDTH,
|
|
95
|
-
height: MIN_NODE_HEIGHT,
|
|
96
|
-
};
|
|
97
|
-
const x = pos.x - size.width / 2;
|
|
98
|
-
const y = pos.y - size.height / 2;
|
|
99
|
-
minX = Math.min(minX, x);
|
|
100
|
-
minY = Math.min(minY, y);
|
|
101
|
-
maxX = Math.max(maxX, x + size.width);
|
|
102
|
-
maxY = Math.max(maxY, y + size.height);
|
|
103
|
-
}
|
|
104
|
-
// Now positions are correct, apply them
|
|
105
|
-
for (const child of children) {
|
|
106
|
-
const pos = g.node(child.id);
|
|
107
|
-
const size = resolvedSizes[child.id] ?? {
|
|
108
|
-
width: MIN_NODE_WIDTH,
|
|
109
|
-
height: MIN_NODE_HEIGHT,
|
|
110
|
-
};
|
|
111
|
-
updatedNodes[child.id] = {
|
|
112
|
-
...updatedNodes[child.id],
|
|
113
|
-
position: {
|
|
114
|
-
x: pos.x - size.width / 2 - minX + PADDING,
|
|
115
|
-
y: pos.y - size.height / 2 - minY + PADDING + HEADER_HEIGHT,
|
|
116
|
-
},
|
|
117
|
-
};
|
|
118
|
-
}
|
|
119
|
-
const groupW = maxX - minX + PADDING * 2;
|
|
120
|
-
const groupH = maxY - minY + PADDING * 2 + HEADER_HEIGHT;
|
|
121
|
-
// Store this group's resolved size for its own parent to use
|
|
122
|
-
resolvedSizes[group.id] = { width: groupW, height: groupH };
|
|
123
|
-
updatedNodes[group.id] = {
|
|
124
|
-
...updatedNodes[group.id],
|
|
125
|
-
style: { ...updatedNodes[group.id].style, width: groupW, height: groupH },
|
|
126
|
-
width: groupW,
|
|
127
|
-
height: groupH,
|
|
128
|
-
};
|
|
129
|
-
}
|
|
130
|
-
// Now lay out the top-level (root) nodes using Dagre with correct sizes
|
|
131
|
-
const graph = new Dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({}));
|
|
132
|
-
graph.setGraph({ rankdir: 'LR' });
|
|
133
|
-
const rootNodes = nodes.filter((n) => !n.parentId);
|
|
134
|
-
for (const node of rootNodes) {
|
|
135
|
-
const size = resolvedSizes[node.id] ?? {
|
|
136
|
-
width: MIN_NODE_WIDTH,
|
|
137
|
-
height: MIN_NODE_HEIGHT,
|
|
138
|
-
};
|
|
139
|
-
graph.setNode(node.id, { width: size.width, height: size.height });
|
|
140
|
-
}
|
|
141
|
-
const rootEdges = edges.filter((e) => rootNodes.some((n) => n.id === e.source) &&
|
|
142
|
-
rootNodes.some((n) => n.id === e.target));
|
|
143
|
-
for (const edge of rootEdges) {
|
|
144
|
-
graph.setEdge(edge.source, edge.target);
|
|
145
|
-
}
|
|
146
|
-
Dagre.layout(graph);
|
|
147
|
-
for (const node of rootNodes) {
|
|
148
|
-
const pos = graph.node(node.id);
|
|
149
|
-
if (!pos)
|
|
150
|
-
continue;
|
|
151
|
-
const size = resolvedSizes[node.id] ?? {
|
|
152
|
-
width: MIN_NODE_WIDTH,
|
|
153
|
-
height: MIN_NODE_HEIGHT,
|
|
154
|
-
};
|
|
155
|
-
updatedNodes[node.id] = {
|
|
156
|
-
...updatedNodes[node.id],
|
|
157
|
-
position: {
|
|
158
|
-
x: pos.x - size.width / 2,
|
|
159
|
-
y: pos.y - size.height / 2,
|
|
160
|
-
},
|
|
161
|
-
};
|
|
162
|
-
}
|
|
163
|
-
return topoSort(Object.values(updatedNodes));
|
|
164
|
-
};
|
|
165
|
-
export const updateNodesOnPipelineStateChange = (nodes, pipelineState) => {
|
|
166
|
-
const parsedNodes = [];
|
|
167
|
-
for (const node of nodes) {
|
|
168
|
-
const nodeState = pipelineState.find((state) => state.id.toString() === node.id.toString());
|
|
169
|
-
// Inputs and outputs
|
|
170
|
-
const inputHandles = node.data.handles.input;
|
|
171
|
-
const outputHandles = node.data.handles.output;
|
|
172
|
-
const newInputHandles = [];
|
|
173
|
-
for (const inputHandle of inputHandles) {
|
|
174
|
-
const obj = { ...inputHandle };
|
|
175
|
-
const inputState = nodeState?.inputs[inputHandle.name];
|
|
176
|
-
if (inputState) {
|
|
177
|
-
obj.maxQueueSize = inputState.numQueued;
|
|
178
|
-
obj.fps = inputState.timing.fps;
|
|
179
|
-
obj.dotColor = inputState.dotColor;
|
|
180
|
-
}
|
|
181
|
-
newInputHandles.push(obj);
|
|
182
|
-
}
|
|
183
|
-
const newOutputHandles = [];
|
|
184
|
-
for (const outputHandle of outputHandles) {
|
|
185
|
-
const obj = { ...outputHandle };
|
|
186
|
-
const outputState = nodeState?.outputs[outputHandle.name];
|
|
187
|
-
if (outputState) {
|
|
188
|
-
obj.maxQueueSize = outputState.numQueued;
|
|
189
|
-
obj.fps = outputState.timing.fps;
|
|
190
|
-
obj.dotColor = outputState.dotColor;
|
|
191
|
-
}
|
|
192
|
-
newOutputHandles.push(obj);
|
|
193
|
-
}
|
|
194
|
-
// GPST timings
|
|
195
|
-
const gpst = nodeState?.gpstTimings;
|
|
196
|
-
// Node state
|
|
197
|
-
const state = nodeState?.stateInfo;
|
|
198
|
-
parsedNodes.push({
|
|
199
|
-
...node,
|
|
200
|
-
data: {
|
|
201
|
-
...node.data,
|
|
202
|
-
handles: {
|
|
203
|
-
input: newInputHandles,
|
|
204
|
-
output: newOutputHandles,
|
|
205
|
-
},
|
|
206
|
-
extras: {
|
|
207
|
-
gpstTimings: gpst,
|
|
208
|
-
stateInfo: state,
|
|
209
|
-
},
|
|
210
|
-
},
|
|
211
|
-
});
|
|
212
|
-
}
|
|
213
|
-
return parsedNodes;
|
|
214
|
-
};
|
|
215
|
-
const PipelineCanvasBody = ({ pipeline, pipelineState: pipelineStateParsed, action, ...flexProps }) => {
|
|
8
|
+
import { PipelineLegend } from './PipelineLegend.js';
|
|
9
|
+
const nodeTypes = { generic: PipelineNode };
|
|
10
|
+
const edgeTypes = { generic: BezierEdge, bridge: BridgeEdge };
|
|
11
|
+
const PipelineCanvasBody = ({ pipeline, pipelineState: pipelineStateParsed, header, isDebugging = false, ...flexProps }) => {
|
|
216
12
|
const { fitView, setViewport, getViewport } = useReactFlow();
|
|
217
13
|
const autoArrangeRef = React.useRef(true);
|
|
218
14
|
const widthSelector = (state) => state.width;
|
|
@@ -220,6 +16,7 @@ const PipelineCanvasBody = ({ pipeline, pipelineState: pipelineStateParsed, acti
|
|
|
220
16
|
const reactFlowWidth = useStore(widthSelector);
|
|
221
17
|
const reactFlowHeight = useStore(heightSelector);
|
|
222
18
|
const [shouldFitAndResize, setShouldFitAndResize] = React.useState(false);
|
|
19
|
+
const [openLegend, setOpenLegend] = React.useState(false);
|
|
223
20
|
const pipelineState = React.useMemo(() => pipelineStateParsed, [pipelineStateParsed]);
|
|
224
21
|
// biome-ignore lint/correctness/useExhaustiveDependencies: Intended
|
|
225
22
|
React.useEffect(() => {
|
|
@@ -272,6 +69,6 @@ const PipelineCanvasBody = ({ pipeline, pipelineState: pipelineStateParsed, acti
|
|
|
272
69
|
}
|
|
273
70
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
274
71
|
}, [shouldFitAndResize]);
|
|
275
|
-
return (_jsxs(Flex, { align: "center", justify: "center", full: true, height: "container.xs", rounded: "common", border: "base", ...flexProps, children: [!pipeline && _jsx(Header, { text: "Loading pipeline..." }), pipeline && (
|
|
72
|
+
return (_jsxs(Flex, { align: "center", justify: "center", full: true, height: "container.xs", rounded: "common", border: "base", ...flexProps, children: [!pipeline && _jsx(Header, { text: "Loading pipeline..." }), pipeline && (_jsxs(ReactFlow, { nodes: nodes, edges: edges, onNodeDragStart: () => (autoArrangeRef.current = false), onNodesChange: onNodesChange, fitView: true, nodeTypes: nodeTypes, edgeTypes: edgeTypes, minZoom: 0.4, children: [header && _jsx(Panel, { position: "top-center", children: header }), _jsx(Panel, { position: "top-right", children: _jsxs(Flex, { gap: "xs", flexDirection: "column", children: [_jsx(Button, { onClick: () => setOpenLegend(!openLegend), label: openLegend ? 'Collapse' : 'Legend' }), openLegend && _jsx(PipelineLegend, { isDebugging: isDebugging })] }) })] }))] }));
|
|
276
73
|
};
|
|
277
74
|
export const PipelineCanvas = (props) => (_jsx(ReactFlowProvider, { children: _jsx(PipelineCanvasBody, { ...props }) }));
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { Badge, Flex, Label, SubLabel } from '@luxonis/common-fe-components';
|
|
3
|
+
import React from 'react';
|
|
4
|
+
const debugSections = [
|
|
5
|
+
{
|
|
6
|
+
title: 'Nodes',
|
|
7
|
+
items: [
|
|
8
|
+
{
|
|
9
|
+
0: {
|
|
10
|
+
id: 'device-node',
|
|
11
|
+
label: 'Device Node',
|
|
12
|
+
marker: (_jsx(Badge, { label: "D", variant: "active", style: { padding: '0px', paddingInline: '4px' } })),
|
|
13
|
+
},
|
|
14
|
+
1: {
|
|
15
|
+
id: 'host-node',
|
|
16
|
+
label: 'Host Node',
|
|
17
|
+
marker: (_jsx(Badge, { label: "H", variant: "cyan", style: { padding: '0px', paddingInline: '4px' } })),
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
],
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
title: 'Node States',
|
|
24
|
+
items: [
|
|
25
|
+
{
|
|
26
|
+
0: {
|
|
27
|
+
id: 'node-state-idle',
|
|
28
|
+
label: 'Idle',
|
|
29
|
+
marker: _jsx(SubLabel, { text: "(I)", weight: "medium" }),
|
|
30
|
+
},
|
|
31
|
+
1: {
|
|
32
|
+
id: 'node-state-getting-inputs',
|
|
33
|
+
label: 'Getting inputs',
|
|
34
|
+
marker: _jsx(SubLabel, { text: "(G)", weight: "medium" }),
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
0: {
|
|
39
|
+
id: 'node-state-processing',
|
|
40
|
+
label: 'Processing',
|
|
41
|
+
marker: _jsx(SubLabel, { text: "(P)", weight: "medium" }),
|
|
42
|
+
},
|
|
43
|
+
1: {
|
|
44
|
+
id: 'node-state-sending-outputs',
|
|
45
|
+
label: 'Sending outputs',
|
|
46
|
+
marker: _jsx(SubLabel, { text: "(S)", weight: "medium" }),
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
title: 'GPST Timings',
|
|
53
|
+
items: [
|
|
54
|
+
{
|
|
55
|
+
0: {
|
|
56
|
+
id: 'timing-get',
|
|
57
|
+
label: 'Get time',
|
|
58
|
+
marker: _jsx(SubLabel, { text: "G", weight: "medium" }),
|
|
59
|
+
},
|
|
60
|
+
1: {
|
|
61
|
+
id: 'timing-process',
|
|
62
|
+
label: 'Process time',
|
|
63
|
+
marker: _jsx(SubLabel, { text: "P", weight: "medium" }),
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
0: {
|
|
68
|
+
id: 'timing-send',
|
|
69
|
+
label: 'Send time',
|
|
70
|
+
marker: _jsx(SubLabel, { text: "S", weight: "medium" }),
|
|
71
|
+
},
|
|
72
|
+
1: {
|
|
73
|
+
id: 'timing-total',
|
|
74
|
+
label: 'Total time',
|
|
75
|
+
marker: _jsx(SubLabel, { text: "T", weight: "medium" }),
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
],
|
|
79
|
+
},
|
|
80
|
+
];
|
|
81
|
+
const portStatusSection = [
|
|
82
|
+
{
|
|
83
|
+
title: 'Port Status',
|
|
84
|
+
items: [
|
|
85
|
+
{
|
|
86
|
+
0: {
|
|
87
|
+
id: 'port-inactive',
|
|
88
|
+
label: 'Inactive',
|
|
89
|
+
marker: _jsx(LegendDot, { tone: "gray" }),
|
|
90
|
+
},
|
|
91
|
+
1: {
|
|
92
|
+
id: 'port-ready',
|
|
93
|
+
label: 'Ready',
|
|
94
|
+
marker: _jsx(LegendDot, { tone: "green" }),
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
0: {
|
|
99
|
+
id: 'port-busy',
|
|
100
|
+
label: 'Busy',
|
|
101
|
+
marker: _jsx(LegendDot, { tone: "yellow" }),
|
|
102
|
+
},
|
|
103
|
+
1: {
|
|
104
|
+
id: 'port-blocked',
|
|
105
|
+
label: 'Blocked',
|
|
106
|
+
marker: _jsx(LegendDot, { tone: "red" }),
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
},
|
|
111
|
+
];
|
|
112
|
+
const sharedSections = [
|
|
113
|
+
{
|
|
114
|
+
title: 'Ports',
|
|
115
|
+
items: [
|
|
116
|
+
{
|
|
117
|
+
0: {
|
|
118
|
+
id: 'blocking-port',
|
|
119
|
+
label: 'Blocking',
|
|
120
|
+
marker: _jsx(LegendDot, { tone: "gray", square: true }),
|
|
121
|
+
},
|
|
122
|
+
1: {
|
|
123
|
+
id: 'non-blocking-port',
|
|
124
|
+
label: 'Non-blocking',
|
|
125
|
+
marker: _jsx(LegendDot, { tone: "gray" }),
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
],
|
|
129
|
+
},
|
|
130
|
+
];
|
|
131
|
+
const bridgeSection = {
|
|
132
|
+
title: 'Bridges',
|
|
133
|
+
items: [
|
|
134
|
+
{
|
|
135
|
+
0: {
|
|
136
|
+
id: 'bridge-direction',
|
|
137
|
+
label: 'Direction from source node to target node',
|
|
138
|
+
marker: _jsx(SubLabel, { text: ">" }),
|
|
139
|
+
},
|
|
140
|
+
1: null,
|
|
141
|
+
},
|
|
142
|
+
],
|
|
143
|
+
};
|
|
144
|
+
// NOTE: Colors from hub repo
|
|
145
|
+
// success: '#12B76A',
|
|
146
|
+
// warning: '#DC6803',
|
|
147
|
+
// error: '#F04438',
|
|
148
|
+
// gray: '#667085',
|
|
149
|
+
function LegendDot({ tone, square = false, }) {
|
|
150
|
+
const color = React.useMemo(() => {
|
|
151
|
+
switch (tone) {
|
|
152
|
+
case 'gray':
|
|
153
|
+
return '#667085';
|
|
154
|
+
case 'green':
|
|
155
|
+
return '#12B76A';
|
|
156
|
+
case 'yellow':
|
|
157
|
+
return '#DC6803';
|
|
158
|
+
case 'red':
|
|
159
|
+
return '#F04438';
|
|
160
|
+
}
|
|
161
|
+
}, [tone]);
|
|
162
|
+
return (_jsx("div", { style: {
|
|
163
|
+
width: '8px',
|
|
164
|
+
height: '8px',
|
|
165
|
+
backgroundColor: color,
|
|
166
|
+
border: 'none',
|
|
167
|
+
borderRadius: square ? undefined : '100%',
|
|
168
|
+
} }));
|
|
169
|
+
}
|
|
170
|
+
function LegendCell({ left, right, }) {
|
|
171
|
+
return (_jsxs(Flex, { gap: "xs", width: "full", alignItems: "center", children: [_jsxs(Flex, { alignItems: "center", gap: "xs", width: "full", children: [left.marker, _jsx(SubLabel, { text: `${left.label}` })] }), right ? (_jsxs(Flex, { alignItems: "center", gap: "xs", width: "full", children: [right.marker, _jsx(SubLabel, { text: `${right.label}` })] })) : (_jsx(_Fragment, {}))] }));
|
|
172
|
+
}
|
|
173
|
+
function LegendSection({ title, items }) {
|
|
174
|
+
return (_jsxs(Flex, { direction: "column", gap: "xs", width: "full", children: [_jsx(Label, { text: title, weight: "medium" }), items.map((item) => (_jsx(LegendCell, { left: item[0], right: item[1] }, `${item[0].id}-${item[1]?.id}`)))] }));
|
|
175
|
+
}
|
|
176
|
+
export const PipelineLegend = ({ isDebugging, }) => {
|
|
177
|
+
const sections = isDebugging
|
|
178
|
+
? [...debugSections, ...sharedSections, ...portStatusSection, bridgeSection]
|
|
179
|
+
: sharedSections;
|
|
180
|
+
return (_jsxs(Flex, { direction: "column", gap: "xs", width: "full", padding: "sm", rounded: "common", border: "base", backgroundColor: "white", maxWidth: "250px", children: [_jsx(Label, { text: "Legend", color: "black", weight: "medium", align: "center" }), _jsx(Flex, { direction: "column", gap: "xs", width: "full", children: sections.map((section) => (_jsx(LegendSection, { title: section.title, items: section.items }, section.title))) })] }));
|
|
181
|
+
};
|
|
@@ -51,29 +51,3 @@ export type ParsedNode = Node<{
|
|
|
51
51
|
extras?: NodeExtras;
|
|
52
52
|
}>;
|
|
53
53
|
export declare function parsePipeline(rawPayload: RawPipelinePayload): Pipeline;
|
|
54
|
-
export declare const DOCS_BASE_URL = "https://docs.luxonis.com/software-v3/depthai/depthai-components/nodes";
|
|
55
|
-
export declare const NodesWithLinks: {
|
|
56
|
-
AprilTag: string;
|
|
57
|
-
BenchmarkIn: string;
|
|
58
|
-
BenchmarkOut: string;
|
|
59
|
-
Camera: string;
|
|
60
|
-
DetectionNetwork: string;
|
|
61
|
-
EdgeDetector: string;
|
|
62
|
-
SpatialDetectionNetwork: string;
|
|
63
|
-
FeatureTracker: string;
|
|
64
|
-
ImageAlign: string;
|
|
65
|
-
ImageManip: string;
|
|
66
|
-
IMU: string;
|
|
67
|
-
NeuralNetwork: string;
|
|
68
|
-
ObjectTracker: string;
|
|
69
|
-
RGBD: string;
|
|
70
|
-
Script: string;
|
|
71
|
-
SpatialLocationCalculator: string;
|
|
72
|
-
StereoDepth: string;
|
|
73
|
-
Sync: string;
|
|
74
|
-
SystemLogger: string;
|
|
75
|
-
Thermal: string;
|
|
76
|
-
ToF: string;
|
|
77
|
-
VideoEncoder: string;
|
|
78
|
-
Warp: string;
|
|
79
|
-
};
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { MarkerType, } from '@xyflow/react';
|
|
2
1
|
import { filterNodesHandles, parseHandles, } from './pipeline-handles';
|
|
3
2
|
function addFakeNode(id) {
|
|
4
3
|
return [
|
|
@@ -128,6 +127,7 @@ export function parsePipeline(rawPayload) {
|
|
|
128
127
|
target: connection.node2Id.toString(),
|
|
129
128
|
sourceHandle: connection.node1Output,
|
|
130
129
|
targetHandle: connection.node2Input,
|
|
130
|
+
type: 'generic',
|
|
131
131
|
})) ?? [];
|
|
132
132
|
const bridges = pipeline.bridges?.map((bridge) => ({
|
|
133
133
|
id: `${bridge[0]}-${bridge[1]}-bridge`,
|
|
@@ -136,13 +136,7 @@ export function parsePipeline(rawPayload) {
|
|
|
136
136
|
sourceHandle: 'bottom',
|
|
137
137
|
targetHandle: 'top',
|
|
138
138
|
animated: true,
|
|
139
|
-
|
|
140
|
-
type: MarkerType.ArrowClosed,
|
|
141
|
-
},
|
|
142
|
-
markerEnd: {
|
|
143
|
-
type: MarkerType.ArrowClosed,
|
|
144
|
-
},
|
|
145
|
-
type: 'step',
|
|
139
|
+
type: 'bridge',
|
|
146
140
|
})) ?? [];
|
|
147
141
|
// NOTE: Parent nodes should be rendered before child nodes
|
|
148
142
|
const groupedNodes = [...parentNodes, ...childrenNodes, ...filteredNodes];
|
|
@@ -152,29 +146,3 @@ export function parsePipeline(rawPayload) {
|
|
|
152
146
|
edges: [...edges, ...bridges],
|
|
153
147
|
};
|
|
154
148
|
}
|
|
155
|
-
export const DOCS_BASE_URL = `https://docs.luxonis.com/software-v3/depthai/depthai-components/nodes`;
|
|
156
|
-
export const NodesWithLinks = {
|
|
157
|
-
AprilTag: 'april_tag',
|
|
158
|
-
BenchmarkIn: 'benchmark_in',
|
|
159
|
-
BenchmarkOut: 'benchmark_out',
|
|
160
|
-
Camera: 'camera',
|
|
161
|
-
DetectionNetwork: 'detection_network',
|
|
162
|
-
EdgeDetector: 'edge_detector',
|
|
163
|
-
SpatialDetectionNetwork: 'spatial_detection_network',
|
|
164
|
-
FeatureTracker: 'feature_tracker',
|
|
165
|
-
ImageAlign: 'image_align',
|
|
166
|
-
ImageManip: 'image_manip',
|
|
167
|
-
IMU: 'imu',
|
|
168
|
-
NeuralNetwork: 'neural_network',
|
|
169
|
-
ObjectTracker: 'object_tracker',
|
|
170
|
-
RGBD: 'rgbd',
|
|
171
|
-
Script: 'script',
|
|
172
|
-
SpatialLocationCalculator: 'spatial_location_calculator',
|
|
173
|
-
StereoDepth: 'stereo_depth',
|
|
174
|
-
Sync: 'sync',
|
|
175
|
-
SystemLogger: 'system_logger',
|
|
176
|
-
Thermal: 'thermal',
|
|
177
|
-
ToF: 'tof',
|
|
178
|
-
VideoEncoder: 'video_encoder',
|
|
179
|
-
Warp: 'warp',
|
|
180
|
-
};
|
|
@@ -1,2 +1,38 @@
|
|
|
1
|
+
import { Edge } from '@xyflow/react';
|
|
1
2
|
import { ParsedNode } from './pipeline';
|
|
3
|
+
import { PipelineState } from './pipeline-state';
|
|
2
4
|
export declare const topoSort: (nodes: ParsedNode[]) => ParsedNode[];
|
|
5
|
+
export declare const DOCS_BASE_URL = "https://docs.luxonis.com/software-v3/depthai/depthai-components/nodes";
|
|
6
|
+
export declare const NodesWithLinks: {
|
|
7
|
+
AprilTag: string;
|
|
8
|
+
BenchmarkIn: string;
|
|
9
|
+
BenchmarkOut: string;
|
|
10
|
+
Camera: string;
|
|
11
|
+
DetectionNetwork: string;
|
|
12
|
+
EdgeDetector: string;
|
|
13
|
+
SpatialDetectionNetwork: string;
|
|
14
|
+
FeatureTracker: string;
|
|
15
|
+
ImageAlign: string;
|
|
16
|
+
ImageManip: string;
|
|
17
|
+
IMU: string;
|
|
18
|
+
NeuralNetwork: string;
|
|
19
|
+
ObjectTracker: string;
|
|
20
|
+
RGBD: string;
|
|
21
|
+
Script: string;
|
|
22
|
+
SpatialLocationCalculator: string;
|
|
23
|
+
StereoDepth: string;
|
|
24
|
+
Sync: string;
|
|
25
|
+
SystemLogger: string;
|
|
26
|
+
Thermal: string;
|
|
27
|
+
ToF: string;
|
|
28
|
+
VideoEncoder: string;
|
|
29
|
+
Warp: string;
|
|
30
|
+
};
|
|
31
|
+
export declare const MIN_NODE_WIDTH = 376;
|
|
32
|
+
export declare const MIN_NODE_HEIGHT = 150;
|
|
33
|
+
export declare const getLayoutedElements: (nodes: ParsedNode[], edges: Edge[]) => {
|
|
34
|
+
nodes: any[];
|
|
35
|
+
edges: Edge[];
|
|
36
|
+
};
|
|
37
|
+
export declare const adjustNodes: (nodes: ParsedNode[], edges: Edge[]) => ParsedNode[];
|
|
38
|
+
export declare const updateNodesOnPipelineStateChange: (nodes: ParsedNode[], pipelineState: PipelineState[]) => ParsedNode[];
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import Dagre from '@dagrejs/dagre';
|
|
1
2
|
export const topoSort = (nodes) => {
|
|
2
3
|
const nodeMap = new Map(nodes.map((n) => [n.id, n]));
|
|
3
4
|
const result = [];
|
|
@@ -19,3 +20,236 @@ export const topoSort = (nodes) => {
|
|
|
19
20
|
}
|
|
20
21
|
return result;
|
|
21
22
|
};
|
|
23
|
+
export const DOCS_BASE_URL = `https://docs.luxonis.com/software-v3/depthai/depthai-components/nodes`;
|
|
24
|
+
export const NodesWithLinks = {
|
|
25
|
+
AprilTag: 'april_tag',
|
|
26
|
+
BenchmarkIn: 'benchmark_in',
|
|
27
|
+
BenchmarkOut: 'benchmark_out',
|
|
28
|
+
Camera: 'camera',
|
|
29
|
+
DetectionNetwork: 'detection_network',
|
|
30
|
+
EdgeDetector: 'edge_detector',
|
|
31
|
+
SpatialDetectionNetwork: 'spatial_detection_network',
|
|
32
|
+
FeatureTracker: 'feature_tracker',
|
|
33
|
+
ImageAlign: 'image_align',
|
|
34
|
+
ImageManip: 'image_manip',
|
|
35
|
+
IMU: 'imu',
|
|
36
|
+
NeuralNetwork: 'neural_network',
|
|
37
|
+
ObjectTracker: 'object_tracker',
|
|
38
|
+
RGBD: 'rgbd',
|
|
39
|
+
Script: 'script',
|
|
40
|
+
SpatialLocationCalculator: 'spatial_location_calculator',
|
|
41
|
+
StereoDepth: 'stereo_depth',
|
|
42
|
+
Sync: 'sync',
|
|
43
|
+
SystemLogger: 'system_logger',
|
|
44
|
+
Thermal: 'thermal',
|
|
45
|
+
ToF: 'tof',
|
|
46
|
+
VideoEncoder: 'video_encoder',
|
|
47
|
+
Warp: 'warp',
|
|
48
|
+
};
|
|
49
|
+
export const MIN_NODE_WIDTH = 376;
|
|
50
|
+
export const MIN_NODE_HEIGHT = 150;
|
|
51
|
+
export const getLayoutedElements = (nodes, edges) => {
|
|
52
|
+
const graph = new Dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({}));
|
|
53
|
+
graph.setGraph({ rankdir: 'LR' });
|
|
54
|
+
for (const edge of edges) {
|
|
55
|
+
graph.setEdge(edge.source, edge.target);
|
|
56
|
+
}
|
|
57
|
+
for (const node of nodes) {
|
|
58
|
+
graph.setNode(node.id, {
|
|
59
|
+
...node,
|
|
60
|
+
width: MIN_NODE_WIDTH,
|
|
61
|
+
height: MIN_NODE_HEIGHT,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
Dagre.layout(graph);
|
|
65
|
+
return {
|
|
66
|
+
nodes: nodes.map((node) => {
|
|
67
|
+
const position = graph.node(node.id);
|
|
68
|
+
const x = position.x - MIN_NODE_WIDTH / 2;
|
|
69
|
+
const y = position.y - MIN_NODE_HEIGHT / 2;
|
|
70
|
+
return { ...node, position: { x, y } };
|
|
71
|
+
}),
|
|
72
|
+
edges,
|
|
73
|
+
};
|
|
74
|
+
};
|
|
75
|
+
export const adjustNodes = (nodes, edges) => {
|
|
76
|
+
const PADDING = 16;
|
|
77
|
+
const HEADER_HEIGHT = 24;
|
|
78
|
+
// Build a tree: find all group nodes and their depth
|
|
79
|
+
const groupNodes = nodes.filter((node) => node.type === 'group');
|
|
80
|
+
const childNodes = nodes.filter((node) => node.type !== 'group');
|
|
81
|
+
// Sort groups by depth (deepest first) so children are resolved before parents
|
|
82
|
+
const getDepth = (nodeId, depth = 0) => {
|
|
83
|
+
const node = nodes.find((n) => n.id === nodeId);
|
|
84
|
+
if (!node?.parentId)
|
|
85
|
+
return depth;
|
|
86
|
+
return getDepth(node.parentId, depth + 1);
|
|
87
|
+
};
|
|
88
|
+
const sortedGroups = [...groupNodes].sort((a, b) => getDepth(b.id) - getDepth(a.id));
|
|
89
|
+
// Track resolved sizes for groups as we process them
|
|
90
|
+
const resolvedSizes = {};
|
|
91
|
+
// Start with all non-group nodes at their measured sizes
|
|
92
|
+
for (const node of childNodes) {
|
|
93
|
+
resolvedSizes[node.id] = {
|
|
94
|
+
width: Math.min(node.measured?.width ?? MIN_NODE_WIDTH),
|
|
95
|
+
height: Math.min(node.measured?.height ?? MIN_NODE_HEIGHT),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
const updatedNodes = {};
|
|
99
|
+
for (const node of nodes)
|
|
100
|
+
updatedNodes[node.id] = { ...node };
|
|
101
|
+
// Process groups deepest-first
|
|
102
|
+
for (const group of sortedGroups) {
|
|
103
|
+
const children = nodes.filter((n) => n.parentId === group.id);
|
|
104
|
+
if (children.length === 0) {
|
|
105
|
+
resolvedSizes[group.id] = {
|
|
106
|
+
width: MIN_NODE_WIDTH,
|
|
107
|
+
height: MIN_NODE_HEIGHT,
|
|
108
|
+
};
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
const g = new Dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({}));
|
|
112
|
+
g.setGraph({ rankdir: 'LR' });
|
|
113
|
+
for (const child of children) {
|
|
114
|
+
// Use resolved size (important for nested groups!)
|
|
115
|
+
const size = resolvedSizes[child.id] ?? {
|
|
116
|
+
width: MIN_NODE_WIDTH,
|
|
117
|
+
height: MIN_NODE_HEIGHT,
|
|
118
|
+
};
|
|
119
|
+
g.setNode(child.id, { width: size.width, height: size.height });
|
|
120
|
+
}
|
|
121
|
+
const groupEdges = edges.filter((e) => children.some((c) => c.id === e.source) &&
|
|
122
|
+
children.some((c) => c.id === e.target));
|
|
123
|
+
for (const edge of groupEdges) {
|
|
124
|
+
g.setEdge(edge.source, edge.target);
|
|
125
|
+
}
|
|
126
|
+
Dagre.layout(g);
|
|
127
|
+
let minX = 9999;
|
|
128
|
+
let minY = 9999;
|
|
129
|
+
let maxX = -9999;
|
|
130
|
+
let maxY = -9999;
|
|
131
|
+
// Replace both child loops with this single pass:
|
|
132
|
+
for (const child of children) {
|
|
133
|
+
const pos = g.node(child.id);
|
|
134
|
+
const size = resolvedSizes[child.id] ?? {
|
|
135
|
+
width: MIN_NODE_WIDTH,
|
|
136
|
+
height: MIN_NODE_HEIGHT,
|
|
137
|
+
};
|
|
138
|
+
const x = pos.x - size.width / 2;
|
|
139
|
+
const y = pos.y - size.height / 2;
|
|
140
|
+
minX = Math.min(minX, x);
|
|
141
|
+
minY = Math.min(minY, y);
|
|
142
|
+
maxX = Math.max(maxX, x + size.width);
|
|
143
|
+
maxY = Math.max(maxY, y + size.height);
|
|
144
|
+
}
|
|
145
|
+
// Now positions are correct, apply them
|
|
146
|
+
for (const child of children) {
|
|
147
|
+
const pos = g.node(child.id);
|
|
148
|
+
const size = resolvedSizes[child.id] ?? {
|
|
149
|
+
width: MIN_NODE_WIDTH,
|
|
150
|
+
height: MIN_NODE_HEIGHT,
|
|
151
|
+
};
|
|
152
|
+
updatedNodes[child.id] = {
|
|
153
|
+
...updatedNodes[child.id],
|
|
154
|
+
position: {
|
|
155
|
+
x: pos.x - size.width / 2 - minX + PADDING,
|
|
156
|
+
y: pos.y - size.height / 2 - minY + PADDING + HEADER_HEIGHT,
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
const groupW = maxX - minX + PADDING * 2;
|
|
161
|
+
const groupH = maxY - minY + PADDING * 2 + HEADER_HEIGHT;
|
|
162
|
+
// Store this group's resolved size for its own parent to use
|
|
163
|
+
resolvedSizes[group.id] = { width: groupW, height: groupH };
|
|
164
|
+
updatedNodes[group.id] = {
|
|
165
|
+
...updatedNodes[group.id],
|
|
166
|
+
style: { ...updatedNodes[group.id].style, width: groupW, height: groupH },
|
|
167
|
+
width: groupW,
|
|
168
|
+
height: groupH,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
// Now lay out the top-level (root) nodes using Dagre with correct sizes
|
|
172
|
+
const graph = new Dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({}));
|
|
173
|
+
graph.setGraph({ rankdir: 'LR' });
|
|
174
|
+
const rootNodes = nodes.filter((n) => !n.parentId);
|
|
175
|
+
for (const node of rootNodes) {
|
|
176
|
+
const size = resolvedSizes[node.id] ?? {
|
|
177
|
+
width: MIN_NODE_WIDTH,
|
|
178
|
+
height: MIN_NODE_HEIGHT,
|
|
179
|
+
};
|
|
180
|
+
graph.setNode(node.id, { width: size.width, height: size.height });
|
|
181
|
+
}
|
|
182
|
+
const rootEdges = edges.filter((e) => rootNodes.some((n) => n.id === e.source) &&
|
|
183
|
+
rootNodes.some((n) => n.id === e.target));
|
|
184
|
+
for (const edge of rootEdges) {
|
|
185
|
+
graph.setEdge(edge.source, edge.target);
|
|
186
|
+
}
|
|
187
|
+
Dagre.layout(graph);
|
|
188
|
+
for (const node of rootNodes) {
|
|
189
|
+
const pos = graph.node(node.id);
|
|
190
|
+
if (!pos)
|
|
191
|
+
continue;
|
|
192
|
+
const size = resolvedSizes[node.id] ?? {
|
|
193
|
+
width: MIN_NODE_WIDTH,
|
|
194
|
+
height: MIN_NODE_HEIGHT,
|
|
195
|
+
};
|
|
196
|
+
updatedNodes[node.id] = {
|
|
197
|
+
...updatedNodes[node.id],
|
|
198
|
+
position: {
|
|
199
|
+
x: pos.x - size.width / 2,
|
|
200
|
+
y: pos.y - size.height / 2,
|
|
201
|
+
},
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
return topoSort(Object.values(updatedNodes));
|
|
205
|
+
};
|
|
206
|
+
export const updateNodesOnPipelineStateChange = (nodes, pipelineState) => {
|
|
207
|
+
const parsedNodes = [];
|
|
208
|
+
for (const node of nodes) {
|
|
209
|
+
const nodeState = pipelineState.find((state) => state.id.toString() === node.id.toString());
|
|
210
|
+
// Inputs and outputs
|
|
211
|
+
const inputHandles = node.data.handles.input;
|
|
212
|
+
const outputHandles = node.data.handles.output;
|
|
213
|
+
const newInputHandles = [];
|
|
214
|
+
for (const inputHandle of inputHandles) {
|
|
215
|
+
const obj = { ...inputHandle };
|
|
216
|
+
const inputState = nodeState?.inputs[inputHandle.name];
|
|
217
|
+
if (inputState) {
|
|
218
|
+
obj.maxQueueSize = inputState.numQueued;
|
|
219
|
+
obj.fps = inputState.timing.fps;
|
|
220
|
+
obj.dotColor = inputState.dotColor;
|
|
221
|
+
}
|
|
222
|
+
newInputHandles.push(obj);
|
|
223
|
+
}
|
|
224
|
+
const newOutputHandles = [];
|
|
225
|
+
for (const outputHandle of outputHandles) {
|
|
226
|
+
const obj = { ...outputHandle };
|
|
227
|
+
const outputState = nodeState?.outputs[outputHandle.name];
|
|
228
|
+
if (outputState) {
|
|
229
|
+
obj.maxQueueSize = outputState.numQueued;
|
|
230
|
+
obj.fps = outputState.timing.fps;
|
|
231
|
+
obj.dotColor = outputState.dotColor;
|
|
232
|
+
}
|
|
233
|
+
newOutputHandles.push(obj);
|
|
234
|
+
}
|
|
235
|
+
// GPST timings
|
|
236
|
+
const gpst = nodeState?.gpstTimings;
|
|
237
|
+
// Node state
|
|
238
|
+
const state = nodeState?.stateInfo;
|
|
239
|
+
parsedNodes.push({
|
|
240
|
+
...node,
|
|
241
|
+
data: {
|
|
242
|
+
...node.data,
|
|
243
|
+
handles: {
|
|
244
|
+
input: newInputHandles,
|
|
245
|
+
output: newOutputHandles,
|
|
246
|
+
},
|
|
247
|
+
extras: {
|
|
248
|
+
gpstTimings: gpst,
|
|
249
|
+
stateInfo: state,
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
return parsedNodes;
|
|
255
|
+
};
|