@luxonis/depthai-pipeline-lib 1.13.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
  }
@@ -1305,10 +1321,6 @@
1305
1321
  height: 20px;
1306
1322
  }
1307
1323
 
1308
- .d_flex {
1309
- display: flex;
1310
- }
1311
-
1312
1324
  .w_auto {
1313
1325
  width: auto;
1314
1326
  }
@@ -1317,6 +1329,14 @@
1317
1329
  height: auto;
1318
1330
  }
1319
1331
 
1332
+ .ai_center {
1333
+ align-items: center;
1334
+ }
1335
+
1336
+ .jc_center {
1337
+ justify-content: center;
1338
+ }
1339
+
1320
1340
  .border-style_dashed {
1321
1341
  border-style: dashed;
1322
1342
  }
@@ -1357,10 +1377,6 @@
1357
1377
  background-color: var(--colors-light-gray);
1358
1378
  }
1359
1379
 
1360
- .ai_center {
1361
- align-items: center;
1362
- }
1363
-
1364
1380
  .right_xs {
1365
1381
  right: var(--spacing-xs);
1366
1382
  }
@@ -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
+ };
@@ -2,10 +2,9 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
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';
6
5
  import { formatTiming } from '../services/pipeline-state.js';
6
+ import { DOCS_BASE_URL, MIN_NODE_WIDTH, NodesWithLinks, } from '../services/utils.js';
7
7
  import { css } from '../styled-system/css/css.mjs';
8
- import { MIN_NODE_WIDTH } from './PipelineCanvas.js';
9
8
  const NodeHandles = (props) => {
10
9
  const { handles, type } = props;
11
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, }) => {
@@ -15,20 +14,28 @@ const NodeHandles = (props) => {
15
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({
16
15
  width: 'custom.handle.dot!',
17
16
  height: 'custom.handle.dot!',
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
+ 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!',
29
26
  border: 'none!',
30
27
  borderRadius: blocking ? '0% !important' : '100% !important',
31
- }) }), _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));
32
39
  }) }));
33
40
  };
34
41
  const NodeHandlesSubLabel = (props) => {
@@ -1,13 +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 MIN_NODE_WIDTH = 376;
11
- export declare const MIN_NODE_HEIGHT = 150;
12
- export declare const updateNodesOnPipelineStateChange: (nodes: ParsedNode[], pipelineState: PipelineState[]) => ParsedNode[];
13
11
  export declare const PipelineCanvas: React.FC<PipelineCanvasProps>;
@@ -1,218 +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';
6
- import { topoSort } from '../services/utils.js';
5
+ import { adjustNodes, getLayoutedElements, updateNodesOnPipelineStateChange, } from '../services/utils.js';
6
+ import { BridgeEdge } from './BridgeEdge.js';
7
7
  import { PipelineNode } from './Node.js';
8
- export const MIN_NODE_WIDTH = 376;
9
- export const MIN_NODE_HEIGHT = 150;
10
- const getLayoutedElements = (nodes, edges) => {
11
- const graph = new Dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({}));
12
- graph.setGraph({ rankdir: 'LR' });
13
- for (const edge of edges) {
14
- graph.setEdge(edge.source, edge.target);
15
- }
16
- for (const node of nodes) {
17
- graph.setNode(node.id, {
18
- ...node,
19
- width: MIN_NODE_WIDTH,
20
- height: MIN_NODE_HEIGHT,
21
- });
22
- }
23
- Dagre.layout(graph);
24
- return {
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 } };
30
- }),
31
- edges,
32
- };
33
- };
34
- const adjustNodes = (nodes, edges) => {
35
- const PADDING = 16;
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
51
- for (const node of childNodes) {
52
- resolvedSizes[node.id] = {
53
- width: Math.min(node.measured?.width ?? MIN_NODE_WIDTH),
54
- height: Math.min(node.measured?.height ?? MIN_NODE_HEIGHT),
55
- };
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);
63
- if (children.length === 0) {
64
- resolvedSizes[group.id] = {
65
- width: MIN_NODE_WIDTH,
66
- height: MIN_NODE_HEIGHT,
67
- };
68
- continue;
69
- }
70
- const g = new Dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({}));
71
- g.setGraph({ rankdir: 'LR' });
72
- for (const child of children) {
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 });
79
- }
80
- const groupEdges = edges.filter((e) => children.some((c) => c.id === e.source) &&
81
- children.some((c) => c.id === e.target));
82
- for (const edge of groupEdges) {
83
- g.setEdge(edge.source, edge.target);
84
- }
85
- Dagre.layout(g);
86
- let minX = 9999;
87
- let minY = 9999;
88
- let maxX = -9999;
89
- let maxY = -9999;
90
- // Replace both child loops with this single pass:
91
- for (const child of children) {
92
- const pos = g.node(child.id);
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;
99
- minX = Math.min(minX, x);
100
- minY = Math.min(minY, y);
101
- maxX = Math.max(maxX, x + size.width);
102
- maxY = Math.max(maxY, y + size.height);
103
- }
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],
113
- position: {
114
- x: pos.x - size.width / 2 - minX + PADDING,
115
- y: pos.y - size.height / 2 - minY + PADDING + HEADER_HEIGHT,
116
- },
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,
160
- },
161
- };
162
- }
163
- return topoSort(Object.values(updatedNodes));
164
- };
165
- export const updateNodesOnPipelineStateChange = (nodes, pipelineState) => {
166
- const parsedNodes = [];
167
- for (const node of nodes) {
168
- const nodeState = pipelineState.find((state) => state.id.toString() === node.id.toString());
169
- // Inputs and outputs
170
- const inputHandles = node.data.handles.input;
171
- const outputHandles = node.data.handles.output;
172
- const newInputHandles = [];
173
- for (const inputHandle of inputHandles) {
174
- const obj = { ...inputHandle };
175
- const inputState = nodeState?.inputs[inputHandle.name];
176
- if (inputState) {
177
- obj.maxQueueSize = inputState.numQueued;
178
- obj.fps = inputState.timing.fps;
179
- obj.dotColor = inputState.dotColor;
180
- }
181
- newInputHandles.push(obj);
182
- }
183
- const newOutputHandles = [];
184
- for (const outputHandle of outputHandles) {
185
- const obj = { ...outputHandle };
186
- const outputState = nodeState?.outputs[outputHandle.name];
187
- if (outputState) {
188
- obj.maxQueueSize = outputState.numQueued;
189
- obj.fps = outputState.timing.fps;
190
- obj.dotColor = outputState.dotColor;
191
- }
192
- newOutputHandles.push(obj);
193
- }
194
- // GPST timings
195
- const gpst = nodeState?.gpstTimings;
196
- // Node state
197
- const state = nodeState?.stateInfo;
198
- parsedNodes.push({
199
- ...node,
200
- data: {
201
- ...node.data,
202
- handles: {
203
- input: newInputHandles,
204
- output: newOutputHandles,
205
- },
206
- extras: {
207
- gpstTimings: gpst,
208
- stateInfo: state,
209
- },
210
- },
211
- });
212
- }
213
- return parsedNodes;
214
- };
215
- const PipelineCanvasBody = ({ pipeline, pipelineState: pipelineStateParsed, 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 }) => {
216
12
  const { fitView, setViewport, getViewport } = useReactFlow();
217
13
  const autoArrangeRef = React.useRef(true);
218
14
  const widthSelector = (state) => state.width;
@@ -220,6 +16,7 @@ const PipelineCanvasBody = ({ pipeline, pipelineState: pipelineStateParsed, acti
220
16
  const reactFlowWidth = useStore(widthSelector);
221
17
  const reactFlowHeight = useStore(heightSelector);
222
18
  const [shouldFitAndResize, setShouldFitAndResize] = React.useState(false);
19
+ const [openLegend, setOpenLegend] = React.useState(false);
223
20
  const pipelineState = React.useMemo(() => pipelineStateParsed, [pipelineStateParsed]);
224
21
  // biome-ignore lint/correctness/useExhaustiveDependencies: Intended
225
22
  React.useEffect(() => {
@@ -272,6 +69,6 @@ const PipelineCanvasBody = ({ pipeline, pipelineState: pipelineStateParsed, acti
272
69
  }
273
70
  // eslint-disable-next-line react-hooks/exhaustive-deps
274
71
  }, [shouldFitAndResize]);
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 }) }))] }));
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 })] }) })] }))] }));
276
73
  };
277
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
+ };
@@ -51,29 +51,3 @@ export type ParsedNode = Node<{
51
51
  extras?: NodeExtras;
52
52
  }>;
53
53
  export declare function parsePipeline(rawPayload: RawPipelinePayload): Pipeline;
54
- export declare const DOCS_BASE_URL = "https://docs.luxonis.com/software-v3/depthai/depthai-components/nodes";
55
- export declare const NodesWithLinks: {
56
- AprilTag: string;
57
- BenchmarkIn: string;
58
- BenchmarkOut: string;
59
- Camera: string;
60
- DetectionNetwork: string;
61
- EdgeDetector: string;
62
- SpatialDetectionNetwork: string;
63
- FeatureTracker: string;
64
- ImageAlign: string;
65
- ImageManip: string;
66
- IMU: string;
67
- NeuralNetwork: string;
68
- ObjectTracker: string;
69
- RGBD: string;
70
- Script: string;
71
- SpatialLocationCalculator: string;
72
- StereoDepth: string;
73
- Sync: string;
74
- SystemLogger: string;
75
- Thermal: string;
76
- ToF: string;
77
- VideoEncoder: string;
78
- Warp: string;
79
- };
@@ -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 [
@@ -128,6 +127,7 @@ export function parsePipeline(rawPayload) {
128
127
  target: connection.node2Id.toString(),
129
128
  sourceHandle: connection.node1Output,
130
129
  targetHandle: connection.node2Input,
130
+ type: 'generic',
131
131
  })) ?? [];
132
132
  const bridges = pipeline.bridges?.map((bridge) => ({
133
133
  id: `${bridge[0]}-${bridge[1]}-bridge`,
@@ -136,13 +136,7 @@ export function parsePipeline(rawPayload) {
136
136
  sourceHandle: 'bottom',
137
137
  targetHandle: 'top',
138
138
  animated: true,
139
- markerStart: {
140
- type: MarkerType.ArrowClosed,
141
- },
142
- markerEnd: {
143
- type: MarkerType.ArrowClosed,
144
- },
145
- type: 'step',
139
+ type: 'bridge',
146
140
  })) ?? [];
147
141
  // NOTE: Parent nodes should be rendered before child nodes
148
142
  const groupedNodes = [...parentNodes, ...childrenNodes, ...filteredNodes];
@@ -152,29 +146,3 @@ export function parsePipeline(rawPayload) {
152
146
  edges: [...edges, ...bridges],
153
147
  };
154
148
  }
155
- export const DOCS_BASE_URL = `https://docs.luxonis.com/software-v3/depthai/depthai-components/nodes`;
156
- export const NodesWithLinks = {
157
- AprilTag: 'april_tag',
158
- BenchmarkIn: 'benchmark_in',
159
- BenchmarkOut: 'benchmark_out',
160
- Camera: 'camera',
161
- DetectionNetwork: 'detection_network',
162
- EdgeDetector: 'edge_detector',
163
- SpatialDetectionNetwork: 'spatial_detection_network',
164
- FeatureTracker: 'feature_tracker',
165
- ImageAlign: 'image_align',
166
- ImageManip: 'image_manip',
167
- IMU: 'imu',
168
- NeuralNetwork: 'neural_network',
169
- ObjectTracker: 'object_tracker',
170
- RGBD: 'rgbd',
171
- Script: 'script',
172
- SpatialLocationCalculator: 'spatial_location_calculator',
173
- StereoDepth: 'stereo_depth',
174
- Sync: 'sync',
175
- SystemLogger: 'system_logger',
176
- Thermal: 'thermal',
177
- ToF: 'tof',
178
- VideoEncoder: 'video_encoder',
179
- Warp: 'warp',
180
- };
@@ -1,2 +1,38 @@
1
+ import { Edge } from '@xyflow/react';
1
2
  import { ParsedNode } from './pipeline';
3
+ import { PipelineState } from './pipeline-state';
2
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[];
@@ -1,3 +1,4 @@
1
+ import Dagre from '@dagrejs/dagre';
1
2
  export const topoSort = (nodes) => {
2
3
  const nodeMap = new Map(nodes.map((n) => [n.id, n]));
3
4
  const result = [];
@@ -19,3 +20,236 @@ export const topoSort = (nodes) => {
19
20
  }
20
21
  return result;
21
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.13.0",
3
+ "version": "1.14.0",
4
4
  "type": "module",
5
5
  "license": "UNLICENSED",
6
6
  "main": "./dist/src/index.js",