@luxonis/depthai-pipeline-lib 1.12.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/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
  }
@@ -1,19 +1,31 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Button, Flex, HelpIcon, Label, SubLabel, } from '@luxonis/common-fe-components';
2
+ import { Badge, Button, Flex, HelpIcon, Label, SubLabel, } from '@luxonis/common-fe-components';
3
3
  import { clsx } from '@luxonis/common-fe-components/helpers';
4
4
  import { Handle, Position } from '@xyflow/react';
5
5
  import { DOCS_BASE_URL, NodesWithLinks, } from '../services/pipeline.js';
6
+ import { formatTiming } from '../services/pipeline-state.js';
6
7
  import { css } from '../styled-system/css/css.mjs';
8
+ import { MIN_NODE_WIDTH } from './PipelineCanvas.js';
7
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));
@@ -29,6 +41,9 @@ const NodeHandlesSubLabel = (props) => {
29
41
  ? `[${fps ? `${fps.toFixed(1)} | ` : ''}${`${maxQueueSize ?? 0}/${queueSize}`}] ${name}`
30
42
  : `${fps ? `[${fps.toFixed(1)}]` : ''} ${name}`, break: "none" }));
31
43
  };
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
+ };
32
47
  export const PipelineNode = (props) => {
33
48
  const { data: node } = props;
34
49
  // biome-ignore lint/suspicious/noPrototypeBuiltins: Intended
@@ -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: {
@@ -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>;
@@ -3,39 +3,37 @@ import Dagre from '@dagrejs/dagre';
3
3
  import { Flex, Header } from '@luxonis/common-fe-components';
4
4
  import { Panel, ReactFlow, ReactFlowProvider, useEdgesState, useNodesState, useReactFlow, useStore, } from '@xyflow/react';
5
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
25
  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
- };
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;
36
+ const HEADER_HEIGHT = 24;
39
37
  // Build a tree: find all group nodes and their depth
40
38
  const groupNodes = nodes.filter((node) => node.type === 'group');
41
39
  const childNodes = nodes.filter((node) => node.type !== 'group');
@@ -49,12 +47,11 @@ const adjustNodes = (nodes, edges) => {
49
47
  const sortedGroups = [...groupNodes].sort((a, b) => getDepth(b.id) - getDepth(a.id));
50
48
  // Track resolved sizes for groups as we process them
51
49
  const resolvedSizes = {};
52
- const resolvedPositions = {};
53
50
  // Start with all non-group nodes at their measured sizes
54
51
  for (const node of childNodes) {
55
52
  resolvedSizes[node.id] = {
56
- width: node.measured?.width ?? 300,
57
- height: node.measured?.height ?? 150,
53
+ width: Math.min(node.measured?.width ?? MIN_NODE_WIDTH),
54
+ height: Math.min(node.measured?.height ?? MIN_NODE_HEIGHT),
58
55
  };
59
56
  }
60
57
  const updatedNodes = {};
@@ -64,14 +61,20 @@ const adjustNodes = (nodes, edges) => {
64
61
  for (const group of sortedGroups) {
65
62
  const children = nodes.filter((n) => n.parentId === group.id);
66
63
  if (children.length === 0) {
67
- resolvedSizes[group.id] = { width: 300, height: 150 };
64
+ resolvedSizes[group.id] = {
65
+ width: MIN_NODE_WIDTH,
66
+ height: MIN_NODE_HEIGHT,
67
+ };
68
68
  continue;
69
69
  }
70
70
  const g = new Dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({}));
71
- g.setGraph({ rankdir: 'LR', ranksep: 60, nodesep: 20 });
71
+ g.setGraph({ rankdir: 'LR' });
72
72
  for (const child of children) {
73
73
  // Use resolved size (important for nested groups!)
74
- const size = resolvedSizes[child.id] ?? { width: 300, height: 150 };
74
+ const size = resolvedSizes[child.id] ?? {
75
+ width: MIN_NODE_WIDTH,
76
+ height: MIN_NODE_HEIGHT,
77
+ };
75
78
  g.setNode(child.id, { width: size.width, height: size.height });
76
79
  }
77
80
  const groupEdges = edges.filter((e) => children.some((c) => c.id === e.source) &&
@@ -84,39 +87,37 @@ const adjustNodes = (nodes, edges) => {
84
87
  let minY = 9999;
85
88
  let maxX = -9999;
86
89
  let maxY = -9999;
90
+ // Replace both child loops with this single pass:
87
91
  for (const child of children) {
88
92
  const pos = g.node(child.id);
89
- const size = resolvedSizes[child.id] ?? { width: 300, height: 150 };
93
+ const size = resolvedSizes[child.id] ?? {
94
+ width: MIN_NODE_WIDTH,
95
+ height: MIN_NODE_HEIGHT,
96
+ };
90
97
  const x = pos.x - size.width / 2;
91
98
  const y = pos.y - size.height / 2;
92
99
  minX = Math.min(minX, x);
93
100
  minY = Math.min(minY, y);
94
101
  maxX = Math.max(maxX, x + size.width);
95
102
  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
103
  }
106
- // Re-pass to fix positions now that minX/minY are known
104
+ // Now positions are correct, apply them
107
105
  for (const child of children) {
108
106
  const pos = g.node(child.id);
109
- const size = resolvedSizes[child.id] ?? { width: 300, height: 150 };
107
+ const size = resolvedSizes[child.id] ?? {
108
+ width: MIN_NODE_WIDTH,
109
+ height: MIN_NODE_HEIGHT,
110
+ };
110
111
  updatedNodes[child.id] = {
111
112
  ...updatedNodes[child.id],
112
113
  position: {
113
114
  x: pos.x - size.width / 2 - minX + PADDING,
114
- y: pos.y - size.height / 2 - minY + PADDING,
115
+ y: pos.y - size.height / 2 - minY + PADDING + HEADER_HEIGHT,
115
116
  },
116
117
  };
117
118
  }
118
119
  const groupW = maxX - minX + PADDING * 2;
119
- const groupH = maxY - minY + PADDING * 2;
120
+ const groupH = maxY - minY + PADDING * 2 + HEADER_HEIGHT;
120
121
  // Store this group's resolved size for its own parent to use
121
122
  resolvedSizes[group.id] = { width: groupW, height: groupH };
122
123
  updatedNodes[group.id] = {
@@ -128,10 +129,13 @@ const adjustNodes = (nodes, edges) => {
128
129
  }
129
130
  // Now lay out the top-level (root) nodes using Dagre with correct sizes
130
131
  const graph = new Dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({}));
131
- graph.setGraph({ rankdir: 'LR', ranksep: 80, nodesep: 40 });
132
+ graph.setGraph({ rankdir: 'LR' });
132
133
  const rootNodes = nodes.filter((n) => !n.parentId);
133
134
  for (const node of rootNodes) {
134
- const size = resolvedSizes[node.id] ?? { width: 300, height: 150 };
135
+ const size = resolvedSizes[node.id] ?? {
136
+ width: MIN_NODE_WIDTH,
137
+ height: MIN_NODE_HEIGHT,
138
+ };
135
139
  graph.setNode(node.id, { width: size.width, height: size.height });
136
140
  }
137
141
  const rootEdges = edges.filter((e) => rootNodes.some((n) => n.id === e.source) &&
@@ -144,7 +148,10 @@ const adjustNodes = (nodes, edges) => {
144
148
  const pos = graph.node(node.id);
145
149
  if (!pos)
146
150
  continue;
147
- const size = resolvedSizes[node.id] ?? { width: 300, height: 150 };
151
+ const size = resolvedSizes[node.id] ?? {
152
+ width: MIN_NODE_WIDTH,
153
+ height: MIN_NODE_HEIGHT,
154
+ };
148
155
  updatedNodes[node.id] = {
149
156
  ...updatedNodes[node.id],
150
157
  position: {
@@ -153,33 +160,13 @@ const adjustNodes = (nodes, edges) => {
153
160
  },
154
161
  };
155
162
  }
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
163
  return topoSort(Object.values(updatedNodes));
178
164
  };
179
165
  export const updateNodesOnPipelineStateChange = (nodes, pipelineState) => {
180
166
  const parsedNodes = [];
181
167
  for (const node of nodes) {
182
168
  const nodeState = pipelineState.find((state) => state.id.toString() === node.id.toString());
169
+ // Inputs and outputs
183
170
  const inputHandles = node.data.handles.input;
184
171
  const outputHandles = node.data.handles.output;
185
172
  const newInputHandles = [];
@@ -189,6 +176,7 @@ export const updateNodesOnPipelineStateChange = (nodes, pipelineState) => {
189
176
  if (inputState) {
190
177
  obj.maxQueueSize = inputState.numQueued;
191
178
  obj.fps = inputState.timing.fps;
179
+ obj.dotColor = inputState.dotColor;
192
180
  }
193
181
  newInputHandles.push(obj);
194
182
  }
@@ -199,9 +187,14 @@ export const updateNodesOnPipelineStateChange = (nodes, pipelineState) => {
199
187
  if (outputState) {
200
188
  obj.maxQueueSize = outputState.numQueued;
201
189
  obj.fps = outputState.timing.fps;
190
+ obj.dotColor = outputState.dotColor;
202
191
  }
203
192
  newOutputHandles.push(obj);
204
193
  }
194
+ // GPST timings
195
+ const gpst = nodeState?.gpstTimings;
196
+ // Node state
197
+ const state = nodeState?.stateInfo;
205
198
  parsedNodes.push({
206
199
  ...node,
207
200
  data: {
@@ -210,12 +203,16 @@ export const updateNodesOnPipelineStateChange = (nodes, pipelineState) => {
210
203
  input: newInputHandles,
211
204
  output: newOutputHandles,
212
205
  },
206
+ extras: {
207
+ gpstTimings: gpst,
208
+ stateInfo: state,
209
+ },
213
210
  },
214
211
  });
215
212
  }
216
213
  return parsedNodes;
217
214
  };
218
- const PipelineCanvasBody = ({ pipeline, pipelineState, action, ...flexProps }) => {
215
+ const PipelineCanvasBody = ({ pipeline, pipelineState: pipelineStateParsed, action, ...flexProps }) => {
219
216
  const { fitView, setViewport, getViewport } = useReactFlow();
220
217
  const autoArrangeRef = React.useRef(true);
221
218
  const widthSelector = (state) => state.width;
@@ -223,6 +220,7 @@ const PipelineCanvasBody = ({ pipeline, pipelineState, action, ...flexProps }) =
223
220
  const reactFlowWidth = useStore(widthSelector);
224
221
  const reactFlowHeight = useStore(heightSelector);
225
222
  const [shouldFitAndResize, setShouldFitAndResize] = React.useState(false);
223
+ const pipelineState = React.useMemo(() => pipelineStateParsed, [pipelineStateParsed]);
226
224
  // biome-ignore lint/correctness/useExhaustiveDependencies: Intended
227
225
  React.useEffect(() => {
228
226
  void fitView();
@@ -235,12 +233,13 @@ const PipelineCanvasBody = ({ pipeline, pipelineState, action, ...flexProps }) =
235
233
  return;
236
234
  }
237
235
  const layouted = getLayoutedElements(pipeline?.nodes ?? [], pipeline?.edges ?? []);
238
- const adjustedNodes = adjustNodes([...layouted.nodes], [...layouted.edges]);
239
236
  if (pipelineState) {
240
- const updatedNodes = updateNodesOnPipelineStateChange(adjustedNodes, pipelineState);
241
- setNodes([...updatedNodes]);
237
+ const updatedNodes = updateNodesOnPipelineStateChange([...layouted.nodes], pipelineState);
238
+ const adjustedNodes = adjustNodes([...updatedNodes], layouted.edges);
239
+ setNodes([...adjustedNodes]);
242
240
  }
243
241
  else {
242
+ const adjustedNodes = adjustNodes([...layouted.nodes], layouted.edges);
244
243
  setNodes([...adjustedNodes]);
245
244
  }
246
245
  setEdges([...layouted.edges]);
@@ -265,17 +264,6 @@ const PipelineCanvasBody = ({ pipeline, pipelineState, action, ...flexProps }) =
265
264
  await fitView({ nodes: nds });
266
265
  };
267
266
  // biome-ignore lint/correctness/useExhaustiveDependencies: Intended
268
- React.useEffect(() => {
269
- if (!autoArrangeRef.current) {
270
- return;
271
- }
272
- if (nodes.length > 0 && edges.length > 0) {
273
- const viewport = getViewport();
274
- void setViewportAndFit(viewport.x, viewport.y, viewport.zoom, nodes);
275
- }
276
- // eslint-disable-next-line react-hooks/exhaustive-deps
277
- }, [nodes, edges]);
278
- // biome-ignore lint/correctness/useExhaustiveDependencies: Intended
279
267
  React.useEffect(() => {
280
268
  if (shouldFitAndResize) {
281
269
  const viewport = getViewport();
@@ -1,5 +1,6 @@
1
1
  import type { Edge } from '@xyflow/react';
2
2
  import type { ParsedNode, RawPipelineNodeIO } from './pipeline';
3
+ import { PipelineStateDotColor } from './pipeline-state';
3
4
  export type RawPipelineBridge = [number, number];
4
5
  export type ParsedHandle = {
5
6
  id: number;
@@ -10,6 +11,7 @@ export type ParsedHandle = {
10
11
  connected: boolean;
11
12
  maxQueueSize?: number;
12
13
  fps?: number;
14
+ dotColor?: PipelineStateDotColor;
13
15
  };
14
16
  export declare function parseHandles(handles: RawPipelineNodeIO[]): {
15
17
  input: ParsedHandle[];
@@ -27,6 +29,7 @@ export declare function filterNodesHandles(nodes: ParsedNode[], edges: Edge[]):
27
29
  queueSize: number;
28
30
  maxQueueSize?: number;
29
31
  fps?: number;
32
+ dotColor?: PipelineStateDotColor;
30
33
  }[];
31
34
  output: {
32
35
  connected: boolean;
@@ -37,11 +40,14 @@ export declare function filterNodesHandles(nodes: ParsedNode[], edges: Edge[]):
37
40
  queueSize: number;
38
41
  maxQueueSize?: number;
39
42
  fps?: number;
43
+ dotColor?: PipelineStateDotColor;
40
44
  }[];
41
45
  };
42
46
  id: string;
43
47
  parentId?: string;
44
48
  name: string;
49
+ nodeType?: "device" | "host";
50
+ extras?: import("./pipeline-state").NodeExtras;
45
51
  };
46
52
  id: string;
47
53
  position: import("@xyflow/system").XYPosition;
@@ -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,5 +1,6 @@
1
1
  import { type Edge, type Node } from '@xyflow/react';
2
2
  import { type ParsedHandle } from './pipeline-handles';
3
+ import { NodeExtras } from './pipeline-state';
3
4
  export type RawPipelineNodeIO = [
4
5
  [
5
6
  string,
@@ -18,6 +19,7 @@ export type RawPipelineNode = {
18
19
  ioInfo: RawPipelineNodeIO[];
19
20
  name: string;
20
21
  parentId?: number;
22
+ deviceNode?: boolean;
21
23
  };
22
24
  export type RawPipelineEdge = {
23
25
  node1Id: number;
@@ -45,6 +47,8 @@ export type ParsedNode = Node<{
45
47
  input: ParsedHandle[];
46
48
  output: ParsedHandle[];
47
49
  };
50
+ nodeType?: 'device' | 'host';
51
+ extras?: NodeExtras;
48
52
  }>;
49
53
  export declare function parsePipeline(rawPayload: RawPipelinePayload): Pipeline;
50
54
  export declare const DOCS_BASE_URL = "https://docs.luxonis.com/software-v3/depthai/depthai-components/nodes";
@@ -40,6 +40,11 @@ export function parsePipeline(rawPayload) {
40
40
  id: id.toString(),
41
41
  name: node.name,
42
42
  handles: parseHandles(node.ioInfo),
43
+ nodeType: node.deviceNode === undefined
44
+ ? undefined
45
+ : node.deviceNode
46
+ ? 'device'
47
+ : 'host',
43
48
  },
44
49
  })) ?? [];
45
50
  // Check for parent nodes and if there is children with some parent node that doesn't exist the create it as fake node
@@ -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,6 +1,6 @@
1
1
  {
2
2
  "name": "@luxonis/depthai-pipeline-lib",
3
- "version": "1.12.0",
3
+ "version": "1.13.0",
4
4
  "type": "module",
5
5
  "license": "UNLICENSED",
6
6
  "main": "./dist/src/index.js",