@luxonis/depthai-pipeline-lib 1.11.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/src/components/GroupNode.js +6 -6
- package/dist/src/components/Node.d.ts +1 -1
- package/dist/src/components/Node.js +12 -12
- package/dist/src/components/PipelineCanvas.d.ts +1 -1
- package/dist/src/components/PipelineCanvas.js +124 -75
- 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 -11
- package/dist/src/services/pipeline.js +67 -60
- 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");
|
|
@@ -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,12 +1,12 @@
|
|
|
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, connected }) => {
|
|
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
10
|
if (!connected) {
|
|
11
11
|
return;
|
|
12
12
|
}
|
|
@@ -19,7 +19,7 @@ const NodeHandles = props => {
|
|
|
19
19
|
}) }), _jsx(NodeHandlesSubLabel, { type: type, name: name, queueSize: queueSize, maxQueueSize: maxQueueSize, fps: fps })] }, id));
|
|
20
20
|
}) }));
|
|
21
21
|
};
|
|
22
|
-
const NodeHandlesSubLabel = props => {
|
|
22
|
+
const NodeHandlesSubLabel = (props) => {
|
|
23
23
|
const { type, name, queueSize, maxQueueSize, fps } = props;
|
|
24
24
|
return (_jsx(SubLabel, { className: css({
|
|
25
25
|
...(type === 'input' //
|
|
@@ -29,17 +29,17 @@ const NodeHandlesSubLabel = props => {
|
|
|
29
29
|
? `[${fps ? `${fps.toFixed(1)} | ` : ''}${`${maxQueueSize ?? 0}/${queueSize}`}] ${name}`
|
|
30
30
|
: `${fps ? `[${fps.toFixed(1)}]` : ''} ${name}`, break: "none" }));
|
|
31
31
|
};
|
|
32
|
-
export const PipelineNode = props => {
|
|
32
|
+
export const PipelineNode = (props) => {
|
|
33
33
|
const { data: node } = props;
|
|
34
|
-
//
|
|
34
|
+
// biome-ignore lint/suspicious/noPrototypeBuiltins: Intended
|
|
35
35
|
const link = NodesWithLinks.hasOwnProperty(node.name)
|
|
36
36
|
? `${DOCS_BASE_URL}/${NodesWithLinks[node.name]}`
|
|
37
37
|
: DOCS_BASE_URL;
|
|
38
38
|
return (_jsxs(Flex, { direction: "column", className: css({
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
39
|
+
minWidth: 'container.smaller.xxs',
|
|
40
|
+
border: 'base',
|
|
41
|
+
rounded: 'common',
|
|
42
|
+
backgroundColor: 'white',
|
|
43
43
|
'&:hover .node-help-icon': {
|
|
44
44
|
color: 'text.normal',
|
|
45
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,13 +1,13 @@
|
|
|
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
9
|
graph.setGraph({ rankdir: 'LR', ranksep: 80, nodesep: 40 });
|
|
10
|
-
const childNodes = nodes.filter(node => node.type !== 'group');
|
|
10
|
+
const childNodes = nodes.filter((node) => node.type !== 'group');
|
|
11
11
|
for (const edge of edges) {
|
|
12
12
|
graph.setEdge(edge.source, edge.target);
|
|
13
13
|
}
|
|
@@ -19,7 +19,7 @@ const getLayoutedElements = (nodes, edges) => {
|
|
|
19
19
|
}
|
|
20
20
|
Dagre.layout(graph);
|
|
21
21
|
return {
|
|
22
|
-
nodes: nodes.map(node => {
|
|
22
|
+
nodes: nodes.map((node) => {
|
|
23
23
|
const pos = graph.node(node.id);
|
|
24
24
|
if (!pos)
|
|
25
25
|
return node;
|
|
@@ -36,44 +36,46 @@ const getLayoutedElements = (nodes, edges) => {
|
|
|
36
36
|
};
|
|
37
37
|
const adjustNodes = (nodes, edges) => {
|
|
38
38
|
const PADDING = 16;
|
|
39
|
-
|
|
40
|
-
const
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
|
46
54
|
for (const node of childNodes) {
|
|
47
|
-
|
|
55
|
+
resolvedSizes[node.id] = {
|
|
48
56
|
width: node.measured?.width ?? 300,
|
|
49
57
|
height: node.measured?.height ?? 150,
|
|
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
|
-
},
|
|
61
58
|
};
|
|
62
|
-
}
|
|
63
|
-
const
|
|
64
|
-
|
|
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);
|
|
65
66
|
if (children.length === 0) {
|
|
66
|
-
|
|
67
|
+
resolvedSizes[group.id] = { width: 300, height: 150 };
|
|
68
|
+
continue;
|
|
67
69
|
}
|
|
68
70
|
const g = new Dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({}));
|
|
69
71
|
g.setGraph({ rankdir: 'LR', ranksep: 60, nodesep: 20 });
|
|
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] ?? { width: 300, height: 150 };
|
|
75
|
+
g.setNode(child.id, { width: size.width, height: size.height });
|
|
75
76
|
}
|
|
76
|
-
const groupEdges = edges.filter(e => children.some(c => c.id === e.source) &&
|
|
77
|
+
const groupEdges = edges.filter((e) => children.some((c) => c.id === e.source) &&
|
|
78
|
+
children.some((c) => c.id === e.target));
|
|
77
79
|
for (const edge of groupEdges) {
|
|
78
80
|
g.setEdge(edge.source, edge.target);
|
|
79
81
|
}
|
|
@@ -82,59 +84,102 @@ const adjustNodes = (nodes, edges) => {
|
|
|
82
84
|
let minY = 9999;
|
|
83
85
|
let maxX = -9999;
|
|
84
86
|
let maxY = -9999;
|
|
85
|
-
const
|
|
87
|
+
for (const child of children) {
|
|
86
88
|
const pos = g.node(child.id);
|
|
87
|
-
const
|
|
88
|
-
const
|
|
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;
|
|
89
92
|
minX = Math.min(minX, x);
|
|
90
93
|
minY = Math.min(minY, y);
|
|
91
|
-
maxX = Math.max(maxX, x +
|
|
92
|
-
maxY = Math.max(maxY, y +
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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);
|
|
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
|
+
};
|
|
110
105
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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],
|
|
115
112
|
position: {
|
|
116
|
-
x:
|
|
117
|
-
y:
|
|
113
|
+
x: pos.x - size.width / 2 - minX + PADDING,
|
|
114
|
+
y: pos.y - size.height / 2 - minY + PADDING,
|
|
118
115
|
},
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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,
|
|
122
153
|
},
|
|
123
154
|
};
|
|
124
|
-
}
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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));
|
|
133
178
|
};
|
|
134
179
|
export const updateNodesOnPipelineStateChange = (nodes, pipelineState) => {
|
|
135
180
|
const parsedNodes = [];
|
|
136
181
|
for (const node of nodes) {
|
|
137
|
-
const nodeState = pipelineState.find(state => state.id.toString() === node.id.toString());
|
|
182
|
+
const nodeState = pipelineState.find((state) => state.id.toString() === node.id.toString());
|
|
138
183
|
const inputHandles = node.data.handles.input;
|
|
139
184
|
const outputHandles = node.data.handles.output;
|
|
140
185
|
const newInputHandles = [];
|
|
@@ -178,6 +223,7 @@ const PipelineCanvasBody = ({ pipeline, pipelineState, action, ...flexProps }) =
|
|
|
178
223
|
const reactFlowWidth = useStore(widthSelector);
|
|
179
224
|
const reactFlowHeight = useStore(heightSelector);
|
|
180
225
|
const [shouldFitAndResize, setShouldFitAndResize] = React.useState(false);
|
|
226
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: Intended
|
|
181
227
|
React.useEffect(() => {
|
|
182
228
|
void fitView();
|
|
183
229
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
@@ -200,6 +246,7 @@ const PipelineCanvasBody = ({ pipeline, pipelineState, action, ...flexProps }) =
|
|
|
200
246
|
setEdges([...layouted.edges]);
|
|
201
247
|
setShouldFitAndResize(true);
|
|
202
248
|
}, [pipeline?.edges, pipeline?.nodes, setEdges, setNodes, pipelineState]);
|
|
249
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: Intended
|
|
203
250
|
React.useEffect(() => {
|
|
204
251
|
if (pipelineState && nodes.length > 0) {
|
|
205
252
|
const updatedNodes = updateNodesOnPipelineStateChange(nodes, pipelineState);
|
|
@@ -217,6 +264,7 @@ const PipelineCanvasBody = ({ pipeline, pipelineState, action, ...flexProps }) =
|
|
|
217
264
|
await setViewport({ x, y, zoom });
|
|
218
265
|
await fitView({ nodes: nds });
|
|
219
266
|
};
|
|
267
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: Intended
|
|
220
268
|
React.useEffect(() => {
|
|
221
269
|
if (!autoArrangeRef.current) {
|
|
222
270
|
return;
|
|
@@ -227,6 +275,7 @@ const PipelineCanvasBody = ({ pipeline, pipelineState, action, ...flexProps }) =
|
|
|
227
275
|
}
|
|
228
276
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
229
277
|
}, [nodes, edges]);
|
|
278
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: Intended
|
|
230
279
|
React.useEffect(() => {
|
|
231
280
|
if (shouldFitAndResize) {
|
|
232
281
|
const viewport = getViewport();
|
|
@@ -237,4 +286,4 @@ const PipelineCanvasBody = ({ pipeline, pipelineState, action, ...flexProps }) =
|
|
|
237
286
|
}, [shouldFitAndResize]);
|
|
238
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 }) }))] }));
|
|
239
288
|
};
|
|
240
|
-
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,16 +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
|
-
connected: boolean;
|
|
47
|
-
maxQueueSize?: number;
|
|
48
|
-
fps?: number;
|
|
49
|
-
};
|
|
50
40
|
export type ParsedNode = Node<{
|
|
51
41
|
id: string;
|
|
52
42
|
parentId?: string;
|
|
@@ -1,74 +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
|
-
|
|
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),
|
|
58
43
|
},
|
|
59
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));
|
|
60
60
|
// 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
61
|
const formattedParentNodes = parentNodesArr.map(([id, node]) => ({
|
|
68
62
|
type: 'group',
|
|
69
63
|
id: id.toString(),
|
|
70
64
|
position: { x: 0, y: 0 },
|
|
71
|
-
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',
|
|
72
71
|
data: {
|
|
73
72
|
id: id.toString(),
|
|
74
73
|
name: node.name,
|
|
@@ -80,9 +79,10 @@ export function parsePipeline(rawPayload) {
|
|
|
80
79
|
background: 'rgba(0,0,0,0.125)',
|
|
81
80
|
},
|
|
82
81
|
})) ?? [];
|
|
83
|
-
|
|
82
|
+
// Children nodes
|
|
83
|
+
const parentNodesIds = parentNodesIdsRaw.map((id) => id.toString());
|
|
84
84
|
const childrenNodes = nodes
|
|
85
|
-
.map(node => {
|
|
85
|
+
.map((node) => {
|
|
86
86
|
if (node.parentId && parentNodesIds.includes(node.parentId)) {
|
|
87
87
|
return {
|
|
88
88
|
...node,
|
|
@@ -101,11 +101,15 @@ export function parsePipeline(rawPayload) {
|
|
|
101
101
|
return null;
|
|
102
102
|
}
|
|
103
103
|
})
|
|
104
|
-
.filter(node => node !== null);
|
|
104
|
+
.filter((node) => node !== null && node !== undefined);
|
|
105
|
+
// Non-parent and non-child nodes
|
|
105
106
|
const filteredNodes = nodes
|
|
106
|
-
.filter(node => childrenNodes.find(childNode => childNode.id === node.id) !== undefined
|
|
107
|
-
|
|
108
|
-
|
|
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) => ({
|
|
109
113
|
...node,
|
|
110
114
|
id: `${node.id}-parent`,
|
|
111
115
|
data: {
|
|
@@ -113,14 +117,14 @@ export function parsePipeline(rawPayload) {
|
|
|
113
117
|
id: `${node.data.id}-parent`,
|
|
114
118
|
},
|
|
115
119
|
}));
|
|
116
|
-
const edges = pipeline.connections.map(connection => ({
|
|
120
|
+
const edges = pipeline.connections.map((connection) => ({
|
|
117
121
|
id: `${connection.node1Id}-${connection.node2Id}-${connection.node1Output}-${connection.node2Input}-edge`,
|
|
118
122
|
source: connection.node1Id.toString(),
|
|
119
123
|
target: connection.node2Id.toString(),
|
|
120
124
|
sourceHandle: connection.node1Output,
|
|
121
125
|
targetHandle: connection.node2Input,
|
|
122
126
|
})) ?? [];
|
|
123
|
-
const bridges = pipeline.bridges?.map(bridge => ({
|
|
127
|
+
const bridges = pipeline.bridges?.map((bridge) => ({
|
|
124
128
|
id: `${bridge[0]}-${bridge[1]}-bridge`,
|
|
125
129
|
source: bridge[0].toString(),
|
|
126
130
|
target: bridge[1].toString(),
|
|
@@ -136,9 +140,12 @@ export function parsePipeline(rawPayload) {
|
|
|
136
140
|
type: 'step',
|
|
137
141
|
})) ?? [];
|
|
138
142
|
// NOTE: Parent nodes should be rendered before child nodes
|
|
139
|
-
const groupedNodes = [...
|
|
143
|
+
const groupedNodes = [...parentNodes, ...childrenNodes, ...filteredNodes];
|
|
140
144
|
const nodesWithFilteredHandles = filterNodesHandles(groupedNodes, edges);
|
|
141
|
-
return {
|
|
145
|
+
return {
|
|
146
|
+
nodes: [...nodesWithFilteredHandles],
|
|
147
|
+
edges: [...edges, ...bridges],
|
|
148
|
+
};
|
|
142
149
|
}
|
|
143
150
|
export const DOCS_BASE_URL = `https://docs.luxonis.com/software-v3/depthai/depthai-components/nodes`;
|
|
144
151
|
export const NodesWithLinks = {
|
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.12.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
|
}
|