@luxonis/depthai-pipeline-lib 1.11.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/index.css +3 -3
- package/dist/panda.css +22 -6
- package/dist/src/components/GroupNode.js +6 -6
- package/dist/src/components/Node.d.ts +1 -1
- package/dist/src/components/Node.js +45 -17
- package/dist/src/components/PipelineCanvas.d.ts +3 -1
- package/dist/src/components/PipelineCanvas.js +143 -106
- package/dist/src/index.d.ts +4 -4
- package/dist/src/index.js +1 -1
- package/dist/src/services/pipeline-handles.d.ts +84 -0
- package/dist/src/services/pipeline-handles.js +41 -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 +5 -11
- package/dist/src/services/pipeline.js +72 -60
- package/dist/src/services/utils.d.ts +2 -0
- package/dist/src/services/utils.js +21 -0
- package/package.json +48 -62
package/dist/index.css
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
@import url(
|
|
2
|
-
@import url(
|
|
3
|
-
@import url(
|
|
1
|
+
@import url("@xyflow/react/dist/style.css");
|
|
2
|
+
@import url("@luxonis/common-fe-components/styles");
|
|
3
|
+
@import url("./panda.css");
|
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
|
}
|
|
@@ -6,14 +6,14 @@ import { css } from '../styled-system/css/index.mjs';
|
|
|
6
6
|
export const GroupNode = (props) => {
|
|
7
7
|
const { data: _node } = props;
|
|
8
8
|
return (_jsx(Flex, { direction: "column", className: css({
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
minWidth: 'container.smaller.xxs',
|
|
10
|
+
border: 'base',
|
|
11
|
+
rounded: 'common',
|
|
12
12
|
'&:hover .node-help-icon': {
|
|
13
13
|
color: 'text.normal',
|
|
14
14
|
},
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
padding: 0,
|
|
16
|
+
borderStyle: 'dashed',
|
|
17
|
+
backgroundColor: 'rgba(0,0,0,0.125)',
|
|
18
18
|
}) }));
|
|
19
19
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
1
|
import type { NodeProps } from '@xyflow/react';
|
|
2
|
+
import React from 'react';
|
|
3
3
|
import { type ParsedNode } from '../services/pipeline.js';
|
|
4
4
|
export type PipelineNodeProps = NodeProps<ParsedNode>;
|
|
5
5
|
export declare const PipelineNode: React.FC<PipelineNodeProps>;
|
|
@@ -1,25 +1,37 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
2
|
+
import { Badge, Button, Flex, HelpIcon, Label, SubLabel, } from '@luxonis/common-fe-components';
|
|
3
3
|
import { clsx } from '@luxonis/common-fe-components/helpers';
|
|
4
|
-
import {
|
|
5
|
-
import { css } from '../styled-system/css/css.mjs';
|
|
4
|
+
import { Handle, Position } from '@xyflow/react';
|
|
6
5
|
import { DOCS_BASE_URL, NodesWithLinks, } from '../services/pipeline.js';
|
|
7
|
-
|
|
6
|
+
import { formatTiming } from '../services/pipeline-state.js';
|
|
7
|
+
import { css } from '../styled-system/css/css.mjs';
|
|
8
|
+
import { MIN_NODE_WIDTH } from './PipelineCanvas.js';
|
|
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));
|
|
20
32
|
}) }));
|
|
21
33
|
};
|
|
22
|
-
const NodeHandlesSubLabel = props => {
|
|
34
|
+
const NodeHandlesSubLabel = (props) => {
|
|
23
35
|
const { type, name, queueSize, maxQueueSize, fps } = props;
|
|
24
36
|
return (_jsx(SubLabel, { className: css({
|
|
25
37
|
...(type === 'input' //
|
|
@@ -29,17 +41,20 @@ const NodeHandlesSubLabel = props => {
|
|
|
29
41
|
? `[${fps ? `${fps.toFixed(1)} | ` : ''}${`${maxQueueSize ?? 0}/${queueSize}`}] ${name}`
|
|
30
42
|
: `${fps ? `[${fps.toFixed(1)}]` : ''} ${name}`, break: "none" }));
|
|
31
43
|
};
|
|
32
|
-
|
|
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
|
+
};
|
|
47
|
+
export const PipelineNode = (props) => {
|
|
33
48
|
const { data: node } = props;
|
|
34
|
-
//
|
|
49
|
+
// biome-ignore lint/suspicious/noPrototypeBuiltins: Intended
|
|
35
50
|
const link = NodesWithLinks.hasOwnProperty(node.name)
|
|
36
51
|
? `${DOCS_BASE_URL}/${NodesWithLinks[node.name]}`
|
|
37
52
|
: DOCS_BASE_URL;
|
|
38
53
|
return (_jsxs(Flex, { direction: "column", className: css({
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
54
|
+
minWidth: 'container.smaller.xxs',
|
|
55
|
+
border: 'base',
|
|
56
|
+
rounded: 'common',
|
|
57
|
+
backgroundColor: 'white',
|
|
43
58
|
'&:hover .node-help-icon': {
|
|
44
59
|
color: 'text.normal',
|
|
45
60
|
},
|
|
@@ -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: {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
1
|
import type { FlexProps } from '@luxonis/common-fe-components';
|
|
2
|
+
import React from 'react';
|
|
3
3
|
import type { ParsedNode, Pipeline } from '../services/pipeline.js';
|
|
4
4
|
import type { PipelineState } from '../services/pipeline-state.js';
|
|
5
5
|
export type PipelineCanvasProps = FlexProps & {
|
|
@@ -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>;
|
|
@@ -1,79 +1,84 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import React from 'react';
|
|
3
|
-
import { Panel, ReactFlow, ReactFlowProvider, useEdgesState, useNodesState, useReactFlow, useStore, } from '@xyflow/react';
|
|
4
|
-
import { Flex, Header } from '@luxonis/common-fe-components';
|
|
5
2
|
import Dagre from '@dagrejs/dagre';
|
|
3
|
+
import { Flex, Header } from '@luxonis/common-fe-components';
|
|
4
|
+
import { Panel, ReactFlow, ReactFlowProvider, useEdgesState, useNodesState, useReactFlow, useStore, } from '@xyflow/react';
|
|
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
|
-
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
|
-
};
|
|
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 } };
|
|
33
30
|
}),
|
|
34
31
|
edges,
|
|
35
32
|
};
|
|
36
33
|
};
|
|
37
34
|
const adjustNodes = (nodes, edges) => {
|
|
38
35
|
const PADDING = 16;
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
|
46
51
|
for (const node of childNodes) {
|
|
47
|
-
|
|
48
|
-
width: node.measured?.width ??
|
|
49
|
-
height: node.measured?.height ??
|
|
50
|
-
});
|
|
51
|
-
}
|
|
52
|
-
Dagre.layout(graph);
|
|
53
|
-
const positionedChildren = childNodes.map(node => {
|
|
54
|
-
const pos = graph.node(node.id);
|
|
55
|
-
return {
|
|
56
|
-
...node,
|
|
57
|
-
position: {
|
|
58
|
-
x: pos.x - (node.measured?.width ?? 300) / 2,
|
|
59
|
-
y: pos.y - (node.measured?.height ?? 150) / 2,
|
|
60
|
-
},
|
|
52
|
+
resolvedSizes[node.id] = {
|
|
53
|
+
width: Math.min(node.measured?.width ?? MIN_NODE_WIDTH),
|
|
54
|
+
height: Math.min(node.measured?.height ?? MIN_NODE_HEIGHT),
|
|
61
55
|
};
|
|
62
|
-
}
|
|
63
|
-
const
|
|
64
|
-
|
|
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);
|
|
65
63
|
if (children.length === 0) {
|
|
66
|
-
|
|
64
|
+
resolvedSizes[group.id] = {
|
|
65
|
+
width: MIN_NODE_WIDTH,
|
|
66
|
+
height: MIN_NODE_HEIGHT,
|
|
67
|
+
};
|
|
68
|
+
continue;
|
|
67
69
|
}
|
|
68
70
|
const g = new Dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({}));
|
|
69
|
-
g.setGraph({ rankdir: 'LR'
|
|
71
|
+
g.setGraph({ rankdir: 'LR' });
|
|
70
72
|
for (const child of children) {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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 });
|
|
75
79
|
}
|
|
76
|
-
const groupEdges = edges.filter(e => children.some(c => c.id === e.source) &&
|
|
80
|
+
const groupEdges = edges.filter((e) => children.some((c) => c.id === e.source) &&
|
|
81
|
+
children.some((c) => c.id === e.target));
|
|
77
82
|
for (const edge of groupEdges) {
|
|
78
83
|
g.setEdge(edge.source, edge.target);
|
|
79
84
|
}
|
|
@@ -82,59 +87,86 @@ const adjustNodes = (nodes, edges) => {
|
|
|
82
87
|
let minY = 9999;
|
|
83
88
|
let maxX = -9999;
|
|
84
89
|
let maxY = -9999;
|
|
85
|
-
|
|
90
|
+
// Replace both child loops with this single pass:
|
|
91
|
+
for (const child of children) {
|
|
86
92
|
const pos = g.node(child.id);
|
|
87
|
-
const
|
|
88
|
-
|
|
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;
|
|
89
99
|
minX = Math.min(minX, x);
|
|
90
100
|
minY = Math.min(minY, y);
|
|
91
|
-
maxX = Math.max(maxX, x +
|
|
92
|
-
maxY = Math.max(maxY, y +
|
|
93
|
-
return { ...child, position: { x, y } };
|
|
94
|
-
});
|
|
95
|
-
const relativeChildren = locallyPositioned.map(child => ({
|
|
96
|
-
...child,
|
|
97
|
-
position: {
|
|
98
|
-
x: child.position.x - minX + PADDING,
|
|
99
|
-
y: child.position.y - minY + PADDING,
|
|
100
|
-
},
|
|
101
|
-
}));
|
|
102
|
-
const groupW = maxX - minX + PADDING * 2;
|
|
103
|
-
const groupH = maxY - minY + PADDING * 2;
|
|
104
|
-
let absMinX = 9999;
|
|
105
|
-
let absMinY = 9999;
|
|
106
|
-
for (const child of children) {
|
|
107
|
-
const pos = graph.node(child.id);
|
|
108
|
-
absMinX = Math.min(absMinX, pos.x - (child.measured?.width ?? 300) / 2);
|
|
109
|
-
absMinY = Math.min(absMinY, pos.y - (child.measured?.height ?? 150) / 2);
|
|
101
|
+
maxX = Math.max(maxX, x + size.width);
|
|
102
|
+
maxY = Math.max(maxY, y + size.height);
|
|
110
103
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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],
|
|
115
113
|
position: {
|
|
116
|
-
x:
|
|
117
|
-
y:
|
|
114
|
+
x: pos.x - size.width / 2 - minX + PADDING,
|
|
115
|
+
y: pos.y - size.height / 2 - minY + PADDING + HEADER_HEIGHT,
|
|
118
116
|
},
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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,
|
|
122
160
|
},
|
|
123
161
|
};
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const groupedChildren = positionedGroups.flatMap(g => g.nodes);
|
|
127
|
-
const groups = positionedGroups.map(g => g.group);
|
|
128
|
-
return [
|
|
129
|
-
...groups.filter(g => g !== undefined),
|
|
130
|
-
...groupedChildren.filter(c => c !== undefined),
|
|
131
|
-
...rootChildren,
|
|
132
|
-
];
|
|
162
|
+
}
|
|
163
|
+
return topoSort(Object.values(updatedNodes));
|
|
133
164
|
};
|
|
134
165
|
export const updateNodesOnPipelineStateChange = (nodes, pipelineState) => {
|
|
135
166
|
const parsedNodes = [];
|
|
136
167
|
for (const node of nodes) {
|
|
137
|
-
const nodeState = pipelineState.find(state => state.id.toString() === node.id.toString());
|
|
168
|
+
const nodeState = pipelineState.find((state) => state.id.toString() === node.id.toString());
|
|
169
|
+
// Inputs and outputs
|
|
138
170
|
const inputHandles = node.data.handles.input;
|
|
139
171
|
const outputHandles = node.data.handles.output;
|
|
140
172
|
const newInputHandles = [];
|
|
@@ -144,6 +176,7 @@ export const updateNodesOnPipelineStateChange = (nodes, pipelineState) => {
|
|
|
144
176
|
if (inputState) {
|
|
145
177
|
obj.maxQueueSize = inputState.numQueued;
|
|
146
178
|
obj.fps = inputState.timing.fps;
|
|
179
|
+
obj.dotColor = inputState.dotColor;
|
|
147
180
|
}
|
|
148
181
|
newInputHandles.push(obj);
|
|
149
182
|
}
|
|
@@ -154,9 +187,14 @@ export const updateNodesOnPipelineStateChange = (nodes, pipelineState) => {
|
|
|
154
187
|
if (outputState) {
|
|
155
188
|
obj.maxQueueSize = outputState.numQueued;
|
|
156
189
|
obj.fps = outputState.timing.fps;
|
|
190
|
+
obj.dotColor = outputState.dotColor;
|
|
157
191
|
}
|
|
158
192
|
newOutputHandles.push(obj);
|
|
159
193
|
}
|
|
194
|
+
// GPST timings
|
|
195
|
+
const gpst = nodeState?.gpstTimings;
|
|
196
|
+
// Node state
|
|
197
|
+
const state = nodeState?.stateInfo;
|
|
160
198
|
parsedNodes.push({
|
|
161
199
|
...node,
|
|
162
200
|
data: {
|
|
@@ -165,12 +203,16 @@ export const updateNodesOnPipelineStateChange = (nodes, pipelineState) => {
|
|
|
165
203
|
input: newInputHandles,
|
|
166
204
|
output: newOutputHandles,
|
|
167
205
|
},
|
|
206
|
+
extras: {
|
|
207
|
+
gpstTimings: gpst,
|
|
208
|
+
stateInfo: state,
|
|
209
|
+
},
|
|
168
210
|
},
|
|
169
211
|
});
|
|
170
212
|
}
|
|
171
213
|
return parsedNodes;
|
|
172
214
|
};
|
|
173
|
-
const PipelineCanvasBody = ({ pipeline, pipelineState, action, ...flexProps }) => {
|
|
215
|
+
const PipelineCanvasBody = ({ pipeline, pipelineState: pipelineStateParsed, action, ...flexProps }) => {
|
|
174
216
|
const { fitView, setViewport, getViewport } = useReactFlow();
|
|
175
217
|
const autoArrangeRef = React.useRef(true);
|
|
176
218
|
const widthSelector = (state) => state.width;
|
|
@@ -178,6 +220,8 @@ const PipelineCanvasBody = ({ pipeline, pipelineState, action, ...flexProps }) =
|
|
|
178
220
|
const reactFlowWidth = useStore(widthSelector);
|
|
179
221
|
const reactFlowHeight = useStore(heightSelector);
|
|
180
222
|
const [shouldFitAndResize, setShouldFitAndResize] = React.useState(false);
|
|
223
|
+
const pipelineState = React.useMemo(() => pipelineStateParsed, [pipelineStateParsed]);
|
|
224
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: Intended
|
|
181
225
|
React.useEffect(() => {
|
|
182
226
|
void fitView();
|
|
183
227
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
@@ -189,17 +233,19 @@ const PipelineCanvasBody = ({ pipeline, pipelineState, action, ...flexProps }) =
|
|
|
189
233
|
return;
|
|
190
234
|
}
|
|
191
235
|
const layouted = getLayoutedElements(pipeline?.nodes ?? [], pipeline?.edges ?? []);
|
|
192
|
-
const adjustedNodes = adjustNodes([...layouted.nodes], [...layouted.edges]);
|
|
193
236
|
if (pipelineState) {
|
|
194
|
-
const updatedNodes = updateNodesOnPipelineStateChange(
|
|
195
|
-
|
|
237
|
+
const updatedNodes = updateNodesOnPipelineStateChange([...layouted.nodes], pipelineState);
|
|
238
|
+
const adjustedNodes = adjustNodes([...updatedNodes], layouted.edges);
|
|
239
|
+
setNodes([...adjustedNodes]);
|
|
196
240
|
}
|
|
197
241
|
else {
|
|
242
|
+
const adjustedNodes = adjustNodes([...layouted.nodes], layouted.edges);
|
|
198
243
|
setNodes([...adjustedNodes]);
|
|
199
244
|
}
|
|
200
245
|
setEdges([...layouted.edges]);
|
|
201
246
|
setShouldFitAndResize(true);
|
|
202
247
|
}, [pipeline?.edges, pipeline?.nodes, setEdges, setNodes, pipelineState]);
|
|
248
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: Intended
|
|
203
249
|
React.useEffect(() => {
|
|
204
250
|
if (pipelineState && nodes.length > 0) {
|
|
205
251
|
const updatedNodes = updateNodesOnPipelineStateChange(nodes, pipelineState);
|
|
@@ -217,16 +263,7 @@ const PipelineCanvasBody = ({ pipeline, pipelineState, action, ...flexProps }) =
|
|
|
217
263
|
await setViewport({ x, y, zoom });
|
|
218
264
|
await fitView({ nodes: nds });
|
|
219
265
|
};
|
|
220
|
-
|
|
221
|
-
if (!autoArrangeRef.current) {
|
|
222
|
-
return;
|
|
223
|
-
}
|
|
224
|
-
if (nodes.length > 0 && edges.length > 0) {
|
|
225
|
-
const viewport = getViewport();
|
|
226
|
-
void setViewportAndFit(viewport.x, viewport.y, viewport.zoom, nodes);
|
|
227
|
-
}
|
|
228
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
229
|
-
}, [nodes, edges]);
|
|
266
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: Intended
|
|
230
267
|
React.useEffect(() => {
|
|
231
268
|
if (shouldFitAndResize) {
|
|
232
269
|
const viewport = getViewport();
|
|
@@ -237,4 +274,4 @@ const PipelineCanvasBody = ({ pipeline, pipelineState, action, ...flexProps }) =
|
|
|
237
274
|
}, [shouldFitAndResize]);
|
|
238
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 && (_jsx(ReactFlow, { nodes: nodes, edges: edges, onNodeDragStart: () => (autoArrangeRef.current = false), onNodesChange: onNodesChange, fitView: true, nodeTypes: { generic: PipelineNode }, minZoom: 0.4, children: action && _jsx(Panel, { position: "top-right", children: action }) }))] }));
|
|
239
276
|
};
|
|
240
|
-
export const PipelineCanvas = props => (_jsx(ReactFlowProvider, { children: _jsx(PipelineCanvasBody, { ...props }) }));
|
|
277
|
+
export const PipelineCanvas = (props) => (_jsx(ReactFlowProvider, { children: _jsx(PipelineCanvasBody, { ...props }) }));
|
package/dist/src/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export type { PipelineCanvasProps } from './components/PipelineCanvas.js';
|
|
2
|
+
export { PipelineCanvas } from './components/PipelineCanvas.js';
|
|
2
3
|
export type { Pipeline, RawPipelinePayload } from './services/pipeline.js';
|
|
4
|
+
export { parsePipeline } from './services/pipeline.js';
|
|
5
|
+
export type { PipelineState, RawPipelineStatePayload, } from './services/pipeline-state.js';
|
|
3
6
|
export { parsePipelineState } from './services/pipeline-state.js';
|
|
4
|
-
export type { PipelineState, RawPipelineStatePayload } from './services/pipeline-state.js';
|
|
5
|
-
export { PipelineCanvas } from './components/PipelineCanvas.js';
|
|
6
|
-
export type { PipelineCanvasProps } from './components/PipelineCanvas.js';
|
package/dist/src/index.js
CHANGED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { Edge } from '@xyflow/react';
|
|
2
|
+
import type { ParsedNode, RawPipelineNodeIO } from './pipeline';
|
|
3
|
+
import { PipelineStateDotColor } from './pipeline-state';
|
|
4
|
+
export type RawPipelineBridge = [number, number];
|
|
5
|
+
export type ParsedHandle = {
|
|
6
|
+
id: number;
|
|
7
|
+
name: string;
|
|
8
|
+
type: 'input' | 'output';
|
|
9
|
+
blocking: boolean;
|
|
10
|
+
queueSize: number;
|
|
11
|
+
connected: boolean;
|
|
12
|
+
maxQueueSize?: number;
|
|
13
|
+
fps?: number;
|
|
14
|
+
dotColor?: PipelineStateDotColor;
|
|
15
|
+
};
|
|
16
|
+
export declare function parseHandles(handles: RawPipelineNodeIO[]): {
|
|
17
|
+
input: ParsedHandle[];
|
|
18
|
+
output: ParsedHandle[];
|
|
19
|
+
};
|
|
20
|
+
export declare function filterNodesHandles(nodes: ParsedNode[], edges: Edge[]): {
|
|
21
|
+
data: {
|
|
22
|
+
handles: {
|
|
23
|
+
input: {
|
|
24
|
+
connected: boolean;
|
|
25
|
+
id: number;
|
|
26
|
+
name: string;
|
|
27
|
+
type: "input" | "output";
|
|
28
|
+
blocking: boolean;
|
|
29
|
+
queueSize: number;
|
|
30
|
+
maxQueueSize?: number;
|
|
31
|
+
fps?: number;
|
|
32
|
+
dotColor?: PipelineStateDotColor;
|
|
33
|
+
}[];
|
|
34
|
+
output: {
|
|
35
|
+
connected: boolean;
|
|
36
|
+
id: number;
|
|
37
|
+
name: string;
|
|
38
|
+
type: "input" | "output";
|
|
39
|
+
blocking: boolean;
|
|
40
|
+
queueSize: number;
|
|
41
|
+
maxQueueSize?: number;
|
|
42
|
+
fps?: number;
|
|
43
|
+
dotColor?: PipelineStateDotColor;
|
|
44
|
+
}[];
|
|
45
|
+
};
|
|
46
|
+
id: string;
|
|
47
|
+
parentId?: string;
|
|
48
|
+
name: string;
|
|
49
|
+
nodeType?: "device" | "host";
|
|
50
|
+
extras?: import("./pipeline-state").NodeExtras;
|
|
51
|
+
};
|
|
52
|
+
id: string;
|
|
53
|
+
position: import("@xyflow/system").XYPosition;
|
|
54
|
+
type?: string | undefined;
|
|
55
|
+
sourcePosition?: import("@xyflow/system").Position;
|
|
56
|
+
targetPosition?: import("@xyflow/system").Position;
|
|
57
|
+
hidden?: boolean;
|
|
58
|
+
selected?: boolean;
|
|
59
|
+
dragging?: boolean;
|
|
60
|
+
draggable?: boolean;
|
|
61
|
+
selectable?: boolean;
|
|
62
|
+
connectable?: boolean;
|
|
63
|
+
deletable?: boolean;
|
|
64
|
+
dragHandle?: string;
|
|
65
|
+
width?: number;
|
|
66
|
+
height?: number;
|
|
67
|
+
initialWidth?: number;
|
|
68
|
+
initialHeight?: number;
|
|
69
|
+
parentId?: string;
|
|
70
|
+
zIndex?: number;
|
|
71
|
+
extent?: "parent" | import("@xyflow/system").CoordinateExtent;
|
|
72
|
+
expandParent?: boolean;
|
|
73
|
+
ariaLabel?: string;
|
|
74
|
+
origin?: import("@xyflow/system").NodeOrigin;
|
|
75
|
+
handles?: import("@xyflow/system").NodeHandle[];
|
|
76
|
+
measured?: {
|
|
77
|
+
width?: number;
|
|
78
|
+
height?: number;
|
|
79
|
+
};
|
|
80
|
+
style?: import("react").CSSProperties;
|
|
81
|
+
className?: string;
|
|
82
|
+
resizing?: boolean;
|
|
83
|
+
focusable?: boolean;
|
|
84
|
+
}[];
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export function parseHandles(handles) {
|
|
2
|
+
const parsedHandles = {
|
|
3
|
+
input: [],
|
|
4
|
+
output: [],
|
|
5
|
+
};
|
|
6
|
+
for (const [_, handle] of handles) {
|
|
7
|
+
const type = handle.type === 3 ? 'input' : 'output';
|
|
8
|
+
const parsed = {
|
|
9
|
+
id: handle.id,
|
|
10
|
+
name: handle.name,
|
|
11
|
+
type,
|
|
12
|
+
blocking: handle.blocking,
|
|
13
|
+
queueSize: handle.queueSize,
|
|
14
|
+
connected: true,
|
|
15
|
+
};
|
|
16
|
+
parsedHandles[type].push(parsed);
|
|
17
|
+
}
|
|
18
|
+
return parsedHandles;
|
|
19
|
+
}
|
|
20
|
+
export function filterNodesHandles(nodes, edges) {
|
|
21
|
+
const connectedHandleIds = new Set(edges.flatMap((edge) => [
|
|
22
|
+
`${edge.source}-${edge.sourceHandle}`,
|
|
23
|
+
`${edge.target}-${edge.targetHandle}`,
|
|
24
|
+
]));
|
|
25
|
+
return nodes.map((node) => ({
|
|
26
|
+
...node,
|
|
27
|
+
data: {
|
|
28
|
+
...node.data,
|
|
29
|
+
handles: {
|
|
30
|
+
input: node.data.handles.input.map((handle) => ({
|
|
31
|
+
...handle,
|
|
32
|
+
connected: connectedHandleIds.has(`${node.id}-${handle.name}`),
|
|
33
|
+
})),
|
|
34
|
+
output: node.data.handles.output.map((handle) => ({
|
|
35
|
+
...handle,
|
|
36
|
+
connected: connectedHandleIds.has(`${node.id}-${handle.name}`),
|
|
37
|
+
})),
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
}));
|
|
41
|
+
}
|
|
@@ -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,4 +1,6 @@
|
|
|
1
1
|
import { type Edge, type Node } from '@xyflow/react';
|
|
2
|
+
import { type ParsedHandle } from './pipeline-handles';
|
|
3
|
+
import { NodeExtras } from './pipeline-state';
|
|
2
4
|
export type RawPipelineNodeIO = [
|
|
3
5
|
[
|
|
4
6
|
string,
|
|
@@ -17,8 +19,8 @@ export type RawPipelineNode = {
|
|
|
17
19
|
ioInfo: RawPipelineNodeIO[];
|
|
18
20
|
name: string;
|
|
19
21
|
parentId?: number;
|
|
22
|
+
deviceNode?: boolean;
|
|
20
23
|
};
|
|
21
|
-
export type RawPipelineBridge = [number, number];
|
|
22
24
|
export type RawPipelineEdge = {
|
|
23
25
|
node1Id: number;
|
|
24
26
|
node1Output: string;
|
|
@@ -37,16 +39,6 @@ export type Pipeline = {
|
|
|
37
39
|
nodes: ParsedNode[];
|
|
38
40
|
edges: Edge[];
|
|
39
41
|
};
|
|
40
|
-
export type ParsedHandle = {
|
|
41
|
-
id: number;
|
|
42
|
-
name: string;
|
|
43
|
-
type: 'input' | 'output';
|
|
44
|
-
blocking: boolean;
|
|
45
|
-
queueSize: number;
|
|
46
|
-
connected: boolean;
|
|
47
|
-
maxQueueSize?: number;
|
|
48
|
-
fps?: number;
|
|
49
|
-
};
|
|
50
42
|
export type ParsedNode = Node<{
|
|
51
43
|
id: string;
|
|
52
44
|
parentId?: string;
|
|
@@ -55,6 +47,8 @@ export type ParsedNode = Node<{
|
|
|
55
47
|
input: ParsedHandle[];
|
|
56
48
|
output: ParsedHandle[];
|
|
57
49
|
};
|
|
50
|
+
nodeType?: 'device' | 'host';
|
|
51
|
+
extras?: NodeExtras;
|
|
58
52
|
}>;
|
|
59
53
|
export declare function parsePipeline(rawPayload: RawPipelinePayload): Pipeline;
|
|
60
54
|
export declare const DOCS_BASE_URL = "https://docs.luxonis.com/software-v3/depthai/depthai-components/nodes";
|
|
@@ -1,74 +1,78 @@
|
|
|
1
|
-
import { MarkerType } from '@xyflow/react';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
function filterNodesHandles(nodes, edges) {
|
|
22
|
-
const connectedHandleIds = new Set(edges.flatMap(edge => [
|
|
23
|
-
`${edge.source}-${edge.sourceHandle}`,
|
|
24
|
-
`${edge.target}-${edge.targetHandle}`,
|
|
25
|
-
]));
|
|
26
|
-
return nodes.map(node => ({
|
|
27
|
-
...node,
|
|
28
|
-
data: {
|
|
29
|
-
...node.data,
|
|
30
|
-
handles: {
|
|
31
|
-
input: node.data.handles.input.map(handle => ({
|
|
32
|
-
...handle,
|
|
33
|
-
connected: connectedHandleIds.has(`${node.id}-${handle.name}`),
|
|
34
|
-
})),
|
|
35
|
-
output: node.data.handles.output.map(handle => ({
|
|
36
|
-
...handle,
|
|
37
|
-
connected: connectedHandleIds.has(`${node.id}-${handle.name}`),
|
|
38
|
-
})),
|
|
39
|
-
},
|
|
1
|
+
import { MarkerType, } from '@xyflow/react';
|
|
2
|
+
import { filterNodesHandles, parseHandles, } from './pipeline-handles';
|
|
3
|
+
function addFakeNode(id) {
|
|
4
|
+
return [
|
|
5
|
+
typeof id === 'number' ? id : parseInt(id),
|
|
6
|
+
{
|
|
7
|
+
id: typeof id === 'number' ? id : parseInt(id),
|
|
8
|
+
name: 'Unknown',
|
|
9
|
+
ioInfo: [
|
|
10
|
+
[
|
|
11
|
+
['', ''],
|
|
12
|
+
{
|
|
13
|
+
id: 0,
|
|
14
|
+
name: 'unknown',
|
|
15
|
+
type: 0,
|
|
16
|
+
blocking: false,
|
|
17
|
+
queueSize: 0,
|
|
18
|
+
},
|
|
19
|
+
],
|
|
20
|
+
],
|
|
40
21
|
},
|
|
41
|
-
|
|
22
|
+
];
|
|
42
23
|
}
|
|
43
24
|
export function parsePipeline(rawPayload) {
|
|
44
25
|
const { pipeline } = rawPayload;
|
|
45
26
|
// Set all nodes as generic nodes
|
|
46
27
|
const nodes = pipeline.nodes
|
|
47
|
-
.filter(([_, node]) => pipeline.connections.some(connection => connection.node1Id === node.id || connection.node2Id === node.id))
|
|
28
|
+
.filter(([_, node]) => pipeline.connections.some((connection) => connection.node1Id === node.id || connection.node2Id === node.id))
|
|
48
29
|
.map(([id, node]) => ({
|
|
49
30
|
type: 'generic',
|
|
50
31
|
id: id.toString(),
|
|
51
32
|
position: { x: 0, y: 0 },
|
|
52
|
-
parentId: node.parentId?.toString() === '-1'
|
|
53
|
-
|
|
33
|
+
parentId: node.parentId?.toString() === '-1'
|
|
34
|
+
? undefined
|
|
35
|
+
: node.parentId?.toString(),
|
|
36
|
+
extent: node.parentId?.toString() === '-1' || node.parentId === undefined
|
|
37
|
+
? undefined
|
|
38
|
+
: 'parent',
|
|
54
39
|
data: {
|
|
55
40
|
id: id.toString(),
|
|
56
41
|
name: node.name,
|
|
57
42
|
handles: parseHandles(node.ioInfo),
|
|
43
|
+
nodeType: node.deviceNode === undefined
|
|
44
|
+
? undefined
|
|
45
|
+
: node.deviceNode
|
|
46
|
+
? 'device'
|
|
47
|
+
: 'host',
|
|
58
48
|
},
|
|
59
49
|
})) ?? [];
|
|
50
|
+
// Check for parent nodes and if there is children with some parent node that doesn't exist the create it as fake node
|
|
51
|
+
const parentNodesIdsRaw = [
|
|
52
|
+
...new Set(nodes
|
|
53
|
+
.map((node) => {
|
|
54
|
+
if (node.parentId !== undefined && node.parentId !== '-1') {
|
|
55
|
+
return node.parentId;
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
.filter((id) => id !== null)),
|
|
62
|
+
];
|
|
63
|
+
const parentNodesArr = parentNodesIdsRaw.map((id) => pipeline.nodes.find(([_, node]) => node.id.toString() === id) ??
|
|
64
|
+
addFakeNode(id));
|
|
60
65
|
// Set all parent nodes as group nodes
|
|
61
|
-
const mappedParentNodes = nodes.map(node => node.parentId).filter(id => id !== undefined);
|
|
62
|
-
const filteredParentNodes = mappedParentNodes.filter(id => id !== '-1');
|
|
63
|
-
const parentNodes = [...new Set(filteredParentNodes)];
|
|
64
|
-
const parentNodesArr = parentNodes
|
|
65
|
-
.map(id => pipeline.nodes.find(([_, node]) => node.id.toString() === id))
|
|
66
|
-
.filter(node => node !== undefined);
|
|
67
66
|
const formattedParentNodes = parentNodesArr.map(([id, node]) => ({
|
|
68
67
|
type: 'group',
|
|
69
68
|
id: id.toString(),
|
|
70
69
|
position: { x: 0, y: 0 },
|
|
71
|
-
parentId: node.parentId?.toString() === '-1'
|
|
70
|
+
parentId: node.parentId?.toString() === '-1' || node.parentId === undefined
|
|
71
|
+
? undefined
|
|
72
|
+
: `${node.parentId?.toString()}-parent`,
|
|
73
|
+
extent: node.parentId?.toString() === '-1' || node.parentId === undefined
|
|
74
|
+
? undefined
|
|
75
|
+
: 'parent',
|
|
72
76
|
data: {
|
|
73
77
|
id: id.toString(),
|
|
74
78
|
name: node.name,
|
|
@@ -80,9 +84,10 @@ export function parsePipeline(rawPayload) {
|
|
|
80
84
|
background: 'rgba(0,0,0,0.125)',
|
|
81
85
|
},
|
|
82
86
|
})) ?? [];
|
|
83
|
-
|
|
87
|
+
// Children nodes
|
|
88
|
+
const parentNodesIds = parentNodesIdsRaw.map((id) => id.toString());
|
|
84
89
|
const childrenNodes = nodes
|
|
85
|
-
.map(node => {
|
|
90
|
+
.map((node) => {
|
|
86
91
|
if (node.parentId && parentNodesIds.includes(node.parentId)) {
|
|
87
92
|
return {
|
|
88
93
|
...node,
|
|
@@ -101,11 +106,15 @@ export function parsePipeline(rawPayload) {
|
|
|
101
106
|
return null;
|
|
102
107
|
}
|
|
103
108
|
})
|
|
104
|
-
.filter(node => node !== null);
|
|
109
|
+
.filter((node) => node !== null && node !== undefined);
|
|
110
|
+
// Non-parent and non-child nodes
|
|
105
111
|
const filteredNodes = nodes
|
|
106
|
-
.filter(node => childrenNodes.find(childNode => childNode.id === node.id) !== undefined
|
|
107
|
-
|
|
108
|
-
|
|
112
|
+
.filter((node) => childrenNodes.find((childNode) => childNode.id === node.id) !== undefined
|
|
113
|
+
? null
|
|
114
|
+
: node)
|
|
115
|
+
.filter((node) => node !== null);
|
|
116
|
+
// After formatting and filtering set the parent nodes ids to match with children parentIds
|
|
117
|
+
const parentNodes = formattedParentNodes.map((node) => ({
|
|
109
118
|
...node,
|
|
110
119
|
id: `${node.id}-parent`,
|
|
111
120
|
data: {
|
|
@@ -113,14 +122,14 @@ export function parsePipeline(rawPayload) {
|
|
|
113
122
|
id: `${node.data.id}-parent`,
|
|
114
123
|
},
|
|
115
124
|
}));
|
|
116
|
-
const edges = pipeline.connections.map(connection => ({
|
|
125
|
+
const edges = pipeline.connections.map((connection) => ({
|
|
117
126
|
id: `${connection.node1Id}-${connection.node2Id}-${connection.node1Output}-${connection.node2Input}-edge`,
|
|
118
127
|
source: connection.node1Id.toString(),
|
|
119
128
|
target: connection.node2Id.toString(),
|
|
120
129
|
sourceHandle: connection.node1Output,
|
|
121
130
|
targetHandle: connection.node2Input,
|
|
122
131
|
})) ?? [];
|
|
123
|
-
const bridges = pipeline.bridges?.map(bridge => ({
|
|
132
|
+
const bridges = pipeline.bridges?.map((bridge) => ({
|
|
124
133
|
id: `${bridge[0]}-${bridge[1]}-bridge`,
|
|
125
134
|
source: bridge[0].toString(),
|
|
126
135
|
target: bridge[1].toString(),
|
|
@@ -136,9 +145,12 @@ export function parsePipeline(rawPayload) {
|
|
|
136
145
|
type: 'step',
|
|
137
146
|
})) ?? [];
|
|
138
147
|
// NOTE: Parent nodes should be rendered before child nodes
|
|
139
|
-
const groupedNodes = [...
|
|
148
|
+
const groupedNodes = [...parentNodes, ...childrenNodes, ...filteredNodes];
|
|
140
149
|
const nodesWithFilteredHandles = filterNodesHandles(groupedNodes, edges);
|
|
141
|
-
return {
|
|
150
|
+
return {
|
|
151
|
+
nodes: [...nodesWithFilteredHandles],
|
|
152
|
+
edges: [...edges, ...bridges],
|
|
153
|
+
};
|
|
142
154
|
}
|
|
143
155
|
export const DOCS_BASE_URL = `https://docs.luxonis.com/software-v3/depthai/depthai-components/nodes`;
|
|
144
156
|
export const NodesWithLinks = {
|
|
@@ -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
|
+
};
|
package/package.json
CHANGED
|
@@ -1,64 +1,50 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
"overrides": {
|
|
51
|
-
"@radix-ui/react-alert-dialog": "1.0.5"
|
|
52
|
-
},
|
|
53
|
-
"files": [
|
|
54
|
-
"dist/src/*",
|
|
55
|
-
"dist/*.css"
|
|
56
|
-
],
|
|
57
|
-
"exports": {
|
|
58
|
-
".": "./dist/src/index.js",
|
|
59
|
-
"./styles": "./dist/index.css"
|
|
60
|
-
},
|
|
61
|
-
"publishConfig": {
|
|
62
|
-
"access": "public"
|
|
63
|
-
}
|
|
2
|
+
"name": "@luxonis/depthai-pipeline-lib",
|
|
3
|
+
"version": "1.13.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
|
+
"main": "./dist/src/index.js",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "./build.sh",
|
|
9
|
+
"lint": "biome check .",
|
|
10
|
+
"lint:fix": "biome check . --write",
|
|
11
|
+
"gen:styles": "panda codegen",
|
|
12
|
+
"prepublishOnly": "npm run build"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@dagrejs/dagre": "^1.1.3",
|
|
16
|
+
"@luxonis/common-fe-components": "1.25.9",
|
|
17
|
+
"@xyflow/react": "^12.0.4",
|
|
18
|
+
"postcss-import": "^16.1.0",
|
|
19
|
+
"postcss-nested": "^6.2.0",
|
|
20
|
+
"postcss-preset-env": "^10.0.0",
|
|
21
|
+
"rehype-sanitize": "^6.0.0"
|
|
22
|
+
},
|
|
23
|
+
"peerDependencies": {
|
|
24
|
+
"react": "^18.3.1 || ^19.0.0",
|
|
25
|
+
"react-dom": "^18.3.1 || ^19.0.0"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@biomejs/biome": "2.4.6",
|
|
29
|
+
"@pandacss/dev": "^0.53.0",
|
|
30
|
+
"@types/react": "^18.3.1 || ^19.0.0",
|
|
31
|
+
"@types/react-dom": "^18.3.1 || ^19.0.0",
|
|
32
|
+
"postcss": "^8.4.31",
|
|
33
|
+
"prettier": "^3.2.5",
|
|
34
|
+
"typescript": "^5.2.2"
|
|
35
|
+
},
|
|
36
|
+
"overrides": {
|
|
37
|
+
"@radix-ui/react-alert-dialog": "1.0.5"
|
|
38
|
+
},
|
|
39
|
+
"files": [
|
|
40
|
+
"dist/src/*",
|
|
41
|
+
"dist/*.css"
|
|
42
|
+
],
|
|
43
|
+
"exports": {
|
|
44
|
+
".": "./dist/src/index.js",
|
|
45
|
+
"./styles": "./dist/index.css"
|
|
46
|
+
},
|
|
47
|
+
"publishConfig": {
|
|
48
|
+
"access": "public"
|
|
49
|
+
}
|
|
64
50
|
}
|