@luxonis/depthai-pipeline-lib 1.12.0 → 1.13.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 +22 -6
- package/dist/src/components/Node.js +35 -7
- package/dist/src/components/PipelineCanvas.d.ts +2 -0
- package/dist/src/components/PipelineCanvas.js +62 -74
- package/dist/src/services/pipeline-handles.d.ts +6 -0
- package/dist/src/services/pipeline-state.d.ts +25 -2
- package/dist/src/services/pipeline-state.js +83 -2
- package/dist/src/services/pipeline.d.ts +4 -0
- package/dist/src/services/pipeline.js +5 -0
- package/dist/src/services/utils.d.ts +2 -0
- package/dist/src/services/utils.js +21 -0
- package/package.json +1 -1
package/dist/panda.css
CHANGED
|
@@ -1297,12 +1297,16 @@
|
|
|
1297
1297
|
border-top-right-radius: var(--radii-common);
|
|
1298
1298
|
}
|
|
1299
1299
|
|
|
1300
|
-
.
|
|
1301
|
-
width:
|
|
1300
|
+
.w_20px {
|
|
1301
|
+
width: 20px;
|
|
1302
1302
|
}
|
|
1303
1303
|
|
|
1304
|
-
.
|
|
1305
|
-
height:
|
|
1304
|
+
.h_20px {
|
|
1305
|
+
height: 20px;
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
.d_flex {
|
|
1309
|
+
display: flex;
|
|
1306
1310
|
}
|
|
1307
1311
|
|
|
1308
1312
|
.w_auto {
|
|
@@ -1321,14 +1325,22 @@
|
|
|
1321
1325
|
background-color: rgba(0,0,0,0.125);
|
|
1322
1326
|
}
|
|
1323
1327
|
|
|
1324
|
-
.bg-c_dark\.
|
|
1325
|
-
background-color: var(--colors-dark-
|
|
1328
|
+
.bg-c_dark\.warning\! {
|
|
1329
|
+
background-color: var(--colors-dark-warning) !important;
|
|
1326
1330
|
}
|
|
1327
1331
|
|
|
1328
1332
|
.bg-c_dark\.success\! {
|
|
1329
1333
|
background-color: var(--colors-dark-success) !important;
|
|
1330
1334
|
}
|
|
1331
1335
|
|
|
1336
|
+
.bg-c_dark\.error\! {
|
|
1337
|
+
background-color: var(--colors-dark-error) !important;
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
.bg-c_dark\.gray\! {
|
|
1341
|
+
background-color: var(--colors-dark-gray) !important;
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1332
1344
|
.ml_xs {
|
|
1333
1345
|
margin-left: var(--spacing-xs);
|
|
1334
1346
|
}
|
|
@@ -1345,6 +1357,10 @@
|
|
|
1345
1357
|
background-color: var(--colors-light-gray);
|
|
1346
1358
|
}
|
|
1347
1359
|
|
|
1360
|
+
.ai_center {
|
|
1361
|
+
align-items: center;
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1348
1364
|
.right_xs {
|
|
1349
1365
|
right: var(--spacing-xs);
|
|
1350
1366
|
}
|
|
@@ -1,19 +1,31 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { Button, Flex, HelpIcon, Label, SubLabel, } from '@luxonis/common-fe-components';
|
|
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
5
|
import { DOCS_BASE_URL, NodesWithLinks, } from '../services/pipeline.js';
|
|
6
|
+
import { formatTiming } from '../services/pipeline-state.js';
|
|
6
7
|
import { css } from '../styled-system/css/css.mjs';
|
|
8
|
+
import { MIN_NODE_WIDTH } from './PipelineCanvas.js';
|
|
7
9
|
const NodeHandles = (props) => {
|
|
8
10
|
const { handles, type } = props;
|
|
9
|
-
return (_jsx(Flex, { full: true, direction: "column", align: type === 'input' ? 'start' : 'end', children: handles.map(({ type: handleType, id, blocking, queueSize, name, maxQueueSize, fps, connected, }) => {
|
|
11
|
+
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, }) => {
|
|
10
12
|
if (!connected) {
|
|
11
13
|
return;
|
|
12
14
|
}
|
|
13
15
|
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({
|
|
14
16
|
width: 'custom.handle.dot!',
|
|
15
17
|
height: 'custom.handle.dot!',
|
|
16
|
-
backgroundColor: blocking
|
|
18
|
+
backgroundColor: blocking
|
|
19
|
+
? 'dark.error!'
|
|
20
|
+
: dotColor === 'gray'
|
|
21
|
+
? 'dark.gray!'
|
|
22
|
+
: dotColor === 'green'
|
|
23
|
+
? 'dark.success!'
|
|
24
|
+
: dotColor === 'red'
|
|
25
|
+
? 'dark.error!'
|
|
26
|
+
: dotColor === 'yellow'
|
|
27
|
+
? 'dark.warning!'
|
|
28
|
+
: 'dark.success!',
|
|
17
29
|
border: 'none!',
|
|
18
30
|
borderRadius: blocking ? '0% !important' : '100% !important',
|
|
19
31
|
}) }), _jsx(NodeHandlesSubLabel, { type: type, name: name, queueSize: queueSize, maxQueueSize: maxQueueSize, fps: fps })] }, id));
|
|
@@ -29,6 +41,9 @@ const NodeHandlesSubLabel = (props) => {
|
|
|
29
41
|
? `[${fps ? `${fps.toFixed(1)} | ` : ''}${`${maxQueueSize ?? 0}/${queueSize}`}] ${name}`
|
|
30
42
|
: `${fps ? `[${fps.toFixed(1)}]` : ''} ${name}`, break: "none" }));
|
|
31
43
|
};
|
|
44
|
+
const GPSTTimings = (props) => {
|
|
45
|
+
return (_jsx(Flex, { flexDirection: "column", alignItems: "center", minWidth: "120px", children: Object.entries(props).map(([key, item]) => (_jsxs(Flex, { gap: "xxs", children: [_jsx(Label, { text: `${key}:`, size: "small" }), _jsx(Label, { text: `${formatTiming(item)}ms`, size: "small" })] }, `${key}-${item}-label`))) }));
|
|
46
|
+
};
|
|
32
47
|
export const PipelineNode = (props) => {
|
|
33
48
|
const { data: node } = props;
|
|
34
49
|
// biome-ignore lint/suspicious/noPrototypeBuiltins: Intended
|
|
@@ -48,9 +63,22 @@ export const PipelineNode = (props) => {
|
|
|
48
63
|
backgroundColor: 'light.gray',
|
|
49
64
|
roundedTop: 'common',
|
|
50
65
|
}), children: [_jsx("span", { className: css({
|
|
51
|
-
width: '
|
|
52
|
-
height: '
|
|
53
|
-
|
|
66
|
+
width: '20px',
|
|
67
|
+
height: '20px',
|
|
68
|
+
display: 'flex',
|
|
69
|
+
alignItems: 'center',
|
|
70
|
+
}), children: (node.nodeType === 'device' || node.nodeType === 'host') && (_jsx(Badge, { label: node.nodeType === 'device' ? 'D' : 'H', variant: node.nodeType === 'device' ? 'active' : 'cyan', style: {
|
|
71
|
+
padding: '0px',
|
|
72
|
+
display: 'flex',
|
|
73
|
+
justifyContent: 'center',
|
|
74
|
+
alignItems: 'center',
|
|
75
|
+
width: '20px',
|
|
76
|
+
height: '20px',
|
|
77
|
+
}, tooltip: node.nodeType === 'device' ? 'Device Node' : 'Host Node', cursor: "help" })) }), _jsx(Label, { text: node.id
|
|
78
|
+
? `${node.name} (${node.id}) ${node.extras?.stateInfo ? `(${node.extras.stateInfo.letter})` : ''}`
|
|
79
|
+
: node.extras?.stateInfo
|
|
80
|
+
? `${node.name} (${node.extras.stateInfo.letter})`
|
|
81
|
+
: node.name, color: "unset" }), _jsx(Button, { variant: "ghost", color: "transparent", icon: HelpIcon, onClick: () => window.open(link, '_blank'), className: clsx('node-help-icon', css({
|
|
54
82
|
width: 'auto',
|
|
55
83
|
height: 'auto',
|
|
56
84
|
right: 'xs',
|
|
@@ -60,7 +88,7 @@ export const PipelineNode = (props) => {
|
|
|
60
88
|
}), style: {
|
|
61
89
|
border: 'none',
|
|
62
90
|
background: 'transparent',
|
|
63
|
-
} }), _jsxs(Flex, { gap: "sm", paddingY: "xs", children: [_jsx(NodeHandles, { type: "input", handles: node.handles.input }), _jsx(NodeHandles, { type: "output", handles: node.handles.output })] }), _jsx(Handle, { type: "source", position: Position.Bottom, id: "bottom", isConnectable: false, className: css({
|
|
91
|
+
} }), _jsxs(Flex, { gap: "sm", paddingY: "xs", minWidth: `${MIN_NODE_WIDTH}px`, children: [_jsx(NodeHandles, { type: "input", handles: node.handles.input }), node.extras && node.extras.gpstTimings && (_jsx(GPSTTimings, { ...node.extras.gpstTimings })), _jsx(NodeHandles, { type: "output", handles: node.handles.output })] }), _jsx(Handle, { type: "source", position: Position.Bottom, id: "bottom", isConnectable: false, className: css({
|
|
64
92
|
width: 'custom.handle.dot!',
|
|
65
93
|
height: 'custom.handle.dot!',
|
|
66
94
|
}), style: {
|
|
@@ -7,5 +7,7 @@ export type PipelineCanvasProps = FlexProps & {
|
|
|
7
7
|
pipelineState: PipelineState[] | null;
|
|
8
8
|
action?: React.ReactNode;
|
|
9
9
|
};
|
|
10
|
+
export declare const MIN_NODE_WIDTH = 376;
|
|
11
|
+
export declare const MIN_NODE_HEIGHT = 150;
|
|
10
12
|
export declare const updateNodesOnPipelineStateChange: (nodes: ParsedNode[], pipelineState: PipelineState[]) => ParsedNode[];
|
|
11
13
|
export declare const PipelineCanvas: React.FC<PipelineCanvasProps>;
|
|
@@ -3,39 +3,37 @@ import Dagre from '@dagrejs/dagre';
|
|
|
3
3
|
import { Flex, Header } from '@luxonis/common-fe-components';
|
|
4
4
|
import { Panel, ReactFlow, ReactFlowProvider, useEdgesState, useNodesState, useReactFlow, useStore, } from '@xyflow/react';
|
|
5
5
|
import React from 'react';
|
|
6
|
+
import { topoSort } from '../services/utils.js';
|
|
6
7
|
import { PipelineNode } from './Node.js';
|
|
8
|
+
export const MIN_NODE_WIDTH = 376;
|
|
9
|
+
export const MIN_NODE_HEIGHT = 150;
|
|
7
10
|
const getLayoutedElements = (nodes, edges) => {
|
|
8
11
|
const graph = new Dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({}));
|
|
9
|
-
graph.setGraph({ rankdir: 'LR'
|
|
10
|
-
const childNodes = nodes.filter((node) => node.type !== 'group');
|
|
12
|
+
graph.setGraph({ rankdir: 'LR' });
|
|
11
13
|
for (const edge of edges) {
|
|
12
14
|
graph.setEdge(edge.source, edge.target);
|
|
13
15
|
}
|
|
14
|
-
for (const node of
|
|
16
|
+
for (const node of nodes) {
|
|
15
17
|
graph.setNode(node.id, {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
+
...node,
|
|
19
|
+
width: MIN_NODE_WIDTH,
|
|
20
|
+
height: MIN_NODE_HEIGHT,
|
|
18
21
|
});
|
|
19
22
|
}
|
|
20
23
|
Dagre.layout(graph);
|
|
21
24
|
return {
|
|
22
25
|
nodes: nodes.map((node) => {
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
return {
|
|
27
|
-
...node,
|
|
28
|
-
position: {
|
|
29
|
-
x: pos.x - (node.measured?.width ?? 300) / 2,
|
|
30
|
-
y: pos.y - (node.measured?.height ?? 150) / 2,
|
|
31
|
-
},
|
|
32
|
-
};
|
|
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 } };
|
|
33
30
|
}),
|
|
34
31
|
edges,
|
|
35
32
|
};
|
|
36
33
|
};
|
|
37
34
|
const adjustNodes = (nodes, edges) => {
|
|
38
35
|
const PADDING = 16;
|
|
36
|
+
const HEADER_HEIGHT = 24;
|
|
39
37
|
// Build a tree: find all group nodes and their depth
|
|
40
38
|
const groupNodes = nodes.filter((node) => node.type === 'group');
|
|
41
39
|
const childNodes = nodes.filter((node) => node.type !== 'group');
|
|
@@ -49,12 +47,11 @@ const adjustNodes = (nodes, edges) => {
|
|
|
49
47
|
const sortedGroups = [...groupNodes].sort((a, b) => getDepth(b.id) - getDepth(a.id));
|
|
50
48
|
// Track resolved sizes for groups as we process them
|
|
51
49
|
const resolvedSizes = {};
|
|
52
|
-
const resolvedPositions = {};
|
|
53
50
|
// Start with all non-group nodes at their measured sizes
|
|
54
51
|
for (const node of childNodes) {
|
|
55
52
|
resolvedSizes[node.id] = {
|
|
56
|
-
width: node.measured?.width ??
|
|
57
|
-
height: node.measured?.height ??
|
|
53
|
+
width: Math.min(node.measured?.width ?? MIN_NODE_WIDTH),
|
|
54
|
+
height: Math.min(node.measured?.height ?? MIN_NODE_HEIGHT),
|
|
58
55
|
};
|
|
59
56
|
}
|
|
60
57
|
const updatedNodes = {};
|
|
@@ -64,14 +61,20 @@ const adjustNodes = (nodes, edges) => {
|
|
|
64
61
|
for (const group of sortedGroups) {
|
|
65
62
|
const children = nodes.filter((n) => n.parentId === group.id);
|
|
66
63
|
if (children.length === 0) {
|
|
67
|
-
resolvedSizes[group.id] = {
|
|
64
|
+
resolvedSizes[group.id] = {
|
|
65
|
+
width: MIN_NODE_WIDTH,
|
|
66
|
+
height: MIN_NODE_HEIGHT,
|
|
67
|
+
};
|
|
68
68
|
continue;
|
|
69
69
|
}
|
|
70
70
|
const g = new Dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({}));
|
|
71
|
-
g.setGraph({ rankdir: 'LR'
|
|
71
|
+
g.setGraph({ rankdir: 'LR' });
|
|
72
72
|
for (const child of children) {
|
|
73
73
|
// Use resolved size (important for nested groups!)
|
|
74
|
-
const size = resolvedSizes[child.id] ?? {
|
|
74
|
+
const size = resolvedSizes[child.id] ?? {
|
|
75
|
+
width: MIN_NODE_WIDTH,
|
|
76
|
+
height: MIN_NODE_HEIGHT,
|
|
77
|
+
};
|
|
75
78
|
g.setNode(child.id, { width: size.width, height: size.height });
|
|
76
79
|
}
|
|
77
80
|
const groupEdges = edges.filter((e) => children.some((c) => c.id === e.source) &&
|
|
@@ -84,39 +87,37 @@ const adjustNodes = (nodes, edges) => {
|
|
|
84
87
|
let minY = 9999;
|
|
85
88
|
let maxX = -9999;
|
|
86
89
|
let maxY = -9999;
|
|
90
|
+
// Replace both child loops with this single pass:
|
|
87
91
|
for (const child of children) {
|
|
88
92
|
const pos = g.node(child.id);
|
|
89
|
-
const size = resolvedSizes[child.id] ?? {
|
|
93
|
+
const size = resolvedSizes[child.id] ?? {
|
|
94
|
+
width: MIN_NODE_WIDTH,
|
|
95
|
+
height: MIN_NODE_HEIGHT,
|
|
96
|
+
};
|
|
90
97
|
const x = pos.x - size.width / 2;
|
|
91
98
|
const y = pos.y - size.height / 2;
|
|
92
99
|
minX = Math.min(minX, x);
|
|
93
100
|
minY = Math.min(minY, y);
|
|
94
101
|
maxX = Math.max(maxX, x + size.width);
|
|
95
102
|
maxY = Math.max(maxY, y + size.height);
|
|
96
|
-
// Store relative position within parent
|
|
97
|
-
resolvedPositions[child.id] = {
|
|
98
|
-
x: x - minX + PADDING, // will be adjusted after minX is finalized
|
|
99
|
-
y: y - minY + PADDING,
|
|
100
|
-
};
|
|
101
|
-
updatedNodes[child.id] = {
|
|
102
|
-
...updatedNodes[child.id],
|
|
103
|
-
position: { x: x - minX + PADDING, y: y - minY + PADDING },
|
|
104
|
-
};
|
|
105
103
|
}
|
|
106
|
-
//
|
|
104
|
+
// Now positions are correct, apply them
|
|
107
105
|
for (const child of children) {
|
|
108
106
|
const pos = g.node(child.id);
|
|
109
|
-
const size = resolvedSizes[child.id] ?? {
|
|
107
|
+
const size = resolvedSizes[child.id] ?? {
|
|
108
|
+
width: MIN_NODE_WIDTH,
|
|
109
|
+
height: MIN_NODE_HEIGHT,
|
|
110
|
+
};
|
|
110
111
|
updatedNodes[child.id] = {
|
|
111
112
|
...updatedNodes[child.id],
|
|
112
113
|
position: {
|
|
113
114
|
x: pos.x - size.width / 2 - minX + PADDING,
|
|
114
|
-
y: pos.y - size.height / 2 - minY + PADDING,
|
|
115
|
+
y: pos.y - size.height / 2 - minY + PADDING + HEADER_HEIGHT,
|
|
115
116
|
},
|
|
116
117
|
};
|
|
117
118
|
}
|
|
118
119
|
const groupW = maxX - minX + PADDING * 2;
|
|
119
|
-
const groupH = maxY - minY + PADDING * 2;
|
|
120
|
+
const groupH = maxY - minY + PADDING * 2 + HEADER_HEIGHT;
|
|
120
121
|
// Store this group's resolved size for its own parent to use
|
|
121
122
|
resolvedSizes[group.id] = { width: groupW, height: groupH };
|
|
122
123
|
updatedNodes[group.id] = {
|
|
@@ -128,10 +129,13 @@ const adjustNodes = (nodes, edges) => {
|
|
|
128
129
|
}
|
|
129
130
|
// Now lay out the top-level (root) nodes using Dagre with correct sizes
|
|
130
131
|
const graph = new Dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({}));
|
|
131
|
-
graph.setGraph({ rankdir: 'LR'
|
|
132
|
+
graph.setGraph({ rankdir: 'LR' });
|
|
132
133
|
const rootNodes = nodes.filter((n) => !n.parentId);
|
|
133
134
|
for (const node of rootNodes) {
|
|
134
|
-
const size = resolvedSizes[node.id] ?? {
|
|
135
|
+
const size = resolvedSizes[node.id] ?? {
|
|
136
|
+
width: MIN_NODE_WIDTH,
|
|
137
|
+
height: MIN_NODE_HEIGHT,
|
|
138
|
+
};
|
|
135
139
|
graph.setNode(node.id, { width: size.width, height: size.height });
|
|
136
140
|
}
|
|
137
141
|
const rootEdges = edges.filter((e) => rootNodes.some((n) => n.id === e.source) &&
|
|
@@ -144,7 +148,10 @@ const adjustNodes = (nodes, edges) => {
|
|
|
144
148
|
const pos = graph.node(node.id);
|
|
145
149
|
if (!pos)
|
|
146
150
|
continue;
|
|
147
|
-
const size = resolvedSizes[node.id] ?? {
|
|
151
|
+
const size = resolvedSizes[node.id] ?? {
|
|
152
|
+
width: MIN_NODE_WIDTH,
|
|
153
|
+
height: MIN_NODE_HEIGHT,
|
|
154
|
+
};
|
|
148
155
|
updatedNodes[node.id] = {
|
|
149
156
|
...updatedNodes[node.id],
|
|
150
157
|
position: {
|
|
@@ -153,33 +160,13 @@ const adjustNodes = (nodes, edges) => {
|
|
|
153
160
|
},
|
|
154
161
|
};
|
|
155
162
|
}
|
|
156
|
-
const topoSort = (nodes) => {
|
|
157
|
-
const nodeMap = new Map(nodes.map((n) => [n.id, n]));
|
|
158
|
-
const result = [];
|
|
159
|
-
const visited = new Set();
|
|
160
|
-
const visit = (id) => {
|
|
161
|
-
if (visited.has(id))
|
|
162
|
-
return;
|
|
163
|
-
visited.add(id);
|
|
164
|
-
const node = nodeMap.get(id);
|
|
165
|
-
if (!node)
|
|
166
|
-
return;
|
|
167
|
-
// Visit parent first
|
|
168
|
-
if (node.parentId)
|
|
169
|
-
visit(node.parentId);
|
|
170
|
-
result.push(node);
|
|
171
|
-
};
|
|
172
|
-
for (const node of nodes) {
|
|
173
|
-
visit(node.id);
|
|
174
|
-
}
|
|
175
|
-
return result;
|
|
176
|
-
};
|
|
177
163
|
return topoSort(Object.values(updatedNodes));
|
|
178
164
|
};
|
|
179
165
|
export const updateNodesOnPipelineStateChange = (nodes, pipelineState) => {
|
|
180
166
|
const parsedNodes = [];
|
|
181
167
|
for (const node of nodes) {
|
|
182
168
|
const nodeState = pipelineState.find((state) => state.id.toString() === node.id.toString());
|
|
169
|
+
// Inputs and outputs
|
|
183
170
|
const inputHandles = node.data.handles.input;
|
|
184
171
|
const outputHandles = node.data.handles.output;
|
|
185
172
|
const newInputHandles = [];
|
|
@@ -189,6 +176,7 @@ export const updateNodesOnPipelineStateChange = (nodes, pipelineState) => {
|
|
|
189
176
|
if (inputState) {
|
|
190
177
|
obj.maxQueueSize = inputState.numQueued;
|
|
191
178
|
obj.fps = inputState.timing.fps;
|
|
179
|
+
obj.dotColor = inputState.dotColor;
|
|
192
180
|
}
|
|
193
181
|
newInputHandles.push(obj);
|
|
194
182
|
}
|
|
@@ -199,9 +187,14 @@ export const updateNodesOnPipelineStateChange = (nodes, pipelineState) => {
|
|
|
199
187
|
if (outputState) {
|
|
200
188
|
obj.maxQueueSize = outputState.numQueued;
|
|
201
189
|
obj.fps = outputState.timing.fps;
|
|
190
|
+
obj.dotColor = outputState.dotColor;
|
|
202
191
|
}
|
|
203
192
|
newOutputHandles.push(obj);
|
|
204
193
|
}
|
|
194
|
+
// GPST timings
|
|
195
|
+
const gpst = nodeState?.gpstTimings;
|
|
196
|
+
// Node state
|
|
197
|
+
const state = nodeState?.stateInfo;
|
|
205
198
|
parsedNodes.push({
|
|
206
199
|
...node,
|
|
207
200
|
data: {
|
|
@@ -210,12 +203,16 @@ export const updateNodesOnPipelineStateChange = (nodes, pipelineState) => {
|
|
|
210
203
|
input: newInputHandles,
|
|
211
204
|
output: newOutputHandles,
|
|
212
205
|
},
|
|
206
|
+
extras: {
|
|
207
|
+
gpstTimings: gpst,
|
|
208
|
+
stateInfo: state,
|
|
209
|
+
},
|
|
213
210
|
},
|
|
214
211
|
});
|
|
215
212
|
}
|
|
216
213
|
return parsedNodes;
|
|
217
214
|
};
|
|
218
|
-
const PipelineCanvasBody = ({ pipeline, pipelineState, action, ...flexProps }) => {
|
|
215
|
+
const PipelineCanvasBody = ({ pipeline, pipelineState: pipelineStateParsed, action, ...flexProps }) => {
|
|
219
216
|
const { fitView, setViewport, getViewport } = useReactFlow();
|
|
220
217
|
const autoArrangeRef = React.useRef(true);
|
|
221
218
|
const widthSelector = (state) => state.width;
|
|
@@ -223,6 +220,7 @@ const PipelineCanvasBody = ({ pipeline, pipelineState, action, ...flexProps }) =
|
|
|
223
220
|
const reactFlowWidth = useStore(widthSelector);
|
|
224
221
|
const reactFlowHeight = useStore(heightSelector);
|
|
225
222
|
const [shouldFitAndResize, setShouldFitAndResize] = React.useState(false);
|
|
223
|
+
const pipelineState = React.useMemo(() => pipelineStateParsed, [pipelineStateParsed]);
|
|
226
224
|
// biome-ignore lint/correctness/useExhaustiveDependencies: Intended
|
|
227
225
|
React.useEffect(() => {
|
|
228
226
|
void fitView();
|
|
@@ -235,12 +233,13 @@ const PipelineCanvasBody = ({ pipeline, pipelineState, action, ...flexProps }) =
|
|
|
235
233
|
return;
|
|
236
234
|
}
|
|
237
235
|
const layouted = getLayoutedElements(pipeline?.nodes ?? [], pipeline?.edges ?? []);
|
|
238
|
-
const adjustedNodes = adjustNodes([...layouted.nodes], [...layouted.edges]);
|
|
239
236
|
if (pipelineState) {
|
|
240
|
-
const updatedNodes = updateNodesOnPipelineStateChange(
|
|
241
|
-
|
|
237
|
+
const updatedNodes = updateNodesOnPipelineStateChange([...layouted.nodes], pipelineState);
|
|
238
|
+
const adjustedNodes = adjustNodes([...updatedNodes], layouted.edges);
|
|
239
|
+
setNodes([...adjustedNodes]);
|
|
242
240
|
}
|
|
243
241
|
else {
|
|
242
|
+
const adjustedNodes = adjustNodes([...layouted.nodes], layouted.edges);
|
|
244
243
|
setNodes([...adjustedNodes]);
|
|
245
244
|
}
|
|
246
245
|
setEdges([...layouted.edges]);
|
|
@@ -265,17 +264,6 @@ const PipelineCanvasBody = ({ pipeline, pipelineState, action, ...flexProps }) =
|
|
|
265
264
|
await fitView({ nodes: nds });
|
|
266
265
|
};
|
|
267
266
|
// biome-ignore lint/correctness/useExhaustiveDependencies: Intended
|
|
268
|
-
React.useEffect(() => {
|
|
269
|
-
if (!autoArrangeRef.current) {
|
|
270
|
-
return;
|
|
271
|
-
}
|
|
272
|
-
if (nodes.length > 0 && edges.length > 0) {
|
|
273
|
-
const viewport = getViewport();
|
|
274
|
-
void setViewportAndFit(viewport.x, viewport.y, viewport.zoom, nodes);
|
|
275
|
-
}
|
|
276
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
277
|
-
}, [nodes, edges]);
|
|
278
|
-
// biome-ignore lint/correctness/useExhaustiveDependencies: Intended
|
|
279
267
|
React.useEffect(() => {
|
|
280
268
|
if (shouldFitAndResize) {
|
|
281
269
|
const viewport = getViewport();
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Edge } from '@xyflow/react';
|
|
2
2
|
import type { ParsedNode, RawPipelineNodeIO } from './pipeline';
|
|
3
|
+
import { PipelineStateDotColor } from './pipeline-state';
|
|
3
4
|
export type RawPipelineBridge = [number, number];
|
|
4
5
|
export type ParsedHandle = {
|
|
5
6
|
id: number;
|
|
@@ -10,6 +11,7 @@ export type ParsedHandle = {
|
|
|
10
11
|
connected: boolean;
|
|
11
12
|
maxQueueSize?: number;
|
|
12
13
|
fps?: number;
|
|
14
|
+
dotColor?: PipelineStateDotColor;
|
|
13
15
|
};
|
|
14
16
|
export declare function parseHandles(handles: RawPipelineNodeIO[]): {
|
|
15
17
|
input: ParsedHandle[];
|
|
@@ -27,6 +29,7 @@ export declare function filterNodesHandles(nodes: ParsedNode[], edges: Edge[]):
|
|
|
27
29
|
queueSize: number;
|
|
28
30
|
maxQueueSize?: number;
|
|
29
31
|
fps?: number;
|
|
32
|
+
dotColor?: PipelineStateDotColor;
|
|
30
33
|
}[];
|
|
31
34
|
output: {
|
|
32
35
|
connected: boolean;
|
|
@@ -37,11 +40,14 @@ export declare function filterNodesHandles(nodes: ParsedNode[], edges: Edge[]):
|
|
|
37
40
|
queueSize: number;
|
|
38
41
|
maxQueueSize?: number;
|
|
39
42
|
fps?: number;
|
|
43
|
+
dotColor?: PipelineStateDotColor;
|
|
40
44
|
}[];
|
|
41
45
|
};
|
|
42
46
|
id: string;
|
|
43
47
|
parentId?: string;
|
|
44
48
|
name: string;
|
|
49
|
+
nodeType?: "device" | "host";
|
|
50
|
+
extras?: import("./pipeline-state").NodeExtras;
|
|
45
51
|
};
|
|
46
52
|
id: string;
|
|
47
53
|
position: import("@xyflow/system").XYPosition;
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
export type RawPipelineStatePayload = {
|
|
2
2
|
nodeStates: RawPipelineState[];
|
|
3
3
|
};
|
|
4
|
+
type IONodeState = 0 | 1 | 2;
|
|
4
5
|
export type IOStates = {
|
|
5
6
|
[key: string]: {
|
|
6
7
|
numQueued: number;
|
|
7
8
|
timing: TimingWithFps;
|
|
9
|
+
state?: IONodeState;
|
|
10
|
+
dotColor?: PipelineStateDotColor;
|
|
8
11
|
};
|
|
9
12
|
};
|
|
10
13
|
type Timing = {
|
|
@@ -20,6 +23,8 @@ type TimingWithFps = {
|
|
|
20
23
|
durationStats: Timing;
|
|
21
24
|
fps: number;
|
|
22
25
|
};
|
|
26
|
+
type NodeStateRaw = 0 | 1 | 2 | 3;
|
|
27
|
+
type NodeState = 'IDLE' | 'GETTING_INPUTS' | 'PROCESSING' | 'SENDING_OUTPUTS';
|
|
23
28
|
export type RawPipelineState = [
|
|
24
29
|
number,
|
|
25
30
|
{
|
|
@@ -30,13 +35,31 @@ export type RawPipelineState = [
|
|
|
30
35
|
otherTimings: object;
|
|
31
36
|
outputStates: IOStates;
|
|
32
37
|
outputsSendTiming: TimingWithFps;
|
|
33
|
-
state
|
|
38
|
+
state?: NodeStateRaw;
|
|
34
39
|
}
|
|
35
40
|
];
|
|
41
|
+
export type PipelineStateDotColor = 'gray' | 'green' | 'yellow' | 'red';
|
|
42
|
+
export type GPSTTimingsType = {
|
|
43
|
+
G: number;
|
|
44
|
+
P: number;
|
|
45
|
+
S: number;
|
|
46
|
+
T: number;
|
|
47
|
+
};
|
|
48
|
+
export type NodeExtras = {
|
|
49
|
+
gpstTimings?: GPSTTimingsType;
|
|
50
|
+
stateInfo?: {
|
|
51
|
+
state: NodeState;
|
|
52
|
+
label: string;
|
|
53
|
+
letter: string;
|
|
54
|
+
};
|
|
55
|
+
};
|
|
36
56
|
export type PipelineState = {
|
|
37
57
|
id: number;
|
|
38
58
|
inputs: IOStates;
|
|
39
59
|
outputs: IOStates;
|
|
40
|
-
};
|
|
60
|
+
} & NodeExtras;
|
|
41
61
|
export declare function parsePipelineState(rawPayload: RawPipelineStatePayload): PipelineState[];
|
|
62
|
+
export declare function stateToLabel(state: NodeState): string;
|
|
63
|
+
export declare function stateToLetter(state: NodeState): string;
|
|
64
|
+
export declare function formatTiming(time: number): string;
|
|
42
65
|
export {};
|
|
@@ -1,13 +1,94 @@
|
|
|
1
|
+
function nodeStateToDotColor(state) {
|
|
2
|
+
switch (state) {
|
|
3
|
+
case 0:
|
|
4
|
+
return 'green';
|
|
5
|
+
case 1:
|
|
6
|
+
return 'yellow';
|
|
7
|
+
case 2:
|
|
8
|
+
return 'red';
|
|
9
|
+
default:
|
|
10
|
+
return 'gray';
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
function nodeStateToState(state) {
|
|
14
|
+
switch (state) {
|
|
15
|
+
case 0:
|
|
16
|
+
return 'IDLE';
|
|
17
|
+
case 1:
|
|
18
|
+
return 'GETTING_INPUTS';
|
|
19
|
+
case 2:
|
|
20
|
+
return 'PROCESSING';
|
|
21
|
+
case 3:
|
|
22
|
+
return 'SENDING_OUTPUTS';
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function formatIOStates(values) {
|
|
26
|
+
let returnObj = {};
|
|
27
|
+
for (const [key, value] of Object.entries(values)) {
|
|
28
|
+
returnObj = {
|
|
29
|
+
...returnObj,
|
|
30
|
+
[key]: {
|
|
31
|
+
...value,
|
|
32
|
+
dotColor: value.state ? nodeStateToDotColor(value.state) : 'gray',
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
return returnObj;
|
|
37
|
+
}
|
|
1
38
|
export function parsePipelineState(rawPayload) {
|
|
2
39
|
const { nodeStates } = rawPayload;
|
|
3
40
|
const parsedNodeStates = [];
|
|
4
41
|
for (const [nodeId, nodeState] of nodeStates) {
|
|
42
|
+
const state = nodeState?.state ? nodeStateToState(nodeState.state) : 'IDLE';
|
|
5
43
|
const currentNode = {
|
|
6
44
|
id: nodeId,
|
|
7
|
-
inputs: nodeState.inputStates,
|
|
8
|
-
outputs: nodeState.outputStates,
|
|
45
|
+
inputs: formatIOStates(nodeState.inputStates),
|
|
46
|
+
outputs: formatIOStates(nodeState.outputStates),
|
|
47
|
+
gpstTimings: {
|
|
48
|
+
G: nodeState.inputsGetTiming.durationStats.averageMicrosRecent,
|
|
49
|
+
P: nodeState.mainLoopTiming.durationStats.averageMicrosRecent -
|
|
50
|
+
nodeState.outputsSendTiming.durationStats.averageMicrosRecent -
|
|
51
|
+
nodeState.inputsGetTiming.durationStats.averageMicrosRecent,
|
|
52
|
+
S: nodeState.outputsSendTiming.durationStats.averageMicrosRecent,
|
|
53
|
+
T: nodeState.mainLoopTiming.durationStats.averageMicrosRecent,
|
|
54
|
+
},
|
|
55
|
+
stateInfo: {
|
|
56
|
+
state: state,
|
|
57
|
+
label: stateToLabel(state),
|
|
58
|
+
letter: stateToLetter(state),
|
|
59
|
+
},
|
|
9
60
|
};
|
|
10
61
|
parsedNodeStates.push(currentNode);
|
|
11
62
|
}
|
|
12
63
|
return parsedNodeStates;
|
|
13
64
|
}
|
|
65
|
+
export function stateToLabel(state) {
|
|
66
|
+
switch (state) {
|
|
67
|
+
case 'IDLE':
|
|
68
|
+
return 'Idle';
|
|
69
|
+
case 'GETTING_INPUTS':
|
|
70
|
+
return 'Getting Inputs';
|
|
71
|
+
case 'PROCESSING':
|
|
72
|
+
return 'Processing';
|
|
73
|
+
case 'SENDING_OUTPUTS':
|
|
74
|
+
return 'Sending Outputs';
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
export function stateToLetter(state) {
|
|
78
|
+
switch (state) {
|
|
79
|
+
case 'IDLE':
|
|
80
|
+
return 'I';
|
|
81
|
+
case 'GETTING_INPUTS':
|
|
82
|
+
return 'G';
|
|
83
|
+
case 'PROCESSING':
|
|
84
|
+
return 'P';
|
|
85
|
+
case 'SENDING_OUTPUTS':
|
|
86
|
+
return 'S';
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
// NOTE: Time is in microseconds. 1us => 0.001 ms => 1e-6 s
|
|
90
|
+
export function formatTiming(time) {
|
|
91
|
+
return Math.round(time / 1000)
|
|
92
|
+
.toString()
|
|
93
|
+
.padStart(3, '0');
|
|
94
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type Edge, type Node } from '@xyflow/react';
|
|
2
2
|
import { type ParsedHandle } from './pipeline-handles';
|
|
3
|
+
import { NodeExtras } from './pipeline-state';
|
|
3
4
|
export type RawPipelineNodeIO = [
|
|
4
5
|
[
|
|
5
6
|
string,
|
|
@@ -18,6 +19,7 @@ export type RawPipelineNode = {
|
|
|
18
19
|
ioInfo: RawPipelineNodeIO[];
|
|
19
20
|
name: string;
|
|
20
21
|
parentId?: number;
|
|
22
|
+
deviceNode?: boolean;
|
|
21
23
|
};
|
|
22
24
|
export type RawPipelineEdge = {
|
|
23
25
|
node1Id: number;
|
|
@@ -45,6 +47,8 @@ export type ParsedNode = Node<{
|
|
|
45
47
|
input: ParsedHandle[];
|
|
46
48
|
output: ParsedHandle[];
|
|
47
49
|
};
|
|
50
|
+
nodeType?: 'device' | 'host';
|
|
51
|
+
extras?: NodeExtras;
|
|
48
52
|
}>;
|
|
49
53
|
export declare function parsePipeline(rawPayload: RawPipelinePayload): Pipeline;
|
|
50
54
|
export declare const DOCS_BASE_URL = "https://docs.luxonis.com/software-v3/depthai/depthai-components/nodes";
|
|
@@ -40,6 +40,11 @@ export function parsePipeline(rawPayload) {
|
|
|
40
40
|
id: id.toString(),
|
|
41
41
|
name: node.name,
|
|
42
42
|
handles: parseHandles(node.ioInfo),
|
|
43
|
+
nodeType: node.deviceNode === undefined
|
|
44
|
+
? undefined
|
|
45
|
+
: node.deviceNode
|
|
46
|
+
? 'device'
|
|
47
|
+
: 'host',
|
|
43
48
|
},
|
|
44
49
|
})) ?? [];
|
|
45
50
|
// Check for parent nodes and if there is children with some parent node that doesn't exist the create it as fake node
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export const topoSort = (nodes) => {
|
|
2
|
+
const nodeMap = new Map(nodes.map((n) => [n.id, n]));
|
|
3
|
+
const result = [];
|
|
4
|
+
const visited = new Set();
|
|
5
|
+
const visit = (id) => {
|
|
6
|
+
if (visited.has(id))
|
|
7
|
+
return;
|
|
8
|
+
visited.add(id);
|
|
9
|
+
const node = nodeMap.get(id);
|
|
10
|
+
if (!node)
|
|
11
|
+
return;
|
|
12
|
+
// Visit parent first
|
|
13
|
+
if (node.parentId)
|
|
14
|
+
visit(node.parentId);
|
|
15
|
+
result.push(node);
|
|
16
|
+
};
|
|
17
|
+
for (const node of nodes) {
|
|
18
|
+
visit(node.id);
|
|
19
|
+
}
|
|
20
|
+
return result;
|
|
21
|
+
};
|