@luxonis/depthai-pipeline-lib 1.12.0 → 1.14.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
@@ -1252,6 +1252,22 @@
1252
1252
 
1253
1253
  @layer utilities{
1254
1254
 
1255
+ .w_16px {
1256
+ width: 16px;
1257
+ }
1258
+
1259
+ .h_16px {
1260
+ height: 16px;
1261
+ }
1262
+
1263
+ .d_flex {
1264
+ display: flex;
1265
+ }
1266
+
1267
+ .pointer-events_none {
1268
+ pointer-events: none;
1269
+ }
1270
+
1255
1271
  .min-w_container\.smaller\.xxs {
1256
1272
  min-width: var(--sizes-container-smaller-xxs);
1257
1273
  }
@@ -1297,12 +1313,12 @@
1297
1313
  border-top-right-radius: var(--radii-common);
1298
1314
  }
1299
1315
 
1300
- .w_icon\.sm {
1301
- width: var(--sizes-icon-sm);
1316
+ .w_20px {
1317
+ width: 20px;
1302
1318
  }
1303
1319
 
1304
- .h_icon\.sm {
1305
- height: var(--sizes-icon-sm);
1320
+ .h_20px {
1321
+ height: 20px;
1306
1322
  }
1307
1323
 
1308
1324
  .w_auto {
@@ -1313,6 +1329,14 @@
1313
1329
  height: auto;
1314
1330
  }
1315
1331
 
1332
+ .ai_center {
1333
+ align-items: center;
1334
+ }
1335
+
1336
+ .jc_center {
1337
+ justify-content: center;
1338
+ }
1339
+
1316
1340
  .border-style_dashed {
1317
1341
  border-style: dashed;
1318
1342
  }
@@ -1321,14 +1345,22 @@
1321
1345
  background-color: rgba(0,0,0,0.125);
1322
1346
  }
1323
1347
 
1324
- .bg-c_dark\.error\! {
1325
- background-color: var(--colors-dark-error) !important;
1348
+ .bg-c_dark\.warning\! {
1349
+ background-color: var(--colors-dark-warning) !important;
1326
1350
  }
1327
1351
 
1328
1352
  .bg-c_dark\.success\! {
1329
1353
  background-color: var(--colors-dark-success) !important;
1330
1354
  }
1331
1355
 
1356
+ .bg-c_dark\.error\! {
1357
+ background-color: var(--colors-dark-error) !important;
1358
+ }
1359
+
1360
+ .bg-c_dark\.gray\! {
1361
+ background-color: var(--colors-dark-gray) !important;
1362
+ }
1363
+
1332
1364
  .ml_xs {
1333
1365
  margin-left: var(--spacing-xs);
1334
1366
  }
@@ -0,0 +1,7 @@
1
+ import { type EdgeProps } from '@xyflow/react';
2
+ type BridgeEdgeProps = EdgeProps & {
3
+ data: any;
4
+ type: any;
5
+ };
6
+ export declare const BridgeEdge: ({ id, sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition, style, markerStart, markerEnd, interactionWidth, pathOptions, }: BridgeEdgeProps) => import("react/jsx-runtime").JSX.Element;
7
+ export {};
@@ -0,0 +1,58 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { ArrowRightSideIcon, Icon } from '@luxonis/common-fe-components/icons';
3
+ import { BaseEdge, EdgeLabelRenderer, getSmoothStepPath, } from '@xyflow/react';
4
+ import React from 'react';
5
+ import { css } from '../styled-system/css/css.mjs';
6
+ const bridgeArrowClassName = css({
7
+ width: '16px',
8
+ height: '16px',
9
+ display: 'flex',
10
+ alignItems: 'center',
11
+ justifyContent: 'center',
12
+ pointerEvents: 'none',
13
+ });
14
+ export const BridgeEdge = ({ id, sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition, style, markerStart, markerEnd, interactionWidth, pathOptions, }) => {
15
+ const pathRef = React.useRef(null);
16
+ const [path, labelX, labelY] = getSmoothStepPath({
17
+ sourceX,
18
+ sourceY,
19
+ targetX,
20
+ targetY,
21
+ sourcePosition,
22
+ targetPosition,
23
+ borderRadius: pathOptions?.borderRadius,
24
+ offset: pathOptions?.offset,
25
+ });
26
+ const [arrowState, setArrowState] = React.useState({
27
+ x: labelX,
28
+ y: labelY,
29
+ angle: 0,
30
+ });
31
+ React.useEffect(() => {
32
+ const pathElement = pathRef.current;
33
+ if (!pathElement) {
34
+ return;
35
+ }
36
+ const totalLength = pathElement.getTotalLength();
37
+ const halfLength = totalLength / 2;
38
+ const sampleOffset = Math.min(12, totalLength / 4);
39
+ const startPoint = pathElement.getPointAtLength(Math.max(0, halfLength - sampleOffset));
40
+ const endPoint = pathElement.getPointAtLength(Math.min(totalLength, halfLength + sampleOffset));
41
+ const nextArrowState = {
42
+ x: (startPoint.x + endPoint.x) / 2,
43
+ y: (startPoint.y + endPoint.y) / 2,
44
+ angle: Math.atan2(endPoint.y - startPoint.y, endPoint.x - startPoint.x) *
45
+ (180 / Math.PI),
46
+ };
47
+ setArrowState((currentArrowState) => {
48
+ const didChange = Math.abs(currentArrowState.x - nextArrowState.x) > 0.5 ||
49
+ Math.abs(currentArrowState.y - nextArrowState.y) > 0.5 ||
50
+ Math.abs(currentArrowState.angle - nextArrowState.angle) > 0.5;
51
+ return didChange ? nextArrowState : currentArrowState;
52
+ });
53
+ });
54
+ return (_jsxs(_Fragment, { children: [_jsx("path", { ref: pathRef, d: path, fill: "none", stroke: "transparent" }), _jsx(BaseEdge, { id: id, path: path, style: style, markerStart: markerStart, markerEnd: markerEnd, interactionWidth: interactionWidth }), _jsx(EdgeLabelRenderer, { children: _jsx("div", { className: bridgeArrowClassName, style: {
55
+ position: 'absolute',
56
+ transform: `translate(-50%, -50%) translate(${arrowState.x}px, ${arrowState.y}px) rotate(${arrowState.angle}deg)`,
57
+ }, children: _jsx(Icon, { icon: ArrowRightSideIcon, size: "sm", color: "gray", pointerEvents: "none" }) }) })] }));
58
+ };
@@ -1,22 +1,41 @@
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
- import { DOCS_BASE_URL, NodesWithLinks, } from '../services/pipeline.js';
5
+ import { formatTiming } from '../services/pipeline-state.js';
6
+ import { DOCS_BASE_URL, MIN_NODE_WIDTH, NodesWithLinks, } from '../services/utils.js';
6
7
  import { css } from '../styled-system/css/css.mjs';
7
8
  const NodeHandles = (props) => {
8
9
  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, }) => {
10
+ 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
11
  if (!connected) {
11
12
  return;
12
13
  }
13
14
  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
15
  width: 'custom.handle.dot!',
15
16
  height: 'custom.handle.dot!',
16
- backgroundColor: blocking ? 'dark.error!' : 'dark.success!',
17
+ backgroundColor: dotColor === 'gray'
18
+ ? 'dark.gray!'
19
+ : dotColor === 'green'
20
+ ? 'dark.success!'
21
+ : dotColor === 'red'
22
+ ? 'dark.error!'
23
+ : dotColor === 'yellow'
24
+ ? 'dark.warning!'
25
+ : 'dark.success!',
17
26
  border: 'none!',
18
27
  borderRadius: blocking ? '0% !important' : '100% !important',
19
- }) }), _jsx(NodeHandlesSubLabel, { type: type, name: name, queueSize: queueSize, maxQueueSize: maxQueueSize, fps: fps })] }, id));
28
+ }), style: {
29
+ background: dotColor === 'gray'
30
+ ? 'dark.gray!'
31
+ : dotColor === 'green'
32
+ ? 'dark.success!'
33
+ : dotColor === 'red'
34
+ ? 'dark.error!'
35
+ : dotColor === 'yellow'
36
+ ? 'dark.warning!'
37
+ : 'dark.gray!',
38
+ } }), _jsx(NodeHandlesSubLabel, { type: type, name: name, queueSize: queueSize, maxQueueSize: maxQueueSize, fps: fps })] }, id));
20
39
  }) }));
21
40
  };
22
41
  const NodeHandlesSubLabel = (props) => {
@@ -29,6 +48,9 @@ const NodeHandlesSubLabel = (props) => {
29
48
  ? `[${fps ? `${fps.toFixed(1)} | ` : ''}${`${maxQueueSize ?? 0}/${queueSize}`}] ${name}`
30
49
  : `${fps ? `[${fps.toFixed(1)}]` : ''} ${name}`, break: "none" }));
31
50
  };
51
+ const GPSTTimings = (props) => {
52
+ 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`))) }));
53
+ };
32
54
  export const PipelineNode = (props) => {
33
55
  const { data: node } = props;
34
56
  // biome-ignore lint/suspicious/noPrototypeBuiltins: Intended
@@ -48,9 +70,22 @@ export const PipelineNode = (props) => {
48
70
  backgroundColor: 'light.gray',
49
71
  roundedTop: 'common',
50
72
  }), 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({
73
+ width: '20px',
74
+ height: '20px',
75
+ display: 'flex',
76
+ alignItems: 'center',
77
+ }), children: (node.nodeType === 'device' || node.nodeType === 'host') && (_jsx(Badge, { label: node.nodeType === 'device' ? 'D' : 'H', variant: node.nodeType === 'device' ? 'active' : 'cyan', style: {
78
+ padding: '0px',
79
+ display: 'flex',
80
+ justifyContent: 'center',
81
+ alignItems: 'center',
82
+ width: '20px',
83
+ height: '20px',
84
+ }, tooltip: node.nodeType === 'device' ? 'Device Node' : 'Host Node', cursor: "help" })) }), _jsx(Label, { text: node.id
85
+ ? `${node.name} (${node.id}) ${node.extras?.stateInfo ? `(${node.extras.stateInfo.letter})` : ''}`
86
+ : node.extras?.stateInfo
87
+ ? `${node.name} (${node.extras.stateInfo.letter})`
88
+ : node.name, color: "unset" }), _jsx(Button, { variant: "ghost", color: "transparent", icon: HelpIcon, onClick: () => window.open(link, '_blank'), className: clsx('node-help-icon', css({
54
89
  width: 'auto',
55
90
  height: 'auto',
56
91
  right: 'xs',
@@ -60,7 +95,7 @@ export const PipelineNode = (props) => {
60
95
  }), style: {
61
96
  border: 'none',
62
97
  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({
98
+ } }), _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
99
  width: 'custom.handle.dot!',
65
100
  height: 'custom.handle.dot!',
66
101
  }), style: {
@@ -1,11 +1,11 @@
1
1
  import type { FlexProps } from '@luxonis/common-fe-components';
2
2
  import React from 'react';
3
- import type { ParsedNode, Pipeline } from '../services/pipeline.js';
3
+ import type { Pipeline } from '../services/pipeline.js';
4
4
  import type { PipelineState } from '../services/pipeline-state.js';
5
5
  export type PipelineCanvasProps = FlexProps & {
6
6
  pipeline: Pipeline | null;
7
7
  pipelineState: PipelineState[] | null;
8
- action?: React.ReactNode;
8
+ header?: React.ReactNode;
9
+ isDebugging?: boolean;
9
10
  };
10
- export declare const updateNodesOnPipelineStateChange: (nodes: ParsedNode[], pipelineState: PipelineState[]) => ParsedNode[];
11
11
  export declare const PipelineCanvas: React.FC<PipelineCanvasProps>;
@@ -1,221 +1,14 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
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';
2
+ import { Button, Flex, Header } from '@luxonis/common-fe-components';
3
+ import { BezierEdge, Panel, ReactFlow, ReactFlowProvider, useEdgesState, useNodesState, useReactFlow, useStore, } from '@xyflow/react';
5
4
  import React from 'react';
5
+ import { adjustNodes, getLayoutedElements, updateNodesOnPipelineStateChange, } from '../services/utils.js';
6
+ import { BridgeEdge } from './BridgeEdge.js';
6
7
  import { PipelineNode } from './Node.js';
7
- const getLayoutedElements = (nodes, edges) => {
8
- 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');
11
- for (const edge of edges) {
12
- graph.setEdge(edge.source, edge.target);
13
- }
14
- for (const node of childNodes) {
15
- graph.setNode(node.id, {
16
- width: node.measured?.width ?? 300,
17
- height: node.measured?.height ?? 150,
18
- });
19
- }
20
- Dagre.layout(graph);
21
- 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
- };
33
- }),
34
- edges,
35
- };
36
- };
37
- const adjustNodes = (nodes, edges) => {
38
- const PADDING = 16;
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,
58
- };
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);
66
- if (children.length === 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 });
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);
83
- let minX = 9999;
84
- let minY = 9999;
85
- let maxX = -9999;
86
- let maxY = -9999;
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,
153
- },
154
- };
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));
178
- };
179
- export const updateNodesOnPipelineStateChange = (nodes, pipelineState) => {
180
- const parsedNodes = [];
181
- for (const node of nodes) {
182
- const nodeState = pipelineState.find((state) => state.id.toString() === node.id.toString());
183
- const inputHandles = node.data.handles.input;
184
- const outputHandles = node.data.handles.output;
185
- const newInputHandles = [];
186
- for (const inputHandle of inputHandles) {
187
- const obj = { ...inputHandle };
188
- const inputState = nodeState?.inputs[inputHandle.name];
189
- if (inputState) {
190
- obj.maxQueueSize = inputState.numQueued;
191
- obj.fps = inputState.timing.fps;
192
- }
193
- newInputHandles.push(obj);
194
- }
195
- const newOutputHandles = [];
196
- for (const outputHandle of outputHandles) {
197
- const obj = { ...outputHandle };
198
- const outputState = nodeState?.outputs[outputHandle.name];
199
- if (outputState) {
200
- obj.maxQueueSize = outputState.numQueued;
201
- obj.fps = outputState.timing.fps;
202
- }
203
- newOutputHandles.push(obj);
204
- }
205
- parsedNodes.push({
206
- ...node,
207
- data: {
208
- ...node.data,
209
- handles: {
210
- input: newInputHandles,
211
- output: newOutputHandles,
212
- },
213
- },
214
- });
215
- }
216
- return parsedNodes;
217
- };
218
- const PipelineCanvasBody = ({ pipeline, pipelineState, action, ...flexProps }) => {
8
+ import { PipelineLegend } from './PipelineLegend.js';
9
+ const nodeTypes = { generic: PipelineNode };
10
+ const edgeTypes = { generic: BezierEdge, bridge: BridgeEdge };
11
+ const PipelineCanvasBody = ({ pipeline, pipelineState: pipelineStateParsed, header, isDebugging = false, ...flexProps }) => {
219
12
  const { fitView, setViewport, getViewport } = useReactFlow();
220
13
  const autoArrangeRef = React.useRef(true);
221
14
  const widthSelector = (state) => state.width;
@@ -223,6 +16,8 @@ const PipelineCanvasBody = ({ pipeline, pipelineState, action, ...flexProps }) =
223
16
  const reactFlowWidth = useStore(widthSelector);
224
17
  const reactFlowHeight = useStore(heightSelector);
225
18
  const [shouldFitAndResize, setShouldFitAndResize] = React.useState(false);
19
+ const [openLegend, setOpenLegend] = React.useState(false);
20
+ const pipelineState = React.useMemo(() => pipelineStateParsed, [pipelineStateParsed]);
226
21
  // biome-ignore lint/correctness/useExhaustiveDependencies: Intended
227
22
  React.useEffect(() => {
228
23
  void fitView();
@@ -235,12 +30,13 @@ const PipelineCanvasBody = ({ pipeline, pipelineState, action, ...flexProps }) =
235
30
  return;
236
31
  }
237
32
  const layouted = getLayoutedElements(pipeline?.nodes ?? [], pipeline?.edges ?? []);
238
- const adjustedNodes = adjustNodes([...layouted.nodes], [...layouted.edges]);
239
33
  if (pipelineState) {
240
- const updatedNodes = updateNodesOnPipelineStateChange(adjustedNodes, pipelineState);
241
- setNodes([...updatedNodes]);
34
+ const updatedNodes = updateNodesOnPipelineStateChange([...layouted.nodes], pipelineState);
35
+ const adjustedNodes = adjustNodes([...updatedNodes], layouted.edges);
36
+ setNodes([...adjustedNodes]);
242
37
  }
243
38
  else {
39
+ const adjustedNodes = adjustNodes([...layouted.nodes], layouted.edges);
244
40
  setNodes([...adjustedNodes]);
245
41
  }
246
42
  setEdges([...layouted.edges]);
@@ -265,17 +61,6 @@ const PipelineCanvasBody = ({ pipeline, pipelineState, action, ...flexProps }) =
265
61
  await fitView({ nodes: nds });
266
62
  };
267
63
  // 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
64
  React.useEffect(() => {
280
65
  if (shouldFitAndResize) {
281
66
  const viewport = getViewport();
@@ -284,6 +69,6 @@ const PipelineCanvasBody = ({ pipeline, pipelineState, action, ...flexProps }) =
284
69
  }
285
70
  // eslint-disable-next-line react-hooks/exhaustive-deps
286
71
  }, [shouldFitAndResize]);
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 }) }))] }));
72
+ 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 && (_jsxs(ReactFlow, { nodes: nodes, edges: edges, onNodeDragStart: () => (autoArrangeRef.current = false), onNodesChange: onNodesChange, fitView: true, nodeTypes: nodeTypes, edgeTypes: edgeTypes, minZoom: 0.4, children: [header && _jsx(Panel, { position: "top-center", children: header }), _jsx(Panel, { position: "top-right", children: _jsxs(Flex, { gap: "xs", flexDirection: "column", children: [_jsx(Button, { onClick: () => setOpenLegend(!openLegend), label: openLegend ? 'Collapse' : 'Legend' }), openLegend && _jsx(PipelineLegend, { isDebugging: isDebugging })] }) })] }))] }));
288
73
  };
289
74
  export const PipelineCanvas = (props) => (_jsx(ReactFlowProvider, { children: _jsx(PipelineCanvasBody, { ...props }) }));
@@ -0,0 +1,6 @@
1
+ import React from 'react';
2
+ type PipelineLegendProps = {
3
+ isDebugging: boolean;
4
+ };
5
+ export declare const PipelineLegend: React.FC<PipelineLegendProps>;
6
+ export {};
@@ -0,0 +1,181 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { Badge, Flex, Label, SubLabel } from '@luxonis/common-fe-components';
3
+ import React from 'react';
4
+ const debugSections = [
5
+ {
6
+ title: 'Nodes',
7
+ items: [
8
+ {
9
+ 0: {
10
+ id: 'device-node',
11
+ label: 'Device Node',
12
+ marker: (_jsx(Badge, { label: "D", variant: "active", style: { padding: '0px', paddingInline: '4px' } })),
13
+ },
14
+ 1: {
15
+ id: 'host-node',
16
+ label: 'Host Node',
17
+ marker: (_jsx(Badge, { label: "H", variant: "cyan", style: { padding: '0px', paddingInline: '4px' } })),
18
+ },
19
+ },
20
+ ],
21
+ },
22
+ {
23
+ title: 'Node States',
24
+ items: [
25
+ {
26
+ 0: {
27
+ id: 'node-state-idle',
28
+ label: 'Idle',
29
+ marker: _jsx(SubLabel, { text: "(I)", weight: "medium" }),
30
+ },
31
+ 1: {
32
+ id: 'node-state-getting-inputs',
33
+ label: 'Getting inputs',
34
+ marker: _jsx(SubLabel, { text: "(G)", weight: "medium" }),
35
+ },
36
+ },
37
+ {
38
+ 0: {
39
+ id: 'node-state-processing',
40
+ label: 'Processing',
41
+ marker: _jsx(SubLabel, { text: "(P)", weight: "medium" }),
42
+ },
43
+ 1: {
44
+ id: 'node-state-sending-outputs',
45
+ label: 'Sending outputs',
46
+ marker: _jsx(SubLabel, { text: "(S)", weight: "medium" }),
47
+ },
48
+ },
49
+ ],
50
+ },
51
+ {
52
+ title: 'GPST Timings',
53
+ items: [
54
+ {
55
+ 0: {
56
+ id: 'timing-get',
57
+ label: 'Get time',
58
+ marker: _jsx(SubLabel, { text: "G", weight: "medium" }),
59
+ },
60
+ 1: {
61
+ id: 'timing-process',
62
+ label: 'Process time',
63
+ marker: _jsx(SubLabel, { text: "P", weight: "medium" }),
64
+ },
65
+ },
66
+ {
67
+ 0: {
68
+ id: 'timing-send',
69
+ label: 'Send time',
70
+ marker: _jsx(SubLabel, { text: "S", weight: "medium" }),
71
+ },
72
+ 1: {
73
+ id: 'timing-total',
74
+ label: 'Total time',
75
+ marker: _jsx(SubLabel, { text: "T", weight: "medium" }),
76
+ },
77
+ },
78
+ ],
79
+ },
80
+ ];
81
+ const portStatusSection = [
82
+ {
83
+ title: 'Port Status',
84
+ items: [
85
+ {
86
+ 0: {
87
+ id: 'port-inactive',
88
+ label: 'Inactive',
89
+ marker: _jsx(LegendDot, { tone: "gray" }),
90
+ },
91
+ 1: {
92
+ id: 'port-ready',
93
+ label: 'Ready',
94
+ marker: _jsx(LegendDot, { tone: "green" }),
95
+ },
96
+ },
97
+ {
98
+ 0: {
99
+ id: 'port-busy',
100
+ label: 'Busy',
101
+ marker: _jsx(LegendDot, { tone: "yellow" }),
102
+ },
103
+ 1: {
104
+ id: 'port-blocked',
105
+ label: 'Blocked',
106
+ marker: _jsx(LegendDot, { tone: "red" }),
107
+ },
108
+ },
109
+ ],
110
+ },
111
+ ];
112
+ const sharedSections = [
113
+ {
114
+ title: 'Ports',
115
+ items: [
116
+ {
117
+ 0: {
118
+ id: 'blocking-port',
119
+ label: 'Blocking',
120
+ marker: _jsx(LegendDot, { tone: "gray", square: true }),
121
+ },
122
+ 1: {
123
+ id: 'non-blocking-port',
124
+ label: 'Non-blocking',
125
+ marker: _jsx(LegendDot, { tone: "gray" }),
126
+ },
127
+ },
128
+ ],
129
+ },
130
+ ];
131
+ const bridgeSection = {
132
+ title: 'Bridges',
133
+ items: [
134
+ {
135
+ 0: {
136
+ id: 'bridge-direction',
137
+ label: 'Direction from source node to target node',
138
+ marker: _jsx(SubLabel, { text: ">" }),
139
+ },
140
+ 1: null,
141
+ },
142
+ ],
143
+ };
144
+ // NOTE: Colors from hub repo
145
+ // success: '#12B76A',
146
+ // warning: '#DC6803',
147
+ // error: '#F04438',
148
+ // gray: '#667085',
149
+ function LegendDot({ tone, square = false, }) {
150
+ const color = React.useMemo(() => {
151
+ switch (tone) {
152
+ case 'gray':
153
+ return '#667085';
154
+ case 'green':
155
+ return '#12B76A';
156
+ case 'yellow':
157
+ return '#DC6803';
158
+ case 'red':
159
+ return '#F04438';
160
+ }
161
+ }, [tone]);
162
+ return (_jsx("div", { style: {
163
+ width: '8px',
164
+ height: '8px',
165
+ backgroundColor: color,
166
+ border: 'none',
167
+ borderRadius: square ? undefined : '100%',
168
+ } }));
169
+ }
170
+ function LegendCell({ left, right, }) {
171
+ return (_jsxs(Flex, { gap: "xs", width: "full", alignItems: "center", children: [_jsxs(Flex, { alignItems: "center", gap: "xs", width: "full", children: [left.marker, _jsx(SubLabel, { text: `${left.label}` })] }), right ? (_jsxs(Flex, { alignItems: "center", gap: "xs", width: "full", children: [right.marker, _jsx(SubLabel, { text: `${right.label}` })] })) : (_jsx(_Fragment, {}))] }));
172
+ }
173
+ function LegendSection({ title, items }) {
174
+ return (_jsxs(Flex, { direction: "column", gap: "xs", width: "full", children: [_jsx(Label, { text: title, weight: "medium" }), items.map((item) => (_jsx(LegendCell, { left: item[0], right: item[1] }, `${item[0].id}-${item[1]?.id}`)))] }));
175
+ }
176
+ export const PipelineLegend = ({ isDebugging, }) => {
177
+ const sections = isDebugging
178
+ ? [...debugSections, ...sharedSections, ...portStatusSection, bridgeSection]
179
+ : sharedSections;
180
+ return (_jsxs(Flex, { direction: "column", gap: "xs", width: "full", padding: "sm", rounded: "common", border: "base", backgroundColor: "white", maxWidth: "250px", children: [_jsx(Label, { text: "Legend", color: "black", weight: "medium", align: "center" }), _jsx(Flex, { direction: "column", gap: "xs", width: "full", children: sections.map((section) => (_jsx(LegendSection, { title: section.title, items: section.items }, section.title))) })] }));
181
+ };
@@ -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,31 +47,7 @@ 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
- export declare const DOCS_BASE_URL = "https://docs.luxonis.com/software-v3/depthai/depthai-components/nodes";
51
- export declare const NodesWithLinks: {
52
- AprilTag: string;
53
- BenchmarkIn: string;
54
- BenchmarkOut: string;
55
- Camera: string;
56
- DetectionNetwork: string;
57
- EdgeDetector: string;
58
- SpatialDetectionNetwork: string;
59
- FeatureTracker: string;
60
- ImageAlign: string;
61
- ImageManip: string;
62
- IMU: string;
63
- NeuralNetwork: string;
64
- ObjectTracker: string;
65
- RGBD: string;
66
- Script: string;
67
- SpatialLocationCalculator: string;
68
- StereoDepth: string;
69
- Sync: string;
70
- SystemLogger: string;
71
- Thermal: string;
72
- ToF: string;
73
- VideoEncoder: string;
74
- Warp: string;
75
- };
@@ -1,4 +1,3 @@
1
- import { MarkerType, } from '@xyflow/react';
2
1
  import { filterNodesHandles, parseHandles, } from './pipeline-handles';
3
2
  function addFakeNode(id) {
4
3
  return [
@@ -40,6 +39,11 @@ export function parsePipeline(rawPayload) {
40
39
  id: id.toString(),
41
40
  name: node.name,
42
41
  handles: parseHandles(node.ioInfo),
42
+ nodeType: node.deviceNode === undefined
43
+ ? undefined
44
+ : node.deviceNode
45
+ ? 'device'
46
+ : 'host',
43
47
  },
44
48
  })) ?? [];
45
49
  // Check for parent nodes and if there is children with some parent node that doesn't exist the create it as fake node
@@ -123,6 +127,7 @@ export function parsePipeline(rawPayload) {
123
127
  target: connection.node2Id.toString(),
124
128
  sourceHandle: connection.node1Output,
125
129
  targetHandle: connection.node2Input,
130
+ type: 'generic',
126
131
  })) ?? [];
127
132
  const bridges = pipeline.bridges?.map((bridge) => ({
128
133
  id: `${bridge[0]}-${bridge[1]}-bridge`,
@@ -131,13 +136,7 @@ export function parsePipeline(rawPayload) {
131
136
  sourceHandle: 'bottom',
132
137
  targetHandle: 'top',
133
138
  animated: true,
134
- markerStart: {
135
- type: MarkerType.ArrowClosed,
136
- },
137
- markerEnd: {
138
- type: MarkerType.ArrowClosed,
139
- },
140
- type: 'step',
139
+ type: 'bridge',
141
140
  })) ?? [];
142
141
  // NOTE: Parent nodes should be rendered before child nodes
143
142
  const groupedNodes = [...parentNodes, ...childrenNodes, ...filteredNodes];
@@ -147,29 +146,3 @@ export function parsePipeline(rawPayload) {
147
146
  edges: [...edges, ...bridges],
148
147
  };
149
148
  }
150
- export const DOCS_BASE_URL = `https://docs.luxonis.com/software-v3/depthai/depthai-components/nodes`;
151
- export const NodesWithLinks = {
152
- AprilTag: 'april_tag',
153
- BenchmarkIn: 'benchmark_in',
154
- BenchmarkOut: 'benchmark_out',
155
- Camera: 'camera',
156
- DetectionNetwork: 'detection_network',
157
- EdgeDetector: 'edge_detector',
158
- SpatialDetectionNetwork: 'spatial_detection_network',
159
- FeatureTracker: 'feature_tracker',
160
- ImageAlign: 'image_align',
161
- ImageManip: 'image_manip',
162
- IMU: 'imu',
163
- NeuralNetwork: 'neural_network',
164
- ObjectTracker: 'object_tracker',
165
- RGBD: 'rgbd',
166
- Script: 'script',
167
- SpatialLocationCalculator: 'spatial_location_calculator',
168
- StereoDepth: 'stereo_depth',
169
- Sync: 'sync',
170
- SystemLogger: 'system_logger',
171
- Thermal: 'thermal',
172
- ToF: 'tof',
173
- VideoEncoder: 'video_encoder',
174
- Warp: 'warp',
175
- };
@@ -0,0 +1,38 @@
1
+ import { Edge } from '@xyflow/react';
2
+ import { ParsedNode } from './pipeline';
3
+ import { PipelineState } from './pipeline-state';
4
+ export declare const topoSort: (nodes: ParsedNode[]) => ParsedNode[];
5
+ export declare const DOCS_BASE_URL = "https://docs.luxonis.com/software-v3/depthai/depthai-components/nodes";
6
+ export declare const NodesWithLinks: {
7
+ AprilTag: string;
8
+ BenchmarkIn: string;
9
+ BenchmarkOut: string;
10
+ Camera: string;
11
+ DetectionNetwork: string;
12
+ EdgeDetector: string;
13
+ SpatialDetectionNetwork: string;
14
+ FeatureTracker: string;
15
+ ImageAlign: string;
16
+ ImageManip: string;
17
+ IMU: string;
18
+ NeuralNetwork: string;
19
+ ObjectTracker: string;
20
+ RGBD: string;
21
+ Script: string;
22
+ SpatialLocationCalculator: string;
23
+ StereoDepth: string;
24
+ Sync: string;
25
+ SystemLogger: string;
26
+ Thermal: string;
27
+ ToF: string;
28
+ VideoEncoder: string;
29
+ Warp: string;
30
+ };
31
+ export declare const MIN_NODE_WIDTH = 376;
32
+ export declare const MIN_NODE_HEIGHT = 150;
33
+ export declare const getLayoutedElements: (nodes: ParsedNode[], edges: Edge[]) => {
34
+ nodes: any[];
35
+ edges: Edge[];
36
+ };
37
+ export declare const adjustNodes: (nodes: ParsedNode[], edges: Edge[]) => ParsedNode[];
38
+ export declare const updateNodesOnPipelineStateChange: (nodes: ParsedNode[], pipelineState: PipelineState[]) => ParsedNode[];
@@ -0,0 +1,255 @@
1
+ import Dagre from '@dagrejs/dagre';
2
+ export const topoSort = (nodes) => {
3
+ const nodeMap = new Map(nodes.map((n) => [n.id, n]));
4
+ const result = [];
5
+ const visited = new Set();
6
+ const visit = (id) => {
7
+ if (visited.has(id))
8
+ return;
9
+ visited.add(id);
10
+ const node = nodeMap.get(id);
11
+ if (!node)
12
+ return;
13
+ // Visit parent first
14
+ if (node.parentId)
15
+ visit(node.parentId);
16
+ result.push(node);
17
+ };
18
+ for (const node of nodes) {
19
+ visit(node.id);
20
+ }
21
+ return result;
22
+ };
23
+ export const DOCS_BASE_URL = `https://docs.luxonis.com/software-v3/depthai/depthai-components/nodes`;
24
+ export const NodesWithLinks = {
25
+ AprilTag: 'april_tag',
26
+ BenchmarkIn: 'benchmark_in',
27
+ BenchmarkOut: 'benchmark_out',
28
+ Camera: 'camera',
29
+ DetectionNetwork: 'detection_network',
30
+ EdgeDetector: 'edge_detector',
31
+ SpatialDetectionNetwork: 'spatial_detection_network',
32
+ FeatureTracker: 'feature_tracker',
33
+ ImageAlign: 'image_align',
34
+ ImageManip: 'image_manip',
35
+ IMU: 'imu',
36
+ NeuralNetwork: 'neural_network',
37
+ ObjectTracker: 'object_tracker',
38
+ RGBD: 'rgbd',
39
+ Script: 'script',
40
+ SpatialLocationCalculator: 'spatial_location_calculator',
41
+ StereoDepth: 'stereo_depth',
42
+ Sync: 'sync',
43
+ SystemLogger: 'system_logger',
44
+ Thermal: 'thermal',
45
+ ToF: 'tof',
46
+ VideoEncoder: 'video_encoder',
47
+ Warp: 'warp',
48
+ };
49
+ export const MIN_NODE_WIDTH = 376;
50
+ export const MIN_NODE_HEIGHT = 150;
51
+ export const getLayoutedElements = (nodes, edges) => {
52
+ const graph = new Dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({}));
53
+ graph.setGraph({ rankdir: 'LR' });
54
+ for (const edge of edges) {
55
+ graph.setEdge(edge.source, edge.target);
56
+ }
57
+ for (const node of nodes) {
58
+ graph.setNode(node.id, {
59
+ ...node,
60
+ width: MIN_NODE_WIDTH,
61
+ height: MIN_NODE_HEIGHT,
62
+ });
63
+ }
64
+ Dagre.layout(graph);
65
+ return {
66
+ nodes: nodes.map((node) => {
67
+ const position = graph.node(node.id);
68
+ const x = position.x - MIN_NODE_WIDTH / 2;
69
+ const y = position.y - MIN_NODE_HEIGHT / 2;
70
+ return { ...node, position: { x, y } };
71
+ }),
72
+ edges,
73
+ };
74
+ };
75
+ export const adjustNodes = (nodes, edges) => {
76
+ const PADDING = 16;
77
+ const HEADER_HEIGHT = 24;
78
+ // Build a tree: find all group nodes and their depth
79
+ const groupNodes = nodes.filter((node) => node.type === 'group');
80
+ const childNodes = nodes.filter((node) => node.type !== 'group');
81
+ // Sort groups by depth (deepest first) so children are resolved before parents
82
+ const getDepth = (nodeId, depth = 0) => {
83
+ const node = nodes.find((n) => n.id === nodeId);
84
+ if (!node?.parentId)
85
+ return depth;
86
+ return getDepth(node.parentId, depth + 1);
87
+ };
88
+ const sortedGroups = [...groupNodes].sort((a, b) => getDepth(b.id) - getDepth(a.id));
89
+ // Track resolved sizes for groups as we process them
90
+ const resolvedSizes = {};
91
+ // Start with all non-group nodes at their measured sizes
92
+ for (const node of childNodes) {
93
+ resolvedSizes[node.id] = {
94
+ width: Math.min(node.measured?.width ?? MIN_NODE_WIDTH),
95
+ height: Math.min(node.measured?.height ?? MIN_NODE_HEIGHT),
96
+ };
97
+ }
98
+ const updatedNodes = {};
99
+ for (const node of nodes)
100
+ updatedNodes[node.id] = { ...node };
101
+ // Process groups deepest-first
102
+ for (const group of sortedGroups) {
103
+ const children = nodes.filter((n) => n.parentId === group.id);
104
+ if (children.length === 0) {
105
+ resolvedSizes[group.id] = {
106
+ width: MIN_NODE_WIDTH,
107
+ height: MIN_NODE_HEIGHT,
108
+ };
109
+ continue;
110
+ }
111
+ const g = new Dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({}));
112
+ g.setGraph({ rankdir: 'LR' });
113
+ for (const child of children) {
114
+ // Use resolved size (important for nested groups!)
115
+ const size = resolvedSizes[child.id] ?? {
116
+ width: MIN_NODE_WIDTH,
117
+ height: MIN_NODE_HEIGHT,
118
+ };
119
+ g.setNode(child.id, { width: size.width, height: size.height });
120
+ }
121
+ const groupEdges = edges.filter((e) => children.some((c) => c.id === e.source) &&
122
+ children.some((c) => c.id === e.target));
123
+ for (const edge of groupEdges) {
124
+ g.setEdge(edge.source, edge.target);
125
+ }
126
+ Dagre.layout(g);
127
+ let minX = 9999;
128
+ let minY = 9999;
129
+ let maxX = -9999;
130
+ let maxY = -9999;
131
+ // Replace both child loops with this single pass:
132
+ for (const child of children) {
133
+ const pos = g.node(child.id);
134
+ const size = resolvedSizes[child.id] ?? {
135
+ width: MIN_NODE_WIDTH,
136
+ height: MIN_NODE_HEIGHT,
137
+ };
138
+ const x = pos.x - size.width / 2;
139
+ const y = pos.y - size.height / 2;
140
+ minX = Math.min(minX, x);
141
+ minY = Math.min(minY, y);
142
+ maxX = Math.max(maxX, x + size.width);
143
+ maxY = Math.max(maxY, y + size.height);
144
+ }
145
+ // Now positions are correct, apply them
146
+ for (const child of children) {
147
+ const pos = g.node(child.id);
148
+ const size = resolvedSizes[child.id] ?? {
149
+ width: MIN_NODE_WIDTH,
150
+ height: MIN_NODE_HEIGHT,
151
+ };
152
+ updatedNodes[child.id] = {
153
+ ...updatedNodes[child.id],
154
+ position: {
155
+ x: pos.x - size.width / 2 - minX + PADDING,
156
+ y: pos.y - size.height / 2 - minY + PADDING + HEADER_HEIGHT,
157
+ },
158
+ };
159
+ }
160
+ const groupW = maxX - minX + PADDING * 2;
161
+ const groupH = maxY - minY + PADDING * 2 + HEADER_HEIGHT;
162
+ // Store this group's resolved size for its own parent to use
163
+ resolvedSizes[group.id] = { width: groupW, height: groupH };
164
+ updatedNodes[group.id] = {
165
+ ...updatedNodes[group.id],
166
+ style: { ...updatedNodes[group.id].style, width: groupW, height: groupH },
167
+ width: groupW,
168
+ height: groupH,
169
+ };
170
+ }
171
+ // Now lay out the top-level (root) nodes using Dagre with correct sizes
172
+ const graph = new Dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({}));
173
+ graph.setGraph({ rankdir: 'LR' });
174
+ const rootNodes = nodes.filter((n) => !n.parentId);
175
+ for (const node of rootNodes) {
176
+ const size = resolvedSizes[node.id] ?? {
177
+ width: MIN_NODE_WIDTH,
178
+ height: MIN_NODE_HEIGHT,
179
+ };
180
+ graph.setNode(node.id, { width: size.width, height: size.height });
181
+ }
182
+ const rootEdges = edges.filter((e) => rootNodes.some((n) => n.id === e.source) &&
183
+ rootNodes.some((n) => n.id === e.target));
184
+ for (const edge of rootEdges) {
185
+ graph.setEdge(edge.source, edge.target);
186
+ }
187
+ Dagre.layout(graph);
188
+ for (const node of rootNodes) {
189
+ const pos = graph.node(node.id);
190
+ if (!pos)
191
+ continue;
192
+ const size = resolvedSizes[node.id] ?? {
193
+ width: MIN_NODE_WIDTH,
194
+ height: MIN_NODE_HEIGHT,
195
+ };
196
+ updatedNodes[node.id] = {
197
+ ...updatedNodes[node.id],
198
+ position: {
199
+ x: pos.x - size.width / 2,
200
+ y: pos.y - size.height / 2,
201
+ },
202
+ };
203
+ }
204
+ return topoSort(Object.values(updatedNodes));
205
+ };
206
+ export const updateNodesOnPipelineStateChange = (nodes, pipelineState) => {
207
+ const parsedNodes = [];
208
+ for (const node of nodes) {
209
+ const nodeState = pipelineState.find((state) => state.id.toString() === node.id.toString());
210
+ // Inputs and outputs
211
+ const inputHandles = node.data.handles.input;
212
+ const outputHandles = node.data.handles.output;
213
+ const newInputHandles = [];
214
+ for (const inputHandle of inputHandles) {
215
+ const obj = { ...inputHandle };
216
+ const inputState = nodeState?.inputs[inputHandle.name];
217
+ if (inputState) {
218
+ obj.maxQueueSize = inputState.numQueued;
219
+ obj.fps = inputState.timing.fps;
220
+ obj.dotColor = inputState.dotColor;
221
+ }
222
+ newInputHandles.push(obj);
223
+ }
224
+ const newOutputHandles = [];
225
+ for (const outputHandle of outputHandles) {
226
+ const obj = { ...outputHandle };
227
+ const outputState = nodeState?.outputs[outputHandle.name];
228
+ if (outputState) {
229
+ obj.maxQueueSize = outputState.numQueued;
230
+ obj.fps = outputState.timing.fps;
231
+ obj.dotColor = outputState.dotColor;
232
+ }
233
+ newOutputHandles.push(obj);
234
+ }
235
+ // GPST timings
236
+ const gpst = nodeState?.gpstTimings;
237
+ // Node state
238
+ const state = nodeState?.stateInfo;
239
+ parsedNodes.push({
240
+ ...node,
241
+ data: {
242
+ ...node.data,
243
+ handles: {
244
+ input: newInputHandles,
245
+ output: newOutputHandles,
246
+ },
247
+ extras: {
248
+ gpstTimings: gpst,
249
+ stateInfo: state,
250
+ },
251
+ },
252
+ });
253
+ }
254
+ return parsedNodes;
255
+ };
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.14.0",
4
4
  "type": "module",
5
5
  "license": "UNLICENSED",
6
6
  "main": "./dist/src/index.js",