@luxonis/depthai-pipeline-lib 1.11.0 → 1.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.css 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
@@ -1297,12 +1297,16 @@
1297
1297
  border-top-right-radius: var(--radii-common);
1298
1298
  }
1299
1299
 
1300
- .w_icon\.sm {
1301
- width: var(--sizes-icon-sm);
1300
+ .w_20px {
1301
+ width: 20px;
1302
1302
  }
1303
1303
 
1304
- .h_icon\.sm {
1305
- height: var(--sizes-icon-sm);
1304
+ .h_20px {
1305
+ height: 20px;
1306
+ }
1307
+
1308
+ .d_flex {
1309
+ display: flex;
1306
1310
  }
1307
1311
 
1308
1312
  .w_auto {
@@ -1321,14 +1325,22 @@
1321
1325
  background-color: rgba(0,0,0,0.125);
1322
1326
  }
1323
1327
 
1324
- .bg-c_dark\.error\! {
1325
- background-color: var(--colors-dark-error) !important;
1328
+ .bg-c_dark\.warning\! {
1329
+ background-color: var(--colors-dark-warning) !important;
1326
1330
  }
1327
1331
 
1328
1332
  .bg-c_dark\.success\! {
1329
1333
  background-color: var(--colors-dark-success) !important;
1330
1334
  }
1331
1335
 
1336
+ .bg-c_dark\.error\! {
1337
+ background-color: var(--colors-dark-error) !important;
1338
+ }
1339
+
1340
+ .bg-c_dark\.gray\! {
1341
+ background-color: var(--colors-dark-gray) !important;
1342
+ }
1343
+
1332
1344
  .ml_xs {
1333
1345
  margin-left: var(--spacing-xs);
1334
1346
  }
@@ -1345,6 +1357,10 @@
1345
1357
  background-color: var(--colors-light-gray);
1346
1358
  }
1347
1359
 
1360
+ .ai_center {
1361
+ align-items: center;
1362
+ }
1363
+
1348
1364
  .right_xs {
1349
1365
  right: var(--spacing-xs);
1350
1366
  }
@@ -6,14 +6,14 @@ import { css } from '../styled-system/css/index.mjs';
6
6
  export const GroupNode = (props) => {
7
7
  const { data: _node } = props;
8
8
  return (_jsx(Flex, { direction: "column", className: css({
9
- 'minWidth': 'container.smaller.xxs',
10
- 'border': 'base',
11
- 'rounded': 'common',
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
- 'padding': 0,
16
- 'borderStyle': 'dashed',
17
- 'backgroundColor': 'rgba(0,0,0,0.125)',
15
+ padding: 0,
16
+ borderStyle: 'dashed',
17
+ backgroundColor: 'rgba(0,0,0,0.125)',
18
18
  }) }));
19
19
  };
@@ -1,5 +1,5 @@
1
- import React from 'react';
2
1
  import type { NodeProps } from '@xyflow/react';
2
+ import React from 'react';
3
3
  import { type ParsedNode } from '../services/pipeline.js';
4
4
  export type PipelineNodeProps = NodeProps<ParsedNode>;
5
5
  export declare const PipelineNode: React.FC<PipelineNodeProps>;
@@ -1,25 +1,37 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Handle, Position } from '@xyflow/react';
2
+ import { Badge, Button, Flex, HelpIcon, Label, SubLabel, } from '@luxonis/common-fe-components';
3
3
  import { clsx } from '@luxonis/common-fe-components/helpers';
4
- import { 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 { formatTiming } from '../services/pipeline-state.js';
7
+ import { css } from '../styled-system/css/css.mjs';
8
+ import { MIN_NODE_WIDTH } from './PipelineCanvas.js';
9
+ const NodeHandles = (props) => {
8
10
  const { handles, type } = props;
9
- return (_jsx(Flex, { full: true, direction: "column", align: type === 'input' ? 'start' : 'end', children: handles.map(({ type: handleType, id, blocking, queueSize, name, maxQueueSize, fps, connected }) => {
11
+ return (_jsx(Flex, { full: true, direction: "column", align: type === 'input' ? 'start' : 'end', minWidth: "120px", children: handles.map(({ type: handleType, id, blocking, queueSize, name, maxQueueSize, fps, connected, dotColor, }) => {
10
12
  if (!connected) {
11
13
  return;
12
14
  }
13
15
  return (_jsxs(Flex, { position: "relative", align: "end", direction: handleType === 'input' ? 'row' : 'row-reverse', children: [_jsx(Handle, { type: handleType === 'input' ? 'target' : 'source', position: handleType === 'input' ? Position.Left : Position.Right, id: name, isConnectable: false, className: css({
14
16
  width: 'custom.handle.dot!',
15
17
  height: 'custom.handle.dot!',
16
- backgroundColor: blocking ? 'dark.error!' : 'dark.success!',
18
+ backgroundColor: blocking
19
+ ? 'dark.error!'
20
+ : dotColor === 'gray'
21
+ ? 'dark.gray!'
22
+ : dotColor === 'green'
23
+ ? 'dark.success!'
24
+ : dotColor === 'red'
25
+ ? 'dark.error!'
26
+ : dotColor === 'yellow'
27
+ ? 'dark.warning!'
28
+ : 'dark.success!',
17
29
  border: 'none!',
18
30
  borderRadius: blocking ? '0% !important' : '100% !important',
19
31
  }) }), _jsx(NodeHandlesSubLabel, { type: type, name: name, queueSize: queueSize, maxQueueSize: maxQueueSize, fps: fps })] }, id));
20
32
  }) }));
21
33
  };
22
- const NodeHandlesSubLabel = props => {
34
+ const NodeHandlesSubLabel = (props) => {
23
35
  const { type, name, queueSize, maxQueueSize, fps } = props;
24
36
  return (_jsx(SubLabel, { className: css({
25
37
  ...(type === 'input' //
@@ -29,17 +41,20 @@ const NodeHandlesSubLabel = props => {
29
41
  ? `[${fps ? `${fps.toFixed(1)} | ` : ''}${`${maxQueueSize ?? 0}/${queueSize}`}] ${name}`
30
42
  : `${fps ? `[${fps.toFixed(1)}]` : ''} ${name}`, break: "none" }));
31
43
  };
32
- export const PipelineNode = props => {
44
+ const GPSTTimings = (props) => {
45
+ return (_jsx(Flex, { flexDirection: "column", alignItems: "center", minWidth: "120px", children: Object.entries(props).map(([key, item]) => (_jsxs(Flex, { gap: "xxs", children: [_jsx(Label, { text: `${key}:`, size: "small" }), _jsx(Label, { text: `${formatTiming(item)}ms`, size: "small" })] }, `${key}-${item}-label`))) }));
46
+ };
47
+ export const PipelineNode = (props) => {
33
48
  const { data: node } = props;
34
- // eslint-disable-next-line no-prototype-builtins
49
+ // biome-ignore lint/suspicious/noPrototypeBuiltins: Intended
35
50
  const link = NodesWithLinks.hasOwnProperty(node.name)
36
51
  ? `${DOCS_BASE_URL}/${NodesWithLinks[node.name]}`
37
52
  : DOCS_BASE_URL;
38
53
  return (_jsxs(Flex, { direction: "column", className: css({
39
- 'minWidth': 'container.smaller.xxs',
40
- 'border': 'base',
41
- 'rounded': 'common',
42
- 'backgroundColor': 'white',
54
+ minWidth: 'container.smaller.xxs',
55
+ border: 'base',
56
+ rounded: 'common',
57
+ backgroundColor: 'white',
43
58
  '&:hover .node-help-icon': {
44
59
  color: 'text.normal',
45
60
  },
@@ -48,9 +63,22 @@ export const PipelineNode = props => {
48
63
  backgroundColor: 'light.gray',
49
64
  roundedTop: 'common',
50
65
  }), children: [_jsx("span", { className: css({
51
- width: 'icon.sm',
52
- height: 'icon.sm',
53
- }) }), _jsx(Label, { text: node.id ? `${node.name} (${node.id})` : node.name, color: "unset" }), _jsx(Button, { variant: "ghost", color: "transparent", icon: HelpIcon, onClick: () => window.open(link, '_blank'), className: clsx('node-help-icon', css({
66
+ width: '20px',
67
+ height: '20px',
68
+ display: 'flex',
69
+ alignItems: 'center',
70
+ }), children: (node.nodeType === 'device' || node.nodeType === 'host') && (_jsx(Badge, { label: node.nodeType === 'device' ? 'D' : 'H', variant: node.nodeType === 'device' ? 'active' : 'cyan', style: {
71
+ padding: '0px',
72
+ display: 'flex',
73
+ justifyContent: 'center',
74
+ alignItems: 'center',
75
+ width: '20px',
76
+ height: '20px',
77
+ }, tooltip: node.nodeType === 'device' ? 'Device Node' : 'Host Node', cursor: "help" })) }), _jsx(Label, { text: node.id
78
+ ? `${node.name} (${node.id}) ${node.extras?.stateInfo ? `(${node.extras.stateInfo.letter})` : ''}`
79
+ : node.extras?.stateInfo
80
+ ? `${node.name} (${node.extras.stateInfo.letter})`
81
+ : node.name, color: "unset" }), _jsx(Button, { variant: "ghost", color: "transparent", icon: HelpIcon, onClick: () => window.open(link, '_blank'), className: clsx('node-help-icon', css({
54
82
  width: 'auto',
55
83
  height: 'auto',
56
84
  right: 'xs',
@@ -60,7 +88,7 @@ export const PipelineNode = props => {
60
88
  }), style: {
61
89
  border: 'none',
62
90
  background: 'transparent',
63
- } }), _jsxs(Flex, { gap: "sm", paddingY: "xs", children: [_jsx(NodeHandles, { type: "input", handles: node.handles.input }), _jsx(NodeHandles, { type: "output", handles: node.handles.output })] }), _jsx(Handle, { type: "source", position: Position.Bottom, id: "bottom", isConnectable: false, className: css({
91
+ } }), _jsxs(Flex, { gap: "sm", paddingY: "xs", minWidth: `${MIN_NODE_WIDTH}px`, children: [_jsx(NodeHandles, { type: "input", handles: node.handles.input }), node.extras && node.extras.gpstTimings && (_jsx(GPSTTimings, { ...node.extras.gpstTimings })), _jsx(NodeHandles, { type: "output", handles: node.handles.output })] }), _jsx(Handle, { type: "source", position: Position.Bottom, id: "bottom", isConnectable: false, className: css({
64
92
  width: 'custom.handle.dot!',
65
93
  height: 'custom.handle.dot!',
66
94
  }), style: {
@@ -1,5 +1,5 @@
1
- import React from 'react';
2
1
  import type { FlexProps } from '@luxonis/common-fe-components';
2
+ import React from 'react';
3
3
  import type { ParsedNode, Pipeline } from '../services/pipeline.js';
4
4
  import type { PipelineState } from '../services/pipeline-state.js';
5
5
  export type PipelineCanvasProps = FlexProps & {
@@ -7,5 +7,7 @@ export type PipelineCanvasProps = FlexProps & {
7
7
  pipelineState: PipelineState[] | null;
8
8
  action?: React.ReactNode;
9
9
  };
10
+ export declare const MIN_NODE_WIDTH = 376;
11
+ export declare const MIN_NODE_HEIGHT = 150;
10
12
  export declare const updateNodesOnPipelineStateChange: (nodes: ParsedNode[], pipelineState: PipelineState[]) => ParsedNode[];
11
13
  export declare const PipelineCanvas: React.FC<PipelineCanvasProps>;
@@ -1,79 +1,84 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import React from 'react';
3
- import { Panel, ReactFlow, ReactFlowProvider, useEdgesState, useNodesState, useReactFlow, useStore, } from '@xyflow/react';
4
- import { Flex, Header } from '@luxonis/common-fe-components';
5
2
  import Dagre from '@dagrejs/dagre';
3
+ import { Flex, Header } from '@luxonis/common-fe-components';
4
+ import { Panel, ReactFlow, ReactFlowProvider, useEdgesState, useNodesState, useReactFlow, useStore, } from '@xyflow/react';
5
+ import React from 'react';
6
+ import { topoSort } from '../services/utils.js';
6
7
  import { PipelineNode } from './Node.js';
8
+ export const MIN_NODE_WIDTH = 376;
9
+ export const MIN_NODE_HEIGHT = 150;
7
10
  const getLayoutedElements = (nodes, edges) => {
8
11
  const graph = new Dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({}));
9
- graph.setGraph({ rankdir: 'LR', ranksep: 80, nodesep: 40 });
10
- const childNodes = nodes.filter(node => node.type !== 'group');
12
+ graph.setGraph({ rankdir: 'LR' });
11
13
  for (const edge of edges) {
12
14
  graph.setEdge(edge.source, edge.target);
13
15
  }
14
- for (const node of childNodes) {
16
+ for (const node of nodes) {
15
17
  graph.setNode(node.id, {
16
- width: node.measured?.width ?? 300,
17
- height: node.measured?.height ?? 150,
18
+ ...node,
19
+ width: MIN_NODE_WIDTH,
20
+ height: MIN_NODE_HEIGHT,
18
21
  });
19
22
  }
20
23
  Dagre.layout(graph);
21
24
  return {
22
- nodes: nodes.map(node => {
23
- const 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
- };
25
+ nodes: nodes.map((node) => {
26
+ const position = graph.node(node.id);
27
+ const x = position.x - MIN_NODE_WIDTH / 2;
28
+ const y = position.y - MIN_NODE_HEIGHT / 2;
29
+ return { ...node, position: { x, y } };
33
30
  }),
34
31
  edges,
35
32
  };
36
33
  };
37
34
  const adjustNodes = (nodes, edges) => {
38
35
  const PADDING = 16;
39
- const groupNodes = nodes.filter(node => node.type === 'group');
40
- const childNodes = nodes.filter(node => node.type !== 'group');
41
- const graph = new Dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({}));
42
- graph.setGraph({ rankdir: 'LR', ranksep: 80, nodesep: 40 });
43
- for (const edge of edges) {
44
- graph.setEdge(edge.source, edge.target);
45
- }
36
+ const HEADER_HEIGHT = 24;
37
+ // Build a tree: find all group nodes and their depth
38
+ const groupNodes = nodes.filter((node) => node.type === 'group');
39
+ const childNodes = nodes.filter((node) => node.type !== 'group');
40
+ // Sort groups by depth (deepest first) so children are resolved before parents
41
+ const getDepth = (nodeId, depth = 0) => {
42
+ const node = nodes.find((n) => n.id === nodeId);
43
+ if (!node?.parentId)
44
+ return depth;
45
+ return getDepth(node.parentId, depth + 1);
46
+ };
47
+ const sortedGroups = [...groupNodes].sort((a, b) => getDepth(b.id) - getDepth(a.id));
48
+ // Track resolved sizes for groups as we process them
49
+ const resolvedSizes = {};
50
+ // Start with all non-group nodes at their measured sizes
46
51
  for (const node of childNodes) {
47
- graph.setNode(node.id, {
48
- width: node.measured?.width ?? 300,
49
- 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
- },
52
+ resolvedSizes[node.id] = {
53
+ width: Math.min(node.measured?.width ?? MIN_NODE_WIDTH),
54
+ height: Math.min(node.measured?.height ?? MIN_NODE_HEIGHT),
61
55
  };
62
- });
63
- const positionedGroups = groupNodes.map(group => {
64
- const children = positionedChildren.filter(n => n.parentId === group.id);
56
+ }
57
+ const updatedNodes = {};
58
+ for (const node of nodes)
59
+ updatedNodes[node.id] = { ...node };
60
+ // Process groups deepest-first
61
+ for (const group of sortedGroups) {
62
+ const children = nodes.filter((n) => n.parentId === group.id);
65
63
  if (children.length === 0) {
66
- return { ...group, position: { x: 0, y: 0 } };
64
+ resolvedSizes[group.id] = {
65
+ width: MIN_NODE_WIDTH,
66
+ height: MIN_NODE_HEIGHT,
67
+ };
68
+ continue;
67
69
  }
68
70
  const g = new Dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({}));
69
- g.setGraph({ rankdir: 'LR', ranksep: 60, nodesep: 20 });
71
+ g.setGraph({ rankdir: 'LR' });
70
72
  for (const child of children) {
71
- g.setNode(child.id, {
72
- width: child.measured?.width ?? 300,
73
- height: child.measured?.height ?? 150,
74
- });
73
+ // Use resolved size (important for nested groups!)
74
+ const size = resolvedSizes[child.id] ?? {
75
+ width: MIN_NODE_WIDTH,
76
+ height: MIN_NODE_HEIGHT,
77
+ };
78
+ g.setNode(child.id, { width: size.width, height: size.height });
75
79
  }
76
- const groupEdges = edges.filter(e => children.some(c => c.id === e.source) && children.some(c => c.id === e.target));
80
+ const groupEdges = edges.filter((e) => children.some((c) => c.id === e.source) &&
81
+ children.some((c) => c.id === e.target));
77
82
  for (const edge of groupEdges) {
78
83
  g.setEdge(edge.source, edge.target);
79
84
  }
@@ -82,59 +87,86 @@ const adjustNodes = (nodes, edges) => {
82
87
  let minY = 9999;
83
88
  let maxX = -9999;
84
89
  let maxY = -9999;
85
- const locallyPositioned = children.map(child => {
90
+ // Replace both child loops with this single pass:
91
+ for (const child of children) {
86
92
  const pos = g.node(child.id);
87
- const x = pos.x - (child.measured?.width ?? 300) / 2;
88
- const y = pos.y - (child.measured?.height ?? 150) / 2;
93
+ const size = resolvedSizes[child.id] ?? {
94
+ width: MIN_NODE_WIDTH,
95
+ height: MIN_NODE_HEIGHT,
96
+ };
97
+ const x = pos.x - size.width / 2;
98
+ const y = pos.y - size.height / 2;
89
99
  minX = Math.min(minX, x);
90
100
  minY = Math.min(minY, y);
91
- maxX = Math.max(maxX, x + (child.measured?.width ?? 300));
92
- maxY = Math.max(maxY, y + (child.measured?.height ?? 150));
93
- return { ...child, position: { x, y } };
94
- });
95
- const relativeChildren = locallyPositioned.map(child => ({
96
- ...child,
97
- position: {
98
- x: child.position.x - minX + PADDING,
99
- y: child.position.y - minY + PADDING,
100
- },
101
- }));
102
- const groupW = maxX - minX + PADDING * 2;
103
- const groupH = maxY - minY + PADDING * 2;
104
- let absMinX = 9999;
105
- let absMinY = 9999;
106
- for (const child of children) {
107
- const pos = graph.node(child.id);
108
- absMinX = Math.min(absMinX, pos.x - (child.measured?.width ?? 300) / 2);
109
- absMinY = Math.min(absMinY, pos.y - (child.measured?.height ?? 150) / 2);
101
+ maxX = Math.max(maxX, x + size.width);
102
+ maxY = Math.max(maxY, y + size.height);
110
103
  }
111
- return {
112
- nodes: relativeChildren,
113
- group: {
114
- ...group,
104
+ // Now positions are correct, apply them
105
+ for (const child of children) {
106
+ const pos = g.node(child.id);
107
+ const size = resolvedSizes[child.id] ?? {
108
+ width: MIN_NODE_WIDTH,
109
+ height: MIN_NODE_HEIGHT,
110
+ };
111
+ updatedNodes[child.id] = {
112
+ ...updatedNodes[child.id],
115
113
  position: {
116
- x: absMinX - PADDING,
117
- y: absMinY - PADDING,
114
+ x: pos.x - size.width / 2 - minX + PADDING,
115
+ y: pos.y - size.height / 2 - minY + PADDING + HEADER_HEIGHT,
118
116
  },
119
- width: groupW + PADDING,
120
- height: groupH,
121
- style: { ...group.style, width: groupW + PADDING, height: groupH },
117
+ };
118
+ }
119
+ const groupW = maxX - minX + PADDING * 2;
120
+ const groupH = maxY - minY + PADDING * 2 + HEADER_HEIGHT;
121
+ // Store this group's resolved size for its own parent to use
122
+ resolvedSizes[group.id] = { width: groupW, height: groupH };
123
+ updatedNodes[group.id] = {
124
+ ...updatedNodes[group.id],
125
+ style: { ...updatedNodes[group.id].style, width: groupW, height: groupH },
126
+ width: groupW,
127
+ height: groupH,
128
+ };
129
+ }
130
+ // Now lay out the top-level (root) nodes using Dagre with correct sizes
131
+ const graph = new Dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({}));
132
+ graph.setGraph({ rankdir: 'LR' });
133
+ const rootNodes = nodes.filter((n) => !n.parentId);
134
+ for (const node of rootNodes) {
135
+ const size = resolvedSizes[node.id] ?? {
136
+ width: MIN_NODE_WIDTH,
137
+ height: MIN_NODE_HEIGHT,
138
+ };
139
+ graph.setNode(node.id, { width: size.width, height: size.height });
140
+ }
141
+ const rootEdges = edges.filter((e) => rootNodes.some((n) => n.id === e.source) &&
142
+ rootNodes.some((n) => n.id === e.target));
143
+ for (const edge of rootEdges) {
144
+ graph.setEdge(edge.source, edge.target);
145
+ }
146
+ Dagre.layout(graph);
147
+ for (const node of rootNodes) {
148
+ const pos = graph.node(node.id);
149
+ if (!pos)
150
+ continue;
151
+ const size = resolvedSizes[node.id] ?? {
152
+ width: MIN_NODE_WIDTH,
153
+ height: MIN_NODE_HEIGHT,
154
+ };
155
+ updatedNodes[node.id] = {
156
+ ...updatedNodes[node.id],
157
+ position: {
158
+ x: pos.x - size.width / 2,
159
+ y: pos.y - size.height / 2,
122
160
  },
123
161
  };
124
- });
125
- const rootChildren = positionedChildren.filter(n => !n.parentId);
126
- const groupedChildren = positionedGroups.flatMap(g => g.nodes);
127
- const groups = positionedGroups.map(g => g.group);
128
- return [
129
- ...groups.filter(g => g !== undefined),
130
- ...groupedChildren.filter(c => c !== undefined),
131
- ...rootChildren,
132
- ];
162
+ }
163
+ return topoSort(Object.values(updatedNodes));
133
164
  };
134
165
  export const updateNodesOnPipelineStateChange = (nodes, pipelineState) => {
135
166
  const parsedNodes = [];
136
167
  for (const node of nodes) {
137
- const nodeState = pipelineState.find(state => state.id.toString() === node.id.toString());
168
+ const nodeState = pipelineState.find((state) => state.id.toString() === node.id.toString());
169
+ // Inputs and outputs
138
170
  const inputHandles = node.data.handles.input;
139
171
  const outputHandles = node.data.handles.output;
140
172
  const newInputHandles = [];
@@ -144,6 +176,7 @@ export const updateNodesOnPipelineStateChange = (nodes, pipelineState) => {
144
176
  if (inputState) {
145
177
  obj.maxQueueSize = inputState.numQueued;
146
178
  obj.fps = inputState.timing.fps;
179
+ obj.dotColor = inputState.dotColor;
147
180
  }
148
181
  newInputHandles.push(obj);
149
182
  }
@@ -154,9 +187,14 @@ export const updateNodesOnPipelineStateChange = (nodes, pipelineState) => {
154
187
  if (outputState) {
155
188
  obj.maxQueueSize = outputState.numQueued;
156
189
  obj.fps = outputState.timing.fps;
190
+ obj.dotColor = outputState.dotColor;
157
191
  }
158
192
  newOutputHandles.push(obj);
159
193
  }
194
+ // GPST timings
195
+ const gpst = nodeState?.gpstTimings;
196
+ // Node state
197
+ const state = nodeState?.stateInfo;
160
198
  parsedNodes.push({
161
199
  ...node,
162
200
  data: {
@@ -165,12 +203,16 @@ export const updateNodesOnPipelineStateChange = (nodes, pipelineState) => {
165
203
  input: newInputHandles,
166
204
  output: newOutputHandles,
167
205
  },
206
+ extras: {
207
+ gpstTimings: gpst,
208
+ stateInfo: state,
209
+ },
168
210
  },
169
211
  });
170
212
  }
171
213
  return parsedNodes;
172
214
  };
173
- const PipelineCanvasBody = ({ pipeline, pipelineState, action, ...flexProps }) => {
215
+ const PipelineCanvasBody = ({ pipeline, pipelineState: pipelineStateParsed, action, ...flexProps }) => {
174
216
  const { fitView, setViewport, getViewport } = useReactFlow();
175
217
  const autoArrangeRef = React.useRef(true);
176
218
  const widthSelector = (state) => state.width;
@@ -178,6 +220,8 @@ const PipelineCanvasBody = ({ pipeline, pipelineState, action, ...flexProps }) =
178
220
  const reactFlowWidth = useStore(widthSelector);
179
221
  const reactFlowHeight = useStore(heightSelector);
180
222
  const [shouldFitAndResize, setShouldFitAndResize] = React.useState(false);
223
+ const pipelineState = React.useMemo(() => pipelineStateParsed, [pipelineStateParsed]);
224
+ // biome-ignore lint/correctness/useExhaustiveDependencies: Intended
181
225
  React.useEffect(() => {
182
226
  void fitView();
183
227
  // eslint-disable-next-line react-hooks/exhaustive-deps
@@ -189,17 +233,19 @@ const PipelineCanvasBody = ({ pipeline, pipelineState, action, ...flexProps }) =
189
233
  return;
190
234
  }
191
235
  const layouted = getLayoutedElements(pipeline?.nodes ?? [], pipeline?.edges ?? []);
192
- const adjustedNodes = adjustNodes([...layouted.nodes], [...layouted.edges]);
193
236
  if (pipelineState) {
194
- const updatedNodes = updateNodesOnPipelineStateChange(adjustedNodes, pipelineState);
195
- setNodes([...updatedNodes]);
237
+ const updatedNodes = updateNodesOnPipelineStateChange([...layouted.nodes], pipelineState);
238
+ const adjustedNodes = adjustNodes([...updatedNodes], layouted.edges);
239
+ setNodes([...adjustedNodes]);
196
240
  }
197
241
  else {
242
+ const adjustedNodes = adjustNodes([...layouted.nodes], layouted.edges);
198
243
  setNodes([...adjustedNodes]);
199
244
  }
200
245
  setEdges([...layouted.edges]);
201
246
  setShouldFitAndResize(true);
202
247
  }, [pipeline?.edges, pipeline?.nodes, setEdges, setNodes, pipelineState]);
248
+ // biome-ignore lint/correctness/useExhaustiveDependencies: Intended
203
249
  React.useEffect(() => {
204
250
  if (pipelineState && nodes.length > 0) {
205
251
  const updatedNodes = updateNodesOnPipelineStateChange(nodes, pipelineState);
@@ -217,16 +263,7 @@ const PipelineCanvasBody = ({ pipeline, pipelineState, action, ...flexProps }) =
217
263
  await setViewport({ x, y, zoom });
218
264
  await fitView({ nodes: nds });
219
265
  };
220
- React.useEffect(() => {
221
- if (!autoArrangeRef.current) {
222
- return;
223
- }
224
- if (nodes.length > 0 && edges.length > 0) {
225
- const viewport = getViewport();
226
- void setViewportAndFit(viewport.x, viewport.y, viewport.zoom, nodes);
227
- }
228
- // eslint-disable-next-line react-hooks/exhaustive-deps
229
- }, [nodes, edges]);
266
+ // biome-ignore lint/correctness/useExhaustiveDependencies: Intended
230
267
  React.useEffect(() => {
231
268
  if (shouldFitAndResize) {
232
269
  const viewport = getViewport();
@@ -237,4 +274,4 @@ const PipelineCanvasBody = ({ pipeline, pipelineState, action, ...flexProps }) =
237
274
  }, [shouldFitAndResize]);
238
275
  return (_jsxs(Flex, { align: "center", justify: "center", full: true, height: "container.xs", rounded: "common", border: "base", ...flexProps, children: [!pipeline && _jsx(Header, { text: "Loading pipeline..." }), pipeline && (_jsx(ReactFlow, { nodes: nodes, edges: edges, onNodeDragStart: () => (autoArrangeRef.current = false), onNodesChange: onNodesChange, fitView: true, nodeTypes: { generic: PipelineNode }, minZoom: 0.4, children: action && _jsx(Panel, { position: "top-right", children: action }) }))] }));
239
276
  };
240
- export const PipelineCanvas = props => (_jsx(ReactFlowProvider, { children: _jsx(PipelineCanvasBody, { ...props }) }));
277
+ export const PipelineCanvas = (props) => (_jsx(ReactFlowProvider, { children: _jsx(PipelineCanvasBody, { ...props }) }));
@@ -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,84 @@
1
+ import type { Edge } from '@xyflow/react';
2
+ import type { ParsedNode, RawPipelineNodeIO } from './pipeline';
3
+ import { PipelineStateDotColor } from './pipeline-state';
4
+ export type RawPipelineBridge = [number, number];
5
+ export type ParsedHandle = {
6
+ id: number;
7
+ name: string;
8
+ type: 'input' | 'output';
9
+ blocking: boolean;
10
+ queueSize: number;
11
+ connected: boolean;
12
+ maxQueueSize?: number;
13
+ fps?: number;
14
+ dotColor?: PipelineStateDotColor;
15
+ };
16
+ export declare function parseHandles(handles: RawPipelineNodeIO[]): {
17
+ input: ParsedHandle[];
18
+ output: ParsedHandle[];
19
+ };
20
+ export declare function filterNodesHandles(nodes: ParsedNode[], edges: Edge[]): {
21
+ data: {
22
+ handles: {
23
+ input: {
24
+ connected: boolean;
25
+ id: number;
26
+ name: string;
27
+ type: "input" | "output";
28
+ blocking: boolean;
29
+ queueSize: number;
30
+ maxQueueSize?: number;
31
+ fps?: number;
32
+ dotColor?: PipelineStateDotColor;
33
+ }[];
34
+ output: {
35
+ connected: boolean;
36
+ id: number;
37
+ name: string;
38
+ type: "input" | "output";
39
+ blocking: boolean;
40
+ queueSize: number;
41
+ maxQueueSize?: number;
42
+ fps?: number;
43
+ dotColor?: PipelineStateDotColor;
44
+ }[];
45
+ };
46
+ id: string;
47
+ parentId?: string;
48
+ name: string;
49
+ nodeType?: "device" | "host";
50
+ extras?: import("./pipeline-state").NodeExtras;
51
+ };
52
+ id: string;
53
+ position: import("@xyflow/system").XYPosition;
54
+ type?: string | undefined;
55
+ sourcePosition?: import("@xyflow/system").Position;
56
+ targetPosition?: import("@xyflow/system").Position;
57
+ hidden?: boolean;
58
+ selected?: boolean;
59
+ dragging?: boolean;
60
+ draggable?: boolean;
61
+ selectable?: boolean;
62
+ connectable?: boolean;
63
+ deletable?: boolean;
64
+ dragHandle?: string;
65
+ width?: number;
66
+ height?: number;
67
+ initialWidth?: number;
68
+ initialHeight?: number;
69
+ parentId?: string;
70
+ zIndex?: number;
71
+ extent?: "parent" | import("@xyflow/system").CoordinateExtent;
72
+ expandParent?: boolean;
73
+ ariaLabel?: string;
74
+ origin?: import("@xyflow/system").NodeOrigin;
75
+ handles?: import("@xyflow/system").NodeHandle[];
76
+ measured?: {
77
+ width?: number;
78
+ height?: number;
79
+ };
80
+ style?: import("react").CSSProperties;
81
+ className?: string;
82
+ resizing?: boolean;
83
+ focusable?: boolean;
84
+ }[];
@@ -0,0 +1,41 @@
1
+ export function parseHandles(handles) {
2
+ const parsedHandles = {
3
+ input: [],
4
+ output: [],
5
+ };
6
+ for (const [_, handle] of handles) {
7
+ const type = handle.type === 3 ? 'input' : 'output';
8
+ const parsed = {
9
+ id: handle.id,
10
+ name: handle.name,
11
+ type,
12
+ blocking: handle.blocking,
13
+ queueSize: handle.queueSize,
14
+ connected: true,
15
+ };
16
+ parsedHandles[type].push(parsed);
17
+ }
18
+ return parsedHandles;
19
+ }
20
+ export function filterNodesHandles(nodes, edges) {
21
+ const connectedHandleIds = new Set(edges.flatMap((edge) => [
22
+ `${edge.source}-${edge.sourceHandle}`,
23
+ `${edge.target}-${edge.targetHandle}`,
24
+ ]));
25
+ return nodes.map((node) => ({
26
+ ...node,
27
+ data: {
28
+ ...node.data,
29
+ handles: {
30
+ input: node.data.handles.input.map((handle) => ({
31
+ ...handle,
32
+ connected: connectedHandleIds.has(`${node.id}-${handle.name}`),
33
+ })),
34
+ output: node.data.handles.output.map((handle) => ({
35
+ ...handle,
36
+ connected: connectedHandleIds.has(`${node.id}-${handle.name}`),
37
+ })),
38
+ },
39
+ },
40
+ }));
41
+ }
@@ -1,10 +1,13 @@
1
1
  export type RawPipelineStatePayload = {
2
2
  nodeStates: RawPipelineState[];
3
3
  };
4
+ type IONodeState = 0 | 1 | 2;
4
5
  export type IOStates = {
5
6
  [key: string]: {
6
7
  numQueued: number;
7
8
  timing: TimingWithFps;
9
+ state?: IONodeState;
10
+ dotColor?: PipelineStateDotColor;
8
11
  };
9
12
  };
10
13
  type Timing = {
@@ -20,6 +23,8 @@ type TimingWithFps = {
20
23
  durationStats: Timing;
21
24
  fps: number;
22
25
  };
26
+ type NodeStateRaw = 0 | 1 | 2 | 3;
27
+ type NodeState = 'IDLE' | 'GETTING_INPUTS' | 'PROCESSING' | 'SENDING_OUTPUTS';
23
28
  export type RawPipelineState = [
24
29
  number,
25
30
  {
@@ -30,13 +35,31 @@ export type RawPipelineState = [
30
35
  otherTimings: object;
31
36
  outputStates: IOStates;
32
37
  outputsSendTiming: TimingWithFps;
33
- state: number;
38
+ state?: NodeStateRaw;
34
39
  }
35
40
  ];
41
+ export type PipelineStateDotColor = 'gray' | 'green' | 'yellow' | 'red';
42
+ export type GPSTTimingsType = {
43
+ G: number;
44
+ P: number;
45
+ S: number;
46
+ T: number;
47
+ };
48
+ export type NodeExtras = {
49
+ gpstTimings?: GPSTTimingsType;
50
+ stateInfo?: {
51
+ state: NodeState;
52
+ label: string;
53
+ letter: string;
54
+ };
55
+ };
36
56
  export type PipelineState = {
37
57
  id: number;
38
58
  inputs: IOStates;
39
59
  outputs: IOStates;
40
- };
60
+ } & NodeExtras;
41
61
  export declare function parsePipelineState(rawPayload: RawPipelineStatePayload): PipelineState[];
62
+ export declare function stateToLabel(state: NodeState): string;
63
+ export declare function stateToLetter(state: NodeState): string;
64
+ export declare function formatTiming(time: number): string;
42
65
  export {};
@@ -1,13 +1,94 @@
1
+ function nodeStateToDotColor(state) {
2
+ switch (state) {
3
+ case 0:
4
+ return 'green';
5
+ case 1:
6
+ return 'yellow';
7
+ case 2:
8
+ return 'red';
9
+ default:
10
+ return 'gray';
11
+ }
12
+ }
13
+ function nodeStateToState(state) {
14
+ switch (state) {
15
+ case 0:
16
+ return 'IDLE';
17
+ case 1:
18
+ return 'GETTING_INPUTS';
19
+ case 2:
20
+ return 'PROCESSING';
21
+ case 3:
22
+ return 'SENDING_OUTPUTS';
23
+ }
24
+ }
25
+ function formatIOStates(values) {
26
+ let returnObj = {};
27
+ for (const [key, value] of Object.entries(values)) {
28
+ returnObj = {
29
+ ...returnObj,
30
+ [key]: {
31
+ ...value,
32
+ dotColor: value.state ? nodeStateToDotColor(value.state) : 'gray',
33
+ },
34
+ };
35
+ }
36
+ return returnObj;
37
+ }
1
38
  export function parsePipelineState(rawPayload) {
2
39
  const { nodeStates } = rawPayload;
3
40
  const parsedNodeStates = [];
4
41
  for (const [nodeId, nodeState] of nodeStates) {
42
+ const state = nodeState?.state ? nodeStateToState(nodeState.state) : 'IDLE';
5
43
  const currentNode = {
6
44
  id: nodeId,
7
- inputs: nodeState.inputStates,
8
- outputs: nodeState.outputStates,
45
+ inputs: formatIOStates(nodeState.inputStates),
46
+ outputs: formatIOStates(nodeState.outputStates),
47
+ gpstTimings: {
48
+ G: nodeState.inputsGetTiming.durationStats.averageMicrosRecent,
49
+ P: nodeState.mainLoopTiming.durationStats.averageMicrosRecent -
50
+ nodeState.outputsSendTiming.durationStats.averageMicrosRecent -
51
+ nodeState.inputsGetTiming.durationStats.averageMicrosRecent,
52
+ S: nodeState.outputsSendTiming.durationStats.averageMicrosRecent,
53
+ T: nodeState.mainLoopTiming.durationStats.averageMicrosRecent,
54
+ },
55
+ stateInfo: {
56
+ state: state,
57
+ label: stateToLabel(state),
58
+ letter: stateToLetter(state),
59
+ },
9
60
  };
10
61
  parsedNodeStates.push(currentNode);
11
62
  }
12
63
  return parsedNodeStates;
13
64
  }
65
+ export function stateToLabel(state) {
66
+ switch (state) {
67
+ case 'IDLE':
68
+ return 'Idle';
69
+ case 'GETTING_INPUTS':
70
+ return 'Getting Inputs';
71
+ case 'PROCESSING':
72
+ return 'Processing';
73
+ case 'SENDING_OUTPUTS':
74
+ return 'Sending Outputs';
75
+ }
76
+ }
77
+ export function stateToLetter(state) {
78
+ switch (state) {
79
+ case 'IDLE':
80
+ return 'I';
81
+ case 'GETTING_INPUTS':
82
+ return 'G';
83
+ case 'PROCESSING':
84
+ return 'P';
85
+ case 'SENDING_OUTPUTS':
86
+ return 'S';
87
+ }
88
+ }
89
+ // NOTE: Time is in microseconds. 1us => 0.001 ms => 1e-6 s
90
+ export function formatTiming(time) {
91
+ return Math.round(time / 1000)
92
+ .toString()
93
+ .padStart(3, '0');
94
+ }
@@ -1,4 +1,6 @@
1
1
  import { type Edge, type Node } from '@xyflow/react';
2
+ import { type ParsedHandle } from './pipeline-handles';
3
+ import { NodeExtras } from './pipeline-state';
2
4
  export type RawPipelineNodeIO = [
3
5
  [
4
6
  string,
@@ -17,8 +19,8 @@ export type RawPipelineNode = {
17
19
  ioInfo: RawPipelineNodeIO[];
18
20
  name: string;
19
21
  parentId?: number;
22
+ deviceNode?: boolean;
20
23
  };
21
- export type RawPipelineBridge = [number, number];
22
24
  export type RawPipelineEdge = {
23
25
  node1Id: number;
24
26
  node1Output: string;
@@ -37,16 +39,6 @@ export type Pipeline = {
37
39
  nodes: ParsedNode[];
38
40
  edges: Edge[];
39
41
  };
40
- export type ParsedHandle = {
41
- id: number;
42
- name: string;
43
- type: 'input' | 'output';
44
- blocking: boolean;
45
- queueSize: number;
46
- connected: boolean;
47
- maxQueueSize?: number;
48
- fps?: number;
49
- };
50
42
  export type ParsedNode = Node<{
51
43
  id: string;
52
44
  parentId?: string;
@@ -55,6 +47,8 @@ export type ParsedNode = Node<{
55
47
  input: ParsedHandle[];
56
48
  output: ParsedHandle[];
57
49
  };
50
+ nodeType?: 'device' | 'host';
51
+ extras?: NodeExtras;
58
52
  }>;
59
53
  export declare function parsePipeline(rawPayload: RawPipelinePayload): Pipeline;
60
54
  export declare const DOCS_BASE_URL = "https://docs.luxonis.com/software-v3/depthai/depthai-components/nodes";
@@ -1,74 +1,78 @@
1
- import { MarkerType } from '@xyflow/react';
2
- 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
- connected: true,
16
- };
17
- parsedHandles[type].push(parsed);
18
- }
19
- return parsedHandles;
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' ? undefined : node.parentId?.toString(),
53
- 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',
54
39
  data: {
55
40
  id: id.toString(),
56
41
  name: node.name,
57
42
  handles: parseHandles(node.ioInfo),
43
+ nodeType: node.deviceNode === undefined
44
+ ? undefined
45
+ : node.deviceNode
46
+ ? 'device'
47
+ : 'host',
58
48
  },
59
49
  })) ?? [];
50
+ // Check for parent nodes and if there is children with some parent node that doesn't exist the create it as fake node
51
+ const parentNodesIdsRaw = [
52
+ ...new Set(nodes
53
+ .map((node) => {
54
+ if (node.parentId !== undefined && node.parentId !== '-1') {
55
+ return node.parentId;
56
+ }
57
+ else {
58
+ return null;
59
+ }
60
+ })
61
+ .filter((id) => id !== null)),
62
+ ];
63
+ const parentNodesArr = parentNodesIdsRaw.map((id) => pipeline.nodes.find(([_, node]) => node.id.toString() === id) ??
64
+ addFakeNode(id));
60
65
  // Set all parent nodes as group nodes
61
- const mappedParentNodes = nodes.map(node => node.parentId).filter(id => id !== undefined);
62
- const filteredParentNodes = mappedParentNodes.filter(id => id !== '-1');
63
- const parentNodes = [...new Set(filteredParentNodes)];
64
- const parentNodesArr = parentNodes
65
- .map(id => pipeline.nodes.find(([_, node]) => node.id.toString() === id))
66
- .filter(node => node !== undefined);
67
66
  const formattedParentNodes = parentNodesArr.map(([id, node]) => ({
68
67
  type: 'group',
69
68
  id: id.toString(),
70
69
  position: { x: 0, y: 0 },
71
- parentId: node.parentId?.toString() === '-1' ? undefined : node.parentId?.toString(),
70
+ parentId: node.parentId?.toString() === '-1' || node.parentId === undefined
71
+ ? undefined
72
+ : `${node.parentId?.toString()}-parent`,
73
+ extent: node.parentId?.toString() === '-1' || node.parentId === undefined
74
+ ? undefined
75
+ : 'parent',
72
76
  data: {
73
77
  id: id.toString(),
74
78
  name: node.name,
@@ -80,9 +84,10 @@ export function parsePipeline(rawPayload) {
80
84
  background: 'rgba(0,0,0,0.125)',
81
85
  },
82
86
  })) ?? [];
83
- const parentNodesIds = parentNodes.map(id => id.toString());
87
+ // Children nodes
88
+ const parentNodesIds = parentNodesIdsRaw.map((id) => id.toString());
84
89
  const childrenNodes = nodes
85
- .map(node => {
90
+ .map((node) => {
86
91
  if (node.parentId && parentNodesIds.includes(node.parentId)) {
87
92
  return {
88
93
  ...node,
@@ -101,11 +106,15 @@ export function parsePipeline(rawPayload) {
101
106
  return null;
102
107
  }
103
108
  })
104
- .filter(node => node !== null);
109
+ .filter((node) => node !== null && node !== undefined);
110
+ // Non-parent and non-child nodes
105
111
  const filteredNodes = nodes
106
- .filter(node => childrenNodes.find(childNode => childNode.id === node.id) !== undefined ? null : node)
107
- .filter(node => node !== null);
108
- const newFormattedParentNodes = formattedParentNodes.map(node => ({
112
+ .filter((node) => childrenNodes.find((childNode) => childNode.id === node.id) !== undefined
113
+ ? null
114
+ : node)
115
+ .filter((node) => node !== null);
116
+ // After formatting and filtering set the parent nodes ids to match with children parentIds
117
+ const parentNodes = formattedParentNodes.map((node) => ({
109
118
  ...node,
110
119
  id: `${node.id}-parent`,
111
120
  data: {
@@ -113,14 +122,14 @@ export function parsePipeline(rawPayload) {
113
122
  id: `${node.data.id}-parent`,
114
123
  },
115
124
  }));
116
- const edges = pipeline.connections.map(connection => ({
125
+ const edges = pipeline.connections.map((connection) => ({
117
126
  id: `${connection.node1Id}-${connection.node2Id}-${connection.node1Output}-${connection.node2Input}-edge`,
118
127
  source: connection.node1Id.toString(),
119
128
  target: connection.node2Id.toString(),
120
129
  sourceHandle: connection.node1Output,
121
130
  targetHandle: connection.node2Input,
122
131
  })) ?? [];
123
- const bridges = pipeline.bridges?.map(bridge => ({
132
+ const bridges = pipeline.bridges?.map((bridge) => ({
124
133
  id: `${bridge[0]}-${bridge[1]}-bridge`,
125
134
  source: bridge[0].toString(),
126
135
  target: bridge[1].toString(),
@@ -136,9 +145,12 @@ export function parsePipeline(rawPayload) {
136
145
  type: 'step',
137
146
  })) ?? [];
138
147
  // NOTE: Parent nodes should be rendered before child nodes
139
- const groupedNodes = [...newFormattedParentNodes, ...childrenNodes, ...filteredNodes];
148
+ const groupedNodes = [...parentNodes, ...childrenNodes, ...filteredNodes];
140
149
  const nodesWithFilteredHandles = filterNodesHandles(groupedNodes, edges);
141
- return { nodes: [...nodesWithFilteredHandles], edges: [...edges, ...bridges] };
150
+ return {
151
+ nodes: [...nodesWithFilteredHandles],
152
+ edges: [...edges, ...bridges],
153
+ };
142
154
  }
143
155
  export const DOCS_BASE_URL = `https://docs.luxonis.com/software-v3/depthai/depthai-components/nodes`;
144
156
  export const NodesWithLinks = {
@@ -0,0 +1,2 @@
1
+ import { ParsedNode } from './pipeline';
2
+ export declare const topoSort: (nodes: ParsedNode[]) => ParsedNode[];
@@ -0,0 +1,21 @@
1
+ export const topoSort = (nodes) => {
2
+ const nodeMap = new Map(nodes.map((n) => [n.id, n]));
3
+ const result = [];
4
+ const visited = new Set();
5
+ const visit = (id) => {
6
+ if (visited.has(id))
7
+ return;
8
+ visited.add(id);
9
+ const node = nodeMap.get(id);
10
+ if (!node)
11
+ return;
12
+ // Visit parent first
13
+ if (node.parentId)
14
+ visit(node.parentId);
15
+ result.push(node);
16
+ };
17
+ for (const node of nodes) {
18
+ visit(node.id);
19
+ }
20
+ return result;
21
+ };
package/package.json CHANGED
@@ -1,64 +1,50 @@
1
1
  {
2
- "name": "@luxonis/depthai-pipeline-lib",
3
- "version": "1.11.0",
4
- "type": "module",
5
- "license": "UNLICENSED",
6
- "main": "./dist/src/index.js",
7
- "scripts": {
8
- "build": "./build.sh",
9
- "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
10
- "gen:styles": "panda codegen",
11
- "prepublishOnly": "npm run build"
12
- },
13
- "dependencies": {
14
- "@dagrejs/dagre": "^1.1.3",
15
- "@luxonis/common-fe-components": "1.25.9",
16
- "@xyflow/react": "^12.0.4",
17
- "postcss-import": "^16.1.0",
18
- "postcss-nested": "^6.2.0",
19
- "postcss-preset-env": "^10.0.0",
20
- "rehype-sanitize": "^6.0.0"
21
- },
22
- "peerDependencies": {
23
- "react": "^18.3.1 || ^19.0.0",
24
- "react-dom": "^18.3.1 || ^19.0.0"
25
- },
26
- "devDependencies": {
27
- "@pandacss/dev": "^0.53.0",
28
- "@stylistic/eslint-plugin": "^2.6.1",
29
- "@types/react": "^18.3.1 || ^19.0.0",
30
- "@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
- "postcss": "^8.4.31",
46
- "prettier": "^3.2.5",
47
- "prettier-eslint": "^16.3.0",
48
- "typescript": "^5.2.2"
49
- },
50
- "overrides": {
51
- "@radix-ui/react-alert-dialog": "1.0.5"
52
- },
53
- "files": [
54
- "dist/src/*",
55
- "dist/*.css"
56
- ],
57
- "exports": {
58
- ".": "./dist/src/index.js",
59
- "./styles": "./dist/index.css"
60
- },
61
- "publishConfig": {
62
- "access": "public"
63
- }
2
+ "name": "@luxonis/depthai-pipeline-lib",
3
+ "version": "1.13.0",
4
+ "type": "module",
5
+ "license": "UNLICENSED",
6
+ "main": "./dist/src/index.js",
7
+ "scripts": {
8
+ "build": "./build.sh",
9
+ "lint": "biome check .",
10
+ "lint:fix": "biome check . --write",
11
+ "gen:styles": "panda codegen",
12
+ "prepublishOnly": "npm run build"
13
+ },
14
+ "dependencies": {
15
+ "@dagrejs/dagre": "^1.1.3",
16
+ "@luxonis/common-fe-components": "1.25.9",
17
+ "@xyflow/react": "^12.0.4",
18
+ "postcss-import": "^16.1.0",
19
+ "postcss-nested": "^6.2.0",
20
+ "postcss-preset-env": "^10.0.0",
21
+ "rehype-sanitize": "^6.0.0"
22
+ },
23
+ "peerDependencies": {
24
+ "react": "^18.3.1 || ^19.0.0",
25
+ "react-dom": "^18.3.1 || ^19.0.0"
26
+ },
27
+ "devDependencies": {
28
+ "@biomejs/biome": "2.4.6",
29
+ "@pandacss/dev": "^0.53.0",
30
+ "@types/react": "^18.3.1 || ^19.0.0",
31
+ "@types/react-dom": "^18.3.1 || ^19.0.0",
32
+ "postcss": "^8.4.31",
33
+ "prettier": "^3.2.5",
34
+ "typescript": "^5.2.2"
35
+ },
36
+ "overrides": {
37
+ "@radix-ui/react-alert-dialog": "1.0.5"
38
+ },
39
+ "files": [
40
+ "dist/src/*",
41
+ "dist/*.css"
42
+ ],
43
+ "exports": {
44
+ ".": "./dist/src/index.js",
45
+ "./styles": "./dist/index.css"
46
+ },
47
+ "publishConfig": {
48
+ "access": "public"
49
+ }
64
50
  }