@luxonis/depthai-pipeline-lib 1.10.0 → 1.12.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 +34 -14
- package/dist/src/components/GroupNode.d.ts +5 -0
- package/dist/src/components/GroupNode.js +19 -0
- package/dist/src/components/Node.d.ts +1 -1
- package/dist/src/components/Node.js +24 -18
- package/dist/src/components/PipelineCanvas.d.ts +1 -1
- package/dist/src/components/PipelineCanvas.js +162 -62
- package/dist/src/index.d.ts +4 -4
- package/dist/src/index.js +1 -1
- package/dist/src/services/pipeline-handles.d.ts +78 -0
- package/dist/src/services/pipeline-handles.js +41 -0
- package/dist/src/services/pipeline.d.ts +1 -10
- package/dist/src/services/pipeline.js +69 -38
- package/package.json +4 -18
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
|
@@ -1252,18 +1252,6 @@
|
|
|
1252
1252
|
|
|
1253
1253
|
@layer utilities{
|
|
1254
1254
|
|
|
1255
|
-
.w_custom\.handle\.dot\! {
|
|
1256
|
-
width: var(--sizes-custom-handle-dot) !important;
|
|
1257
|
-
}
|
|
1258
|
-
|
|
1259
|
-
.h_custom\.handle\.dot\! {
|
|
1260
|
-
height: var(--sizes-custom-handle-dot) !important;
|
|
1261
|
-
}
|
|
1262
|
-
|
|
1263
|
-
.bd_none\! {
|
|
1264
|
-
border: var(--borders-none) !important;
|
|
1265
|
-
}
|
|
1266
|
-
|
|
1267
1255
|
.min-w_container\.smaller\.xxs {
|
|
1268
1256
|
min-width: var(--sizes-container-smaller-xxs);
|
|
1269
1257
|
}
|
|
@@ -1276,6 +1264,30 @@
|
|
|
1276
1264
|
border-radius: var(--radii-common);
|
|
1277
1265
|
}
|
|
1278
1266
|
|
|
1267
|
+
.p_0 {
|
|
1268
|
+
padding: var(--spacing-0);
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
.bdr_0\%\! {
|
|
1272
|
+
border-radius: 0% !important;
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
.bdr_100\%\! {
|
|
1276
|
+
border-radius: 100% !important;
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
.w_custom\.handle\.dot\! {
|
|
1280
|
+
width: var(--sizes-custom-handle-dot) !important;
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
.h_custom\.handle\.dot\! {
|
|
1284
|
+
height: var(--sizes-custom-handle-dot) !important;
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
.bd_none\! {
|
|
1288
|
+
border: var(--borders-none) !important;
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1279
1291
|
.w_full {
|
|
1280
1292
|
width: var(--sizes-full);
|
|
1281
1293
|
}
|
|
@@ -1301,8 +1313,16 @@
|
|
|
1301
1313
|
height: auto;
|
|
1302
1314
|
}
|
|
1303
1315
|
|
|
1304
|
-
.
|
|
1305
|
-
|
|
1316
|
+
.border-style_dashed {
|
|
1317
|
+
border-style: dashed;
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
.bg-c_rgba\(0\,0\,0\,0\.125\) {
|
|
1321
|
+
background-color: rgba(0,0,0,0.125);
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
.bg-c_dark\.error\! {
|
|
1325
|
+
background-color: var(--colors-dark-error) !important;
|
|
1306
1326
|
}
|
|
1307
1327
|
|
|
1308
1328
|
.bg-c_dark\.success\! {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Flex } from '@luxonis/common-fe-components';
|
|
3
|
+
import { css } from '../styled-system/css/index.mjs';
|
|
4
|
+
// eslint-disable-next-line no-warning-comments
|
|
5
|
+
// TODO: Add group node
|
|
6
|
+
export const GroupNode = (props) => {
|
|
7
|
+
const { data: _node } = props;
|
|
8
|
+
return (_jsx(Flex, { direction: "column", className: css({
|
|
9
|
+
minWidth: 'container.smaller.xxs',
|
|
10
|
+
border: 'base',
|
|
11
|
+
rounded: 'common',
|
|
12
|
+
'&:hover .node-help-icon': {
|
|
13
|
+
color: 'text.normal',
|
|
14
|
+
},
|
|
15
|
+
padding: 0,
|
|
16
|
+
borderStyle: 'dashed',
|
|
17
|
+
backgroundColor: 'rgba(0,0,0,0.125)',
|
|
18
|
+
}) }));
|
|
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,39 +1,45 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
2
|
+
import { 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 { css } from '../styled-system/css/css.mjs';
|
|
7
|
+
const NodeHandles = (props) => {
|
|
8
8
|
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 }) =>
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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, }) => {
|
|
10
|
+
if (!connected) {
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
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
|
+
width: 'custom.handle.dot!',
|
|
15
|
+
height: 'custom.handle.dot!',
|
|
16
|
+
backgroundColor: blocking ? 'dark.error!' : 'dark.success!',
|
|
17
|
+
border: 'none!',
|
|
18
|
+
borderRadius: blocking ? '0% !important' : '100% !important',
|
|
19
|
+
}) }), _jsx(NodeHandlesSubLabel, { type: type, name: name, queueSize: queueSize, maxQueueSize: maxQueueSize, fps: fps })] }, id));
|
|
20
|
+
}) }));
|
|
15
21
|
};
|
|
16
|
-
const NodeHandlesSubLabel = props => {
|
|
22
|
+
const NodeHandlesSubLabel = (props) => {
|
|
17
23
|
const { type, name, queueSize, maxQueueSize, fps } = props;
|
|
18
24
|
return (_jsx(SubLabel, { className: css({
|
|
19
25
|
...(type === 'input' //
|
|
20
26
|
? { marginLeft: 'xs' }
|
|
21
27
|
: { marginRight: 'xs' }),
|
|
22
28
|
}), text: type === 'input'
|
|
23
|
-
? `[${fps ? `${fps.toFixed(1)} | ` : ''}${maxQueueSize
|
|
29
|
+
? `[${fps ? `${fps.toFixed(1)} | ` : ''}${`${maxQueueSize ?? 0}/${queueSize}`}] ${name}`
|
|
24
30
|
: `${fps ? `[${fps.toFixed(1)}]` : ''} ${name}`, break: "none" }));
|
|
25
31
|
};
|
|
26
|
-
export const PipelineNode = props => {
|
|
32
|
+
export const PipelineNode = (props) => {
|
|
27
33
|
const { data: node } = props;
|
|
28
|
-
//
|
|
34
|
+
// biome-ignore lint/suspicious/noPrototypeBuiltins: Intended
|
|
29
35
|
const link = NodesWithLinks.hasOwnProperty(node.name)
|
|
30
36
|
? `${DOCS_BASE_URL}/${NodesWithLinks[node.name]}`
|
|
31
37
|
: DOCS_BASE_URL;
|
|
32
38
|
return (_jsxs(Flex, { direction: "column", className: css({
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
39
|
+
minWidth: 'container.smaller.xxs',
|
|
40
|
+
border: 'base',
|
|
41
|
+
rounded: 'common',
|
|
42
|
+
backgroundColor: 'white',
|
|
37
43
|
'&:hover .node-help-icon': {
|
|
38
44
|
color: 'text.normal',
|
|
39
45
|
},
|
|
@@ -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 & {
|
|
@@ -1,95 +1,185 @@
|
|
|
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
6
|
import { PipelineNode } from './Node.js';
|
|
7
7
|
const getLayoutedElements = (nodes, edges) => {
|
|
8
8
|
const graph = new Dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({}));
|
|
9
|
-
graph.setGraph({ rankdir: 'LR' });
|
|
9
|
+
graph.setGraph({ rankdir: 'LR', ranksep: 80, nodesep: 40 });
|
|
10
|
+
const childNodes = nodes.filter((node) => node.type !== 'group');
|
|
10
11
|
for (const edge of edges) {
|
|
11
12
|
graph.setEdge(edge.source, edge.target);
|
|
12
13
|
}
|
|
13
|
-
for (const node of
|
|
14
|
+
for (const node of childNodes) {
|
|
14
15
|
graph.setNode(node.id, {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
height: node.measured?.height ?? 100,
|
|
16
|
+
width: node.measured?.width ?? 300,
|
|
17
|
+
height: node.measured?.height ?? 150,
|
|
18
18
|
});
|
|
19
19
|
}
|
|
20
20
|
Dagre.layout(graph);
|
|
21
21
|
return {
|
|
22
22
|
nodes: nodes.map((node) => {
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
23
|
+
const pos = graph.node(node.id);
|
|
24
|
+
if (!pos)
|
|
25
|
+
return node;
|
|
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
|
+
};
|
|
28
33
|
}),
|
|
29
34
|
edges,
|
|
30
35
|
};
|
|
31
36
|
};
|
|
32
|
-
const adjustNodes = (nodes) => {
|
|
37
|
+
const adjustNodes = (nodes, edges) => {
|
|
33
38
|
const PADDING = 16;
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
39
|
+
// Build a tree: find all group nodes and their depth
|
|
40
|
+
const groupNodes = nodes.filter((node) => node.type === 'group');
|
|
41
|
+
const childNodes = nodes.filter((node) => node.type !== 'group');
|
|
42
|
+
// Sort groups by depth (deepest first) so children are resolved before parents
|
|
43
|
+
const getDepth = (nodeId, depth = 0) => {
|
|
44
|
+
const node = nodes.find((n) => n.id === nodeId);
|
|
45
|
+
if (!node?.parentId)
|
|
46
|
+
return depth;
|
|
47
|
+
return getDepth(node.parentId, depth + 1);
|
|
48
|
+
};
|
|
49
|
+
const sortedGroups = [...groupNodes].sort((a, b) => getDepth(b.id) - getDepth(a.id));
|
|
50
|
+
// Track resolved sizes for groups as we process them
|
|
51
|
+
const resolvedSizes = {};
|
|
52
|
+
const resolvedPositions = {};
|
|
53
|
+
// Start with all non-group nodes at their measured sizes
|
|
54
|
+
for (const node of childNodes) {
|
|
55
|
+
resolvedSizes[node.id] = {
|
|
56
|
+
width: node.measured?.width ?? 300,
|
|
57
|
+
height: node.measured?.height ?? 150,
|
|
43
58
|
};
|
|
44
|
-
}
|
|
45
|
-
const
|
|
46
|
-
const
|
|
47
|
-
|
|
59
|
+
}
|
|
60
|
+
const updatedNodes = {};
|
|
61
|
+
for (const node of nodes)
|
|
62
|
+
updatedNodes[node.id] = { ...node };
|
|
63
|
+
// Process groups deepest-first
|
|
64
|
+
for (const group of sortedGroups) {
|
|
65
|
+
const children = nodes.filter((n) => n.parentId === group.id);
|
|
48
66
|
if (children.length === 0) {
|
|
49
|
-
|
|
67
|
+
resolvedSizes[group.id] = { width: 300, height: 150 };
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
const g = new Dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({}));
|
|
71
|
+
g.setGraph({ rankdir: 'LR', ranksep: 60, nodesep: 20 });
|
|
72
|
+
for (const child of children) {
|
|
73
|
+
// Use resolved size (important for nested groups!)
|
|
74
|
+
const size = resolvedSizes[child.id] ?? { width: 300, height: 150 };
|
|
75
|
+
g.setNode(child.id, { width: size.width, height: size.height });
|
|
50
76
|
}
|
|
77
|
+
const groupEdges = edges.filter((e) => children.some((c) => c.id === e.source) &&
|
|
78
|
+
children.some((c) => c.id === e.target));
|
|
79
|
+
for (const edge of groupEdges) {
|
|
80
|
+
g.setEdge(edge.source, edge.target);
|
|
81
|
+
}
|
|
82
|
+
Dagre.layout(g);
|
|
51
83
|
let minX = 9999;
|
|
52
84
|
let minY = 9999;
|
|
53
85
|
let maxX = -9999;
|
|
54
86
|
let maxY = -9999;
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
const
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
child.
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
87
|
+
for (const child of children) {
|
|
88
|
+
const pos = g.node(child.id);
|
|
89
|
+
const size = resolvedSizes[child.id] ?? { width: 300, height: 150 };
|
|
90
|
+
const x = pos.x - size.width / 2;
|
|
91
|
+
const y = pos.y - size.height / 2;
|
|
92
|
+
minX = Math.min(minX, x);
|
|
93
|
+
minY = Math.min(minY, y);
|
|
94
|
+
maxX = Math.max(maxX, x + size.width);
|
|
95
|
+
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
|
+
}
|
|
106
|
+
// Re-pass to fix positions now that minX/minY are known
|
|
107
|
+
for (const child of children) {
|
|
108
|
+
const pos = g.node(child.id);
|
|
109
|
+
const size = resolvedSizes[child.id] ?? { width: 300, height: 150 };
|
|
110
|
+
updatedNodes[child.id] = {
|
|
111
|
+
...updatedNodes[child.id],
|
|
112
|
+
position: {
|
|
113
|
+
x: pos.x - size.width / 2 - minX + PADDING,
|
|
114
|
+
y: pos.y - size.height / 2 - minY + PADDING,
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
const groupW = maxX - minX + PADDING * 2;
|
|
119
|
+
const groupH = maxY - minY + PADDING * 2;
|
|
120
|
+
// Store this group's resolved size for its own parent to use
|
|
121
|
+
resolvedSizes[group.id] = { width: groupW, height: groupH };
|
|
122
|
+
updatedNodes[group.id] = {
|
|
123
|
+
...updatedNodes[group.id],
|
|
124
|
+
style: { ...updatedNodes[group.id].style, width: groupW, height: groupH },
|
|
125
|
+
width: groupW,
|
|
126
|
+
height: groupH,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
// Now lay out the top-level (root) nodes using Dagre with correct sizes
|
|
130
|
+
const graph = new Dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({}));
|
|
131
|
+
graph.setGraph({ rankdir: 'LR', ranksep: 80, nodesep: 40 });
|
|
132
|
+
const rootNodes = nodes.filter((n) => !n.parentId);
|
|
133
|
+
for (const node of rootNodes) {
|
|
134
|
+
const size = resolvedSizes[node.id] ?? { width: 300, height: 150 };
|
|
135
|
+
graph.setNode(node.id, { width: size.width, height: size.height });
|
|
136
|
+
}
|
|
137
|
+
const rootEdges = edges.filter((e) => rootNodes.some((n) => n.id === e.source) &&
|
|
138
|
+
rootNodes.some((n) => n.id === e.target));
|
|
139
|
+
for (const edge of rootEdges) {
|
|
140
|
+
graph.setEdge(edge.source, edge.target);
|
|
141
|
+
}
|
|
142
|
+
Dagre.layout(graph);
|
|
143
|
+
for (const node of rootNodes) {
|
|
144
|
+
const pos = graph.node(node.id);
|
|
145
|
+
if (!pos)
|
|
146
|
+
continue;
|
|
147
|
+
const size = resolvedSizes[node.id] ?? { width: 300, height: 150 };
|
|
148
|
+
updatedNodes[node.id] = {
|
|
149
|
+
...updatedNodes[node.id],
|
|
150
|
+
position: {
|
|
151
|
+
x: pos.x - size.width / 2,
|
|
152
|
+
y: pos.y - size.height / 2,
|
|
84
153
|
},
|
|
85
154
|
};
|
|
86
|
-
}
|
|
87
|
-
|
|
155
|
+
}
|
|
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
|
+
return topoSort(Object.values(updatedNodes));
|
|
88
178
|
};
|
|
89
179
|
export const updateNodesOnPipelineStateChange = (nodes, pipelineState) => {
|
|
90
180
|
const parsedNodes = [];
|
|
91
181
|
for (const node of nodes) {
|
|
92
|
-
const nodeState = pipelineState.find(state => state.id.toString() === node.id.toString());
|
|
182
|
+
const nodeState = pipelineState.find((state) => state.id.toString() === node.id.toString());
|
|
93
183
|
const inputHandles = node.data.handles.input;
|
|
94
184
|
const outputHandles = node.data.handles.output;
|
|
95
185
|
const newInputHandles = [];
|
|
@@ -133,6 +223,7 @@ const PipelineCanvasBody = ({ pipeline, pipelineState, action, ...flexProps }) =
|
|
|
133
223
|
const reactFlowWidth = useStore(widthSelector);
|
|
134
224
|
const reactFlowHeight = useStore(heightSelector);
|
|
135
225
|
const [shouldFitAndResize, setShouldFitAndResize] = React.useState(false);
|
|
226
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: Intended
|
|
136
227
|
React.useEffect(() => {
|
|
137
228
|
void fitView();
|
|
138
229
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
@@ -144,7 +235,7 @@ const PipelineCanvasBody = ({ pipeline, pipelineState, action, ...flexProps }) =
|
|
|
144
235
|
return;
|
|
145
236
|
}
|
|
146
237
|
const layouted = getLayoutedElements(pipeline?.nodes ?? [], pipeline?.edges ?? []);
|
|
147
|
-
const adjustedNodes = adjustNodes([...layouted.nodes]);
|
|
238
|
+
const adjustedNodes = adjustNodes([...layouted.nodes], [...layouted.edges]);
|
|
148
239
|
if (pipelineState) {
|
|
149
240
|
const updatedNodes = updateNodesOnPipelineStateChange(adjustedNodes, pipelineState);
|
|
150
241
|
setNodes([...updatedNodes]);
|
|
@@ -155,10 +246,17 @@ const PipelineCanvasBody = ({ pipeline, pipelineState, action, ...flexProps }) =
|
|
|
155
246
|
setEdges([...layouted.edges]);
|
|
156
247
|
setShouldFitAndResize(true);
|
|
157
248
|
}, [pipeline?.edges, pipeline?.nodes, setEdges, setNodes, pipelineState]);
|
|
249
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: Intended
|
|
158
250
|
React.useEffect(() => {
|
|
159
251
|
if (pipelineState && nodes.length > 0) {
|
|
160
252
|
const updatedNodes = updateNodesOnPipelineStateChange(nodes, pipelineState);
|
|
161
253
|
setNodes([...updatedNodes]);
|
|
254
|
+
if (!autoArrangeRef.current) {
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
else {
|
|
258
|
+
setShouldFitAndResize(true);
|
|
259
|
+
}
|
|
162
260
|
}
|
|
163
261
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
164
262
|
}, [pipelineState]);
|
|
@@ -166,6 +264,7 @@ const PipelineCanvasBody = ({ pipeline, pipelineState, action, ...flexProps }) =
|
|
|
166
264
|
await setViewport({ x, y, zoom });
|
|
167
265
|
await fitView({ nodes: nds });
|
|
168
266
|
};
|
|
267
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: Intended
|
|
169
268
|
React.useEffect(() => {
|
|
170
269
|
if (!autoArrangeRef.current) {
|
|
171
270
|
return;
|
|
@@ -176,6 +275,7 @@ const PipelineCanvasBody = ({ pipeline, pipelineState, action, ...flexProps }) =
|
|
|
176
275
|
}
|
|
177
276
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
178
277
|
}, [nodes, edges]);
|
|
278
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: Intended
|
|
179
279
|
React.useEffect(() => {
|
|
180
280
|
if (shouldFitAndResize) {
|
|
181
281
|
const viewport = getViewport();
|
|
@@ -186,4 +286,4 @@ const PipelineCanvasBody = ({ pipeline, pipelineState, action, ...flexProps }) =
|
|
|
186
286
|
}, [shouldFitAndResize]);
|
|
187
287
|
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 }) }))] }));
|
|
188
288
|
};
|
|
189
|
-
export const PipelineCanvas = props => (_jsx(ReactFlowProvider, { children: _jsx(PipelineCanvasBody, { ...props }) }));
|
|
289
|
+
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,78 @@
|
|
|
1
|
+
import type { Edge } from '@xyflow/react';
|
|
2
|
+
import type { ParsedNode, RawPipelineNodeIO } from './pipeline';
|
|
3
|
+
export type RawPipelineBridge = [number, number];
|
|
4
|
+
export type ParsedHandle = {
|
|
5
|
+
id: number;
|
|
6
|
+
name: string;
|
|
7
|
+
type: 'input' | 'output';
|
|
8
|
+
blocking: boolean;
|
|
9
|
+
queueSize: number;
|
|
10
|
+
connected: boolean;
|
|
11
|
+
maxQueueSize?: number;
|
|
12
|
+
fps?: number;
|
|
13
|
+
};
|
|
14
|
+
export declare function parseHandles(handles: RawPipelineNodeIO[]): {
|
|
15
|
+
input: ParsedHandle[];
|
|
16
|
+
output: ParsedHandle[];
|
|
17
|
+
};
|
|
18
|
+
export declare function filterNodesHandles(nodes: ParsedNode[], edges: Edge[]): {
|
|
19
|
+
data: {
|
|
20
|
+
handles: {
|
|
21
|
+
input: {
|
|
22
|
+
connected: boolean;
|
|
23
|
+
id: number;
|
|
24
|
+
name: string;
|
|
25
|
+
type: "input" | "output";
|
|
26
|
+
blocking: boolean;
|
|
27
|
+
queueSize: number;
|
|
28
|
+
maxQueueSize?: number;
|
|
29
|
+
fps?: number;
|
|
30
|
+
}[];
|
|
31
|
+
output: {
|
|
32
|
+
connected: boolean;
|
|
33
|
+
id: number;
|
|
34
|
+
name: string;
|
|
35
|
+
type: "input" | "output";
|
|
36
|
+
blocking: boolean;
|
|
37
|
+
queueSize: number;
|
|
38
|
+
maxQueueSize?: number;
|
|
39
|
+
fps?: number;
|
|
40
|
+
}[];
|
|
41
|
+
};
|
|
42
|
+
id: string;
|
|
43
|
+
parentId?: string;
|
|
44
|
+
name: string;
|
|
45
|
+
};
|
|
46
|
+
id: string;
|
|
47
|
+
position: import("@xyflow/system").XYPosition;
|
|
48
|
+
type?: string | undefined;
|
|
49
|
+
sourcePosition?: import("@xyflow/system").Position;
|
|
50
|
+
targetPosition?: import("@xyflow/system").Position;
|
|
51
|
+
hidden?: boolean;
|
|
52
|
+
selected?: boolean;
|
|
53
|
+
dragging?: boolean;
|
|
54
|
+
draggable?: boolean;
|
|
55
|
+
selectable?: boolean;
|
|
56
|
+
connectable?: boolean;
|
|
57
|
+
deletable?: boolean;
|
|
58
|
+
dragHandle?: string;
|
|
59
|
+
width?: number;
|
|
60
|
+
height?: number;
|
|
61
|
+
initialWidth?: number;
|
|
62
|
+
initialHeight?: number;
|
|
63
|
+
parentId?: string;
|
|
64
|
+
zIndex?: number;
|
|
65
|
+
extent?: "parent" | import("@xyflow/system").CoordinateExtent;
|
|
66
|
+
expandParent?: boolean;
|
|
67
|
+
ariaLabel?: string;
|
|
68
|
+
origin?: import("@xyflow/system").NodeOrigin;
|
|
69
|
+
handles?: import("@xyflow/system").NodeHandle[];
|
|
70
|
+
measured?: {
|
|
71
|
+
width?: number;
|
|
72
|
+
height?: number;
|
|
73
|
+
};
|
|
74
|
+
style?: import("react").CSSProperties;
|
|
75
|
+
className?: string;
|
|
76
|
+
resizing?: boolean;
|
|
77
|
+
focusable?: boolean;
|
|
78
|
+
}[];
|
|
@@ -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,4 +1,5 @@
|
|
|
1
1
|
import { type Edge, type Node } from '@xyflow/react';
|
|
2
|
+
import { type ParsedHandle } from './pipeline-handles';
|
|
2
3
|
export type RawPipelineNodeIO = [
|
|
3
4
|
[
|
|
4
5
|
string,
|
|
@@ -18,7 +19,6 @@ export type RawPipelineNode = {
|
|
|
18
19
|
name: string;
|
|
19
20
|
parentId?: number;
|
|
20
21
|
};
|
|
21
|
-
export type RawPipelineBridge = [number, number];
|
|
22
22
|
export type RawPipelineEdge = {
|
|
23
23
|
node1Id: number;
|
|
24
24
|
node1Output: string;
|
|
@@ -37,15 +37,6 @@ export type Pipeline = {
|
|
|
37
37
|
nodes: ParsedNode[];
|
|
38
38
|
edges: Edge[];
|
|
39
39
|
};
|
|
40
|
-
export type ParsedHandle = {
|
|
41
|
-
id: number;
|
|
42
|
-
name: string;
|
|
43
|
-
type: 'input' | 'output';
|
|
44
|
-
blocking: boolean;
|
|
45
|
-
queueSize: number;
|
|
46
|
-
maxQueueSize?: number;
|
|
47
|
-
fps?: number;
|
|
48
|
-
};
|
|
49
40
|
export type ParsedNode = Node<{
|
|
50
41
|
id: string;
|
|
51
42
|
parentId?: string;
|
|
@@ -1,51 +1,73 @@
|
|
|
1
|
-
import { MarkerType } from '@xyflow/react';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
+
],
|
|
21
|
+
},
|
|
22
|
+
];
|
|
19
23
|
}
|
|
20
24
|
export function parsePipeline(rawPayload) {
|
|
21
25
|
const { pipeline } = rawPayload;
|
|
22
26
|
// Set all nodes as generic nodes
|
|
23
27
|
const nodes = pipeline.nodes
|
|
24
|
-
.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))
|
|
25
29
|
.map(([id, node]) => ({
|
|
26
30
|
type: 'generic',
|
|
27
31
|
id: id.toString(),
|
|
28
32
|
position: { x: 0, y: 0 },
|
|
29
|
-
parentId: node.parentId?.toString() === '-1'
|
|
30
|
-
|
|
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',
|
|
31
39
|
data: {
|
|
32
40
|
id: id.toString(),
|
|
33
41
|
name: node.name,
|
|
34
42
|
handles: parseHandles(node.ioInfo),
|
|
35
43
|
},
|
|
36
44
|
})) ?? [];
|
|
45
|
+
// Check for parent nodes and if there is children with some parent node that doesn't exist the create it as fake node
|
|
46
|
+
const parentNodesIdsRaw = [
|
|
47
|
+
...new Set(nodes
|
|
48
|
+
.map((node) => {
|
|
49
|
+
if (node.parentId !== undefined && node.parentId !== '-1') {
|
|
50
|
+
return node.parentId;
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
.filter((id) => id !== null)),
|
|
57
|
+
];
|
|
58
|
+
const parentNodesArr = parentNodesIdsRaw.map((id) => pipeline.nodes.find(([_, node]) => node.id.toString() === id) ??
|
|
59
|
+
addFakeNode(id));
|
|
37
60
|
// Set all parent nodes as group nodes
|
|
38
|
-
const mappedParentNodes = nodes.map(node => node.parentId).filter(id => id !== undefined);
|
|
39
|
-
const filteredParentNodes = mappedParentNodes.filter(id => id !== '-1');
|
|
40
|
-
const parentNodes = [...new Set(filteredParentNodes)];
|
|
41
|
-
const parentNodesArr = parentNodes
|
|
42
|
-
.map(id => pipeline.nodes.find(([_, node]) => node.id.toString() === id))
|
|
43
|
-
.filter(node => node !== undefined);
|
|
44
61
|
const formattedParentNodes = parentNodesArr.map(([id, node]) => ({
|
|
45
62
|
type: 'group',
|
|
46
63
|
id: id.toString(),
|
|
47
64
|
position: { x: 0, y: 0 },
|
|
48
|
-
parentId: node.parentId?.toString() === '-1'
|
|
65
|
+
parentId: node.parentId?.toString() === '-1' || node.parentId === undefined
|
|
66
|
+
? undefined
|
|
67
|
+
: `${node.parentId?.toString()}-parent`,
|
|
68
|
+
extent: node.parentId?.toString() === '-1' || node.parentId === undefined
|
|
69
|
+
? undefined
|
|
70
|
+
: 'parent',
|
|
49
71
|
data: {
|
|
50
72
|
id: id.toString(),
|
|
51
73
|
name: node.name,
|
|
@@ -57,9 +79,10 @@ export function parsePipeline(rawPayload) {
|
|
|
57
79
|
background: 'rgba(0,0,0,0.125)',
|
|
58
80
|
},
|
|
59
81
|
})) ?? [];
|
|
60
|
-
|
|
82
|
+
// Children nodes
|
|
83
|
+
const parentNodesIds = parentNodesIdsRaw.map((id) => id.toString());
|
|
61
84
|
const childrenNodes = nodes
|
|
62
|
-
.map(node => {
|
|
85
|
+
.map((node) => {
|
|
63
86
|
if (node.parentId && parentNodesIds.includes(node.parentId)) {
|
|
64
87
|
return {
|
|
65
88
|
...node,
|
|
@@ -78,11 +101,15 @@ export function parsePipeline(rawPayload) {
|
|
|
78
101
|
return null;
|
|
79
102
|
}
|
|
80
103
|
})
|
|
81
|
-
.filter(node => node !== null);
|
|
104
|
+
.filter((node) => node !== null && node !== undefined);
|
|
105
|
+
// Non-parent and non-child nodes
|
|
82
106
|
const filteredNodes = nodes
|
|
83
|
-
.filter(node => childrenNodes.find(childNode => childNode.id === node.id) !== undefined
|
|
84
|
-
|
|
85
|
-
|
|
107
|
+
.filter((node) => childrenNodes.find((childNode) => childNode.id === node.id) !== undefined
|
|
108
|
+
? null
|
|
109
|
+
: node)
|
|
110
|
+
.filter((node) => node !== null);
|
|
111
|
+
// After formatting and filtering set the parent nodes ids to match with children parentIds
|
|
112
|
+
const parentNodes = formattedParentNodes.map((node) => ({
|
|
86
113
|
...node,
|
|
87
114
|
id: `${node.id}-parent`,
|
|
88
115
|
data: {
|
|
@@ -90,14 +117,14 @@ export function parsePipeline(rawPayload) {
|
|
|
90
117
|
id: `${node.data.id}-parent`,
|
|
91
118
|
},
|
|
92
119
|
}));
|
|
93
|
-
const edges = pipeline.connections.map(connection => ({
|
|
120
|
+
const edges = pipeline.connections.map((connection) => ({
|
|
94
121
|
id: `${connection.node1Id}-${connection.node2Id}-${connection.node1Output}-${connection.node2Input}-edge`,
|
|
95
122
|
source: connection.node1Id.toString(),
|
|
96
123
|
target: connection.node2Id.toString(),
|
|
97
124
|
sourceHandle: connection.node1Output,
|
|
98
125
|
targetHandle: connection.node2Input,
|
|
99
126
|
})) ?? [];
|
|
100
|
-
const bridges = pipeline.bridges?.map(bridge => ({
|
|
127
|
+
const bridges = pipeline.bridges?.map((bridge) => ({
|
|
101
128
|
id: `${bridge[0]}-${bridge[1]}-bridge`,
|
|
102
129
|
source: bridge[0].toString(),
|
|
103
130
|
target: bridge[1].toString(),
|
|
@@ -113,8 +140,12 @@ export function parsePipeline(rawPayload) {
|
|
|
113
140
|
type: 'step',
|
|
114
141
|
})) ?? [];
|
|
115
142
|
// NOTE: Parent nodes should be rendered before child nodes
|
|
116
|
-
const groupedNodes = [...
|
|
117
|
-
|
|
143
|
+
const groupedNodes = [...parentNodes, ...childrenNodes, ...filteredNodes];
|
|
144
|
+
const nodesWithFilteredHandles = filterNodesHandles(groupedNodes, edges);
|
|
145
|
+
return {
|
|
146
|
+
nodes: [...nodesWithFilteredHandles],
|
|
147
|
+
edges: [...edges, ...bridges],
|
|
148
|
+
};
|
|
118
149
|
}
|
|
119
150
|
export const DOCS_BASE_URL = `https://docs.luxonis.com/software-v3/depthai/depthai-components/nodes`;
|
|
120
151
|
export const NodesWithLinks = {
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@luxonis/depthai-pipeline-lib",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.12.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"main": "./dist/src/index.js",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"build": "./build.sh",
|
|
9
|
-
"lint": "
|
|
9
|
+
"lint": "biome check .",
|
|
10
|
+
"lint:fix": "biome check . --write",
|
|
10
11
|
"gen:styles": "panda codegen",
|
|
11
12
|
"prepublishOnly": "npm run build"
|
|
12
13
|
},
|
|
@@ -24,27 +25,12 @@
|
|
|
24
25
|
"react-dom": "^18.3.1 || ^19.0.0"
|
|
25
26
|
},
|
|
26
27
|
"devDependencies": {
|
|
28
|
+
"@biomejs/biome": "2.4.6",
|
|
27
29
|
"@pandacss/dev": "^0.53.0",
|
|
28
|
-
"@stylistic/eslint-plugin": "^2.6.1",
|
|
29
30
|
"@types/react": "^18.3.1 || ^19.0.0",
|
|
30
31
|
"@types/react-dom": "^18.3.1 || ^19.0.0",
|
|
31
|
-
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
|
32
|
-
"@typescript-eslint/parser": "^6.21.0",
|
|
33
|
-
"eslint": "^8.56.0",
|
|
34
|
-
"eslint-config-prettier": "^9.1.0",
|
|
35
|
-
"eslint-import-resolver-typescript": "^3.6.1",
|
|
36
|
-
"eslint-interactive": "^10.8.0",
|
|
37
|
-
"eslint-plugin-cypress": "^3.3.0",
|
|
38
|
-
"eslint-plugin-functional": "^6.0.0",
|
|
39
|
-
"eslint-plugin-github": "^4.10.1",
|
|
40
|
-
"eslint-plugin-import": "^2.29.1",
|
|
41
|
-
"eslint-plugin-prettier": "^5.1.3",
|
|
42
|
-
"eslint-plugin-react": "^7.33.2",
|
|
43
|
-
"eslint-plugin-react-hooks": "^4.6.0",
|
|
44
|
-
"eslint-plugin-react-refresh": "^0.4.5",
|
|
45
32
|
"postcss": "^8.4.31",
|
|
46
33
|
"prettier": "^3.2.5",
|
|
47
|
-
"prettier-eslint": "^16.3.0",
|
|
48
34
|
"typescript": "^5.2.2"
|
|
49
35
|
},
|
|
50
36
|
"overrides": {
|