@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 CHANGED
@@ -1,3 +1,3 @@
1
- @import url('@xyflow/react/dist/style.css');
2
- @import url('@luxonis/common-fe-components/styles');
3
- @import url('./panda.css');
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
- .bg-c_dark\.warning\! {
1305
- background-color: var(--colors-dark-warning) !important;
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,5 @@
1
+ import type { NodeProps } from '@xyflow/react';
2
+ import type { ParsedNode } from '../services/pipeline';
3
+ type GroupNodeProps = NodeProps<ParsedNode>;
4
+ export declare const GroupNode: React.FC<GroupNodeProps>;
5
+ export {};
@@ -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 { Handle, Position } from '@xyflow/react';
2
+ import { Button, Flex, HelpIcon, Label, SubLabel, } from '@luxonis/common-fe-components';
3
3
  import { clsx } from '@luxonis/common-fe-components/helpers';
4
- import { Button, Flex, HelpIcon, Label, SubLabel } from '@luxonis/common-fe-components';
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
- const NodeHandles = props => {
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 }) => (_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({
10
- width: 'custom.handle.dot!',
11
- height: 'custom.handle.dot!',
12
- backgroundColor: blocking ? 'dark.warning!' : 'dark.success!',
13
- border: 'none!',
14
- }) }), _jsx(NodeHandlesSubLabel, { type: type, name: name, queueSize: queueSize, maxQueueSize: maxQueueSize, fps: fps })] }, id))) }));
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 ? `${maxQueueSize}/${queueSize}` : queueSize}] ${name}`
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
- // eslint-disable-next-line no-prototype-builtins
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
- 'minWidth': 'container.smaller.xxs',
34
- 'border': 'base',
35
- 'rounded': 'common',
36
- 'backgroundColor': 'white',
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 nodes) {
14
+ for (const node of childNodes) {
14
15
  graph.setNode(node.id, {
15
- ...node,
16
- width: node.measured?.width ?? 200,
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 position = graph.node(node.id);
24
- // Shift Dagre anchor (center) to React Flow anchor (top left)
25
- const x = position.x - (node.measured?.width ?? 200) / 2;
26
- const y = position.y - (node.measured?.height ?? 100) / 2;
27
- return { ...node, position: { x, y } };
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
- const positionedNodes = nodes
35
- .filter(node => node.type !== 'group')
36
- .map((node) => {
37
- const position = node.position;
38
- const x = position.x - (node.measured?.width ?? 200) / 2;
39
- const y = position.y - (node.measured?.height ?? 100) / 2;
40
- return {
41
- ...node,
42
- position: { x, y },
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 groupNodes = nodes.filter(node => node.type === 'group');
46
- const positionedGroups = groupNodes.map(parent => {
47
- const children = positionedNodes.filter(n => n.parentId === parent.id);
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
- return { ...parent, position: { x: 0, y: 0 } };
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
- // eslint-disable-next-line github/array-foreach
56
- children.forEach(child => {
57
- const width = child.measured?.width ?? 200;
58
- const height = child.measured?.height ?? 100;
59
- minX = Math.min(minX, child.position.x);
60
- minY = Math.min(minY, child.position.y);
61
- maxX = Math.max(maxX, child.position.x + width);
62
- maxY = Math.max(maxY, child.position.y + height);
63
- });
64
- const parentX = minX - PADDING;
65
- const parentY = minY - PADDING;
66
- // eslint-disable-next-line github/array-foreach
67
- children.forEach(child => {
68
- child.position.x -= parentX;
69
- child.position.y -= parentY;
70
- });
71
- return {
72
- ...parent,
73
- position: { x: parentX, y: parentY },
74
- measured: {
75
- width: maxX - minX + PADDING * 2,
76
- height: maxY - minY + PADDING * 2,
77
- },
78
- width: maxX - minX + PADDING * 2,
79
- height: maxY - minY + PADDING * 2,
80
- style: {
81
- ...parent.style,
82
- width: maxX - minX + PADDING * 2,
83
- height: maxY - minY + PADDING * 2,
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
- return [...positionedGroups, ...positionedNodes];
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 }) }));
@@ -1,6 +1,6 @@
1
- export { parsePipeline } from './services/pipeline.js';
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
@@ -1,3 +1,3 @@
1
+ export { PipelineCanvas } from './components/PipelineCanvas.js';
1
2
  export { parsePipeline } from './services/pipeline.js';
2
3
  export { parsePipelineState } from './services/pipeline-state.js';
3
- export { PipelineCanvas } from './components/PipelineCanvas.js';
@@ -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
- function parseHandles(handles) {
3
- const parsedHandles = {
4
- input: [],
5
- output: [],
6
- };
7
- for (const [_, handle] of handles) {
8
- const type = handle.type === 3 ? 'input' : 'output';
9
- const parsed = {
10
- id: handle.id,
11
- name: handle.name,
12
- type,
13
- blocking: handle.blocking,
14
- queueSize: handle.queueSize,
15
- };
16
- parsedHandles[type].push(parsed);
17
- }
18
- return parsedHandles;
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' ? undefined : node.parentId?.toString(),
30
- extent: node.parentId?.toString() === '-1' ? undefined : 'parent',
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' ? undefined : node.parentId?.toString(),
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
- const parentNodesIds = parentNodes.map(id => id.toString());
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 ? null : node)
84
- .filter(node => node !== null);
85
- const newFormattedParentNodes = formattedParentNodes.map(node => ({
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 = [...newFormattedParentNodes, ...childrenNodes, ...filteredNodes];
117
- return { nodes: [...groupedNodes], edges: [...edges, ...bridges] };
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.10.0",
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": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
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": {