@luxonis/depthai-pipeline-lib 1.11.0 → 1.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.css CHANGED
@@ -1,3 +1,3 @@
1
- @import url('@xyflow/react/dist/style.css');
2
- @import url('@luxonis/common-fe-components/styles');
3
- @import url('./panda.css');
1
+ @import url("@xyflow/react/dist/style.css");
2
+ @import url("@luxonis/common-fe-components/styles");
3
+ @import url("./panda.css");
@@ -6,14 +6,14 @@ import { css } from '../styled-system/css/index.mjs';
6
6
  export const GroupNode = (props) => {
7
7
  const { data: _node } = props;
8
8
  return (_jsx(Flex, { direction: "column", className: css({
9
- 'minWidth': 'container.smaller.xxs',
10
- 'border': 'base',
11
- 'rounded': 'common',
9
+ minWidth: 'container.smaller.xxs',
10
+ border: 'base',
11
+ rounded: 'common',
12
12
  '&:hover .node-help-icon': {
13
13
  color: 'text.normal',
14
14
  },
15
- 'padding': 0,
16
- 'borderStyle': 'dashed',
17
- 'backgroundColor': 'rgba(0,0,0,0.125)',
15
+ padding: 0,
16
+ borderStyle: 'dashed',
17
+ backgroundColor: 'rgba(0,0,0,0.125)',
18
18
  }) }));
19
19
  };
@@ -1,5 +1,5 @@
1
- import React from 'react';
2
1
  import type { NodeProps } from '@xyflow/react';
2
+ import React from 'react';
3
3
  import { type ParsedNode } from '../services/pipeline.js';
4
4
  export type PipelineNodeProps = NodeProps<ParsedNode>;
5
5
  export declare const PipelineNode: React.FC<PipelineNodeProps>;
@@ -1,12 +1,12 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Handle, Position } from '@xyflow/react';
2
+ import { Button, Flex, HelpIcon, Label, SubLabel, } from '@luxonis/common-fe-components';
3
3
  import { clsx } from '@luxonis/common-fe-components/helpers';
4
- import { Button, Flex, HelpIcon, Label, SubLabel } from '@luxonis/common-fe-components';
5
- import { css } from '../styled-system/css/css.mjs';
4
+ import { Handle, Position } from '@xyflow/react';
6
5
  import { DOCS_BASE_URL, NodesWithLinks, } from '../services/pipeline.js';
7
- const NodeHandles = props => {
6
+ import { css } from '../styled-system/css/css.mjs';
7
+ const NodeHandles = (props) => {
8
8
  const { handles, type } = props;
9
- return (_jsx(Flex, { full: true, direction: "column", align: type === 'input' ? 'start' : 'end', children: handles.map(({ type: handleType, id, blocking, queueSize, name, maxQueueSize, fps, connected }) => {
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
10
  if (!connected) {
11
11
  return;
12
12
  }
@@ -19,7 +19,7 @@ const NodeHandles = props => {
19
19
  }) }), _jsx(NodeHandlesSubLabel, { type: type, name: name, queueSize: queueSize, maxQueueSize: maxQueueSize, fps: fps })] }, id));
20
20
  }) }));
21
21
  };
22
- const NodeHandlesSubLabel = props => {
22
+ const NodeHandlesSubLabel = (props) => {
23
23
  const { type, name, queueSize, maxQueueSize, fps } = props;
24
24
  return (_jsx(SubLabel, { className: css({
25
25
  ...(type === 'input' //
@@ -29,17 +29,17 @@ const NodeHandlesSubLabel = props => {
29
29
  ? `[${fps ? `${fps.toFixed(1)} | ` : ''}${`${maxQueueSize ?? 0}/${queueSize}`}] ${name}`
30
30
  : `${fps ? `[${fps.toFixed(1)}]` : ''} ${name}`, break: "none" }));
31
31
  };
32
- export const PipelineNode = props => {
32
+ export const PipelineNode = (props) => {
33
33
  const { data: node } = props;
34
- // eslint-disable-next-line no-prototype-builtins
34
+ // biome-ignore lint/suspicious/noPrototypeBuiltins: Intended
35
35
  const link = NodesWithLinks.hasOwnProperty(node.name)
36
36
  ? `${DOCS_BASE_URL}/${NodesWithLinks[node.name]}`
37
37
  : DOCS_BASE_URL;
38
38
  return (_jsxs(Flex, { direction: "column", className: css({
39
- 'minWidth': 'container.smaller.xxs',
40
- 'border': 'base',
41
- 'rounded': 'common',
42
- 'backgroundColor': 'white',
39
+ minWidth: 'container.smaller.xxs',
40
+ border: 'base',
41
+ rounded: 'common',
42
+ backgroundColor: 'white',
43
43
  '&:hover .node-help-icon': {
44
44
  color: 'text.normal',
45
45
  },
@@ -1,5 +1,5 @@
1
- import React from 'react';
2
1
  import type { FlexProps } from '@luxonis/common-fe-components';
2
+ import React from 'react';
3
3
  import type { ParsedNode, Pipeline } from '../services/pipeline.js';
4
4
  import type { PipelineState } from '../services/pipeline-state.js';
5
5
  export type PipelineCanvasProps = FlexProps & {
@@ -1,13 +1,13 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import React from 'react';
3
- import { Panel, ReactFlow, ReactFlowProvider, useEdgesState, useNodesState, useReactFlow, useStore, } from '@xyflow/react';
4
- import { Flex, Header } from '@luxonis/common-fe-components';
5
2
  import Dagre from '@dagrejs/dagre';
3
+ import { Flex, Header } from '@luxonis/common-fe-components';
4
+ import { Panel, ReactFlow, ReactFlowProvider, useEdgesState, useNodesState, useReactFlow, useStore, } from '@xyflow/react';
5
+ import React from 'react';
6
6
  import { PipelineNode } from './Node.js';
7
7
  const getLayoutedElements = (nodes, edges) => {
8
8
  const graph = new Dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({}));
9
9
  graph.setGraph({ rankdir: 'LR', ranksep: 80, nodesep: 40 });
10
- const childNodes = nodes.filter(node => node.type !== 'group');
10
+ const childNodes = nodes.filter((node) => node.type !== 'group');
11
11
  for (const edge of edges) {
12
12
  graph.setEdge(edge.source, edge.target);
13
13
  }
@@ -19,7 +19,7 @@ const getLayoutedElements = (nodes, edges) => {
19
19
  }
20
20
  Dagre.layout(graph);
21
21
  return {
22
- nodes: nodes.map(node => {
22
+ nodes: nodes.map((node) => {
23
23
  const pos = graph.node(node.id);
24
24
  if (!pos)
25
25
  return node;
@@ -36,44 +36,46 @@ const getLayoutedElements = (nodes, edges) => {
36
36
  };
37
37
  const adjustNodes = (nodes, edges) => {
38
38
  const PADDING = 16;
39
- const groupNodes = nodes.filter(node => node.type === 'group');
40
- const childNodes = nodes.filter(node => node.type !== 'group');
41
- const graph = new Dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({}));
42
- graph.setGraph({ rankdir: 'LR', ranksep: 80, nodesep: 40 });
43
- for (const edge of edges) {
44
- graph.setEdge(edge.source, edge.target);
45
- }
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
46
54
  for (const node of childNodes) {
47
- graph.setNode(node.id, {
55
+ resolvedSizes[node.id] = {
48
56
  width: node.measured?.width ?? 300,
49
57
  height: node.measured?.height ?? 150,
50
- });
51
- }
52
- Dagre.layout(graph);
53
- const positionedChildren = childNodes.map(node => {
54
- const pos = graph.node(node.id);
55
- return {
56
- ...node,
57
- position: {
58
- x: pos.x - (node.measured?.width ?? 300) / 2,
59
- y: pos.y - (node.measured?.height ?? 150) / 2,
60
- },
61
58
  };
62
- });
63
- const positionedGroups = groupNodes.map(group => {
64
- const children = positionedChildren.filter(n => n.parentId === group.id);
59
+ }
60
+ const updatedNodes = {};
61
+ for (const node of nodes)
62
+ updatedNodes[node.id] = { ...node };
63
+ // Process groups deepest-first
64
+ for (const group of sortedGroups) {
65
+ const children = nodes.filter((n) => n.parentId === group.id);
65
66
  if (children.length === 0) {
66
- return { ...group, position: { x: 0, y: 0 } };
67
+ resolvedSizes[group.id] = { width: 300, height: 150 };
68
+ continue;
67
69
  }
68
70
  const g = new Dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({}));
69
71
  g.setGraph({ rankdir: 'LR', ranksep: 60, nodesep: 20 });
70
72
  for (const child of children) {
71
- g.setNode(child.id, {
72
- width: child.measured?.width ?? 300,
73
- height: child.measured?.height ?? 150,
74
- });
73
+ // Use resolved size (important for nested groups!)
74
+ const size = resolvedSizes[child.id] ?? { width: 300, height: 150 };
75
+ g.setNode(child.id, { width: size.width, height: size.height });
75
76
  }
76
- const groupEdges = edges.filter(e => children.some(c => c.id === e.source) && children.some(c => c.id === e.target));
77
+ const groupEdges = edges.filter((e) => children.some((c) => c.id === e.source) &&
78
+ children.some((c) => c.id === e.target));
77
79
  for (const edge of groupEdges) {
78
80
  g.setEdge(edge.source, edge.target);
79
81
  }
@@ -82,59 +84,102 @@ const adjustNodes = (nodes, edges) => {
82
84
  let minY = 9999;
83
85
  let maxX = -9999;
84
86
  let maxY = -9999;
85
- const locallyPositioned = children.map(child => {
87
+ for (const child of children) {
86
88
  const pos = g.node(child.id);
87
- const x = pos.x - (child.measured?.width ?? 300) / 2;
88
- const y = pos.y - (child.measured?.height ?? 150) / 2;
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;
89
92
  minX = Math.min(minX, x);
90
93
  minY = Math.min(minY, y);
91
- maxX = Math.max(maxX, x + (child.measured?.width ?? 300));
92
- maxY = Math.max(maxY, y + (child.measured?.height ?? 150));
93
- return { ...child, position: { x, y } };
94
- });
95
- const relativeChildren = locallyPositioned.map(child => ({
96
- ...child,
97
- position: {
98
- x: child.position.x - minX + PADDING,
99
- y: child.position.y - minY + PADDING,
100
- },
101
- }));
102
- const groupW = maxX - minX + PADDING * 2;
103
- const groupH = maxY - minY + PADDING * 2;
104
- let absMinX = 9999;
105
- let absMinY = 9999;
106
- for (const child of children) {
107
- const pos = graph.node(child.id);
108
- absMinX = Math.min(absMinX, pos.x - (child.measured?.width ?? 300) / 2);
109
- absMinY = Math.min(absMinY, pos.y - (child.measured?.height ?? 150) / 2);
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
+ };
110
105
  }
111
- return {
112
- nodes: relativeChildren,
113
- group: {
114
- ...group,
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],
115
112
  position: {
116
- x: absMinX - PADDING,
117
- y: absMinY - PADDING,
113
+ x: pos.x - size.width / 2 - minX + PADDING,
114
+ y: pos.y - size.height / 2 - minY + PADDING,
118
115
  },
119
- width: groupW + PADDING,
120
- height: groupH,
121
- style: { ...group.style, width: groupW + PADDING, height: groupH },
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,
122
153
  },
123
154
  };
124
- });
125
- const rootChildren = positionedChildren.filter(n => !n.parentId);
126
- const groupedChildren = positionedGroups.flatMap(g => g.nodes);
127
- const groups = positionedGroups.map(g => g.group);
128
- return [
129
- ...groups.filter(g => g !== undefined),
130
- ...groupedChildren.filter(c => c !== undefined),
131
- ...rootChildren,
132
- ];
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));
133
178
  };
134
179
  export const updateNodesOnPipelineStateChange = (nodes, pipelineState) => {
135
180
  const parsedNodes = [];
136
181
  for (const node of nodes) {
137
- const nodeState = pipelineState.find(state => state.id.toString() === node.id.toString());
182
+ const nodeState = pipelineState.find((state) => state.id.toString() === node.id.toString());
138
183
  const inputHandles = node.data.handles.input;
139
184
  const outputHandles = node.data.handles.output;
140
185
  const newInputHandles = [];
@@ -178,6 +223,7 @@ const PipelineCanvasBody = ({ pipeline, pipelineState, action, ...flexProps }) =
178
223
  const reactFlowWidth = useStore(widthSelector);
179
224
  const reactFlowHeight = useStore(heightSelector);
180
225
  const [shouldFitAndResize, setShouldFitAndResize] = React.useState(false);
226
+ // biome-ignore lint/correctness/useExhaustiveDependencies: Intended
181
227
  React.useEffect(() => {
182
228
  void fitView();
183
229
  // eslint-disable-next-line react-hooks/exhaustive-deps
@@ -200,6 +246,7 @@ const PipelineCanvasBody = ({ pipeline, pipelineState, action, ...flexProps }) =
200
246
  setEdges([...layouted.edges]);
201
247
  setShouldFitAndResize(true);
202
248
  }, [pipeline?.edges, pipeline?.nodes, setEdges, setNodes, pipelineState]);
249
+ // biome-ignore lint/correctness/useExhaustiveDependencies: Intended
203
250
  React.useEffect(() => {
204
251
  if (pipelineState && nodes.length > 0) {
205
252
  const updatedNodes = updateNodesOnPipelineStateChange(nodes, pipelineState);
@@ -217,6 +264,7 @@ const PipelineCanvasBody = ({ pipeline, pipelineState, action, ...flexProps }) =
217
264
  await setViewport({ x, y, zoom });
218
265
  await fitView({ nodes: nds });
219
266
  };
267
+ // biome-ignore lint/correctness/useExhaustiveDependencies: Intended
220
268
  React.useEffect(() => {
221
269
  if (!autoArrangeRef.current) {
222
270
  return;
@@ -227,6 +275,7 @@ const PipelineCanvasBody = ({ pipeline, pipelineState, action, ...flexProps }) =
227
275
  }
228
276
  // eslint-disable-next-line react-hooks/exhaustive-deps
229
277
  }, [nodes, edges]);
278
+ // biome-ignore lint/correctness/useExhaustiveDependencies: Intended
230
279
  React.useEffect(() => {
231
280
  if (shouldFitAndResize) {
232
281
  const viewport = getViewport();
@@ -237,4 +286,4 @@ const PipelineCanvasBody = ({ pipeline, pipelineState, action, ...flexProps }) =
237
286
  }, [shouldFitAndResize]);
238
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 }) }))] }));
239
288
  };
240
- export const PipelineCanvas = props => (_jsx(ReactFlowProvider, { children: _jsx(PipelineCanvasBody, { ...props }) }));
289
+ export const PipelineCanvas = (props) => (_jsx(ReactFlowProvider, { children: _jsx(PipelineCanvasBody, { ...props }) }));
@@ -1,6 +1,6 @@
1
- export { parsePipeline } from './services/pipeline.js';
1
+ export type { PipelineCanvasProps } from './components/PipelineCanvas.js';
2
+ export { PipelineCanvas } from './components/PipelineCanvas.js';
2
3
  export type { Pipeline, RawPipelinePayload } from './services/pipeline.js';
4
+ export { parsePipeline } from './services/pipeline.js';
5
+ export type { PipelineState, RawPipelineStatePayload, } from './services/pipeline-state.js';
3
6
  export { parsePipelineState } from './services/pipeline-state.js';
4
- export type { PipelineState, RawPipelineStatePayload } from './services/pipeline-state.js';
5
- export { PipelineCanvas } from './components/PipelineCanvas.js';
6
- export type { PipelineCanvasProps } from './components/PipelineCanvas.js';
package/dist/src/index.js CHANGED
@@ -1,3 +1,3 @@
1
+ export { PipelineCanvas } from './components/PipelineCanvas.js';
1
2
  export { parsePipeline } from './services/pipeline.js';
2
3
  export { parsePipelineState } from './services/pipeline-state.js';
3
- export { PipelineCanvas } from './components/PipelineCanvas.js';
@@ -0,0 +1,78 @@
1
+ import type { Edge } from '@xyflow/react';
2
+ import type { ParsedNode, RawPipelineNodeIO } from './pipeline';
3
+ export type RawPipelineBridge = [number, number];
4
+ export type ParsedHandle = {
5
+ id: number;
6
+ name: string;
7
+ type: 'input' | 'output';
8
+ blocking: boolean;
9
+ queueSize: number;
10
+ connected: boolean;
11
+ maxQueueSize?: number;
12
+ fps?: number;
13
+ };
14
+ export declare function parseHandles(handles: RawPipelineNodeIO[]): {
15
+ input: ParsedHandle[];
16
+ output: ParsedHandle[];
17
+ };
18
+ export declare function filterNodesHandles(nodes: ParsedNode[], edges: Edge[]): {
19
+ data: {
20
+ handles: {
21
+ input: {
22
+ connected: boolean;
23
+ id: number;
24
+ name: string;
25
+ type: "input" | "output";
26
+ blocking: boolean;
27
+ queueSize: number;
28
+ maxQueueSize?: number;
29
+ fps?: number;
30
+ }[];
31
+ output: {
32
+ connected: boolean;
33
+ id: number;
34
+ name: string;
35
+ type: "input" | "output";
36
+ blocking: boolean;
37
+ queueSize: number;
38
+ maxQueueSize?: number;
39
+ fps?: number;
40
+ }[];
41
+ };
42
+ id: string;
43
+ parentId?: string;
44
+ name: string;
45
+ };
46
+ id: string;
47
+ position: import("@xyflow/system").XYPosition;
48
+ type?: string | undefined;
49
+ sourcePosition?: import("@xyflow/system").Position;
50
+ targetPosition?: import("@xyflow/system").Position;
51
+ hidden?: boolean;
52
+ selected?: boolean;
53
+ dragging?: boolean;
54
+ draggable?: boolean;
55
+ selectable?: boolean;
56
+ connectable?: boolean;
57
+ deletable?: boolean;
58
+ dragHandle?: string;
59
+ width?: number;
60
+ height?: number;
61
+ initialWidth?: number;
62
+ initialHeight?: number;
63
+ parentId?: string;
64
+ zIndex?: number;
65
+ extent?: "parent" | import("@xyflow/system").CoordinateExtent;
66
+ expandParent?: boolean;
67
+ ariaLabel?: string;
68
+ origin?: import("@xyflow/system").NodeOrigin;
69
+ handles?: import("@xyflow/system").NodeHandle[];
70
+ measured?: {
71
+ width?: number;
72
+ height?: number;
73
+ };
74
+ style?: import("react").CSSProperties;
75
+ className?: string;
76
+ resizing?: boolean;
77
+ focusable?: boolean;
78
+ }[];
@@ -0,0 +1,41 @@
1
+ export function parseHandles(handles) {
2
+ const parsedHandles = {
3
+ input: [],
4
+ output: [],
5
+ };
6
+ for (const [_, handle] of handles) {
7
+ const type = handle.type === 3 ? 'input' : 'output';
8
+ const parsed = {
9
+ id: handle.id,
10
+ name: handle.name,
11
+ type,
12
+ blocking: handle.blocking,
13
+ queueSize: handle.queueSize,
14
+ connected: true,
15
+ };
16
+ parsedHandles[type].push(parsed);
17
+ }
18
+ return parsedHandles;
19
+ }
20
+ export function filterNodesHandles(nodes, edges) {
21
+ const connectedHandleIds = new Set(edges.flatMap((edge) => [
22
+ `${edge.source}-${edge.sourceHandle}`,
23
+ `${edge.target}-${edge.targetHandle}`,
24
+ ]));
25
+ return nodes.map((node) => ({
26
+ ...node,
27
+ data: {
28
+ ...node.data,
29
+ handles: {
30
+ input: node.data.handles.input.map((handle) => ({
31
+ ...handle,
32
+ connected: connectedHandleIds.has(`${node.id}-${handle.name}`),
33
+ })),
34
+ output: node.data.handles.output.map((handle) => ({
35
+ ...handle,
36
+ connected: connectedHandleIds.has(`${node.id}-${handle.name}`),
37
+ })),
38
+ },
39
+ },
40
+ }));
41
+ }
@@ -1,4 +1,5 @@
1
1
  import { type Edge, type Node } from '@xyflow/react';
2
+ import { type ParsedHandle } from './pipeline-handles';
2
3
  export type RawPipelineNodeIO = [
3
4
  [
4
5
  string,
@@ -18,7 +19,6 @@ export type RawPipelineNode = {
18
19
  name: string;
19
20
  parentId?: number;
20
21
  };
21
- export type RawPipelineBridge = [number, number];
22
22
  export type RawPipelineEdge = {
23
23
  node1Id: number;
24
24
  node1Output: string;
@@ -37,16 +37,6 @@ export type Pipeline = {
37
37
  nodes: ParsedNode[];
38
38
  edges: Edge[];
39
39
  };
40
- export type ParsedHandle = {
41
- id: number;
42
- name: string;
43
- type: 'input' | 'output';
44
- blocking: boolean;
45
- queueSize: number;
46
- connected: boolean;
47
- maxQueueSize?: number;
48
- fps?: number;
49
- };
50
40
  export type ParsedNode = Node<{
51
41
  id: string;
52
42
  parentId?: string;
@@ -1,74 +1,73 @@
1
- import { MarkerType } from '@xyflow/react';
2
- function parseHandles(handles) {
3
- const parsedHandles = {
4
- input: [],
5
- output: [],
6
- };
7
- for (const [_, handle] of handles) {
8
- const type = handle.type === 3 ? 'input' : 'output';
9
- const parsed = {
10
- id: handle.id,
11
- name: handle.name,
12
- type,
13
- blocking: handle.blocking,
14
- queueSize: handle.queueSize,
15
- connected: true,
16
- };
17
- parsedHandles[type].push(parsed);
18
- }
19
- return parsedHandles;
20
- }
21
- function filterNodesHandles(nodes, edges) {
22
- const connectedHandleIds = new Set(edges.flatMap(edge => [
23
- `${edge.source}-${edge.sourceHandle}`,
24
- `${edge.target}-${edge.targetHandle}`,
25
- ]));
26
- return nodes.map(node => ({
27
- ...node,
28
- data: {
29
- ...node.data,
30
- handles: {
31
- input: node.data.handles.input.map(handle => ({
32
- ...handle,
33
- connected: connectedHandleIds.has(`${node.id}-${handle.name}`),
34
- })),
35
- output: node.data.handles.output.map(handle => ({
36
- ...handle,
37
- connected: connectedHandleIds.has(`${node.id}-${handle.name}`),
38
- })),
39
- },
1
+ import { MarkerType, } from '@xyflow/react';
2
+ import { filterNodesHandles, parseHandles, } from './pipeline-handles';
3
+ function addFakeNode(id) {
4
+ return [
5
+ typeof id === 'number' ? id : parseInt(id),
6
+ {
7
+ id: typeof id === 'number' ? id : parseInt(id),
8
+ name: 'Unknown',
9
+ ioInfo: [
10
+ [
11
+ ['', ''],
12
+ {
13
+ id: 0,
14
+ name: 'unknown',
15
+ type: 0,
16
+ blocking: false,
17
+ queueSize: 0,
18
+ },
19
+ ],
20
+ ],
40
21
  },
41
- }));
22
+ ];
42
23
  }
43
24
  export function parsePipeline(rawPayload) {
44
25
  const { pipeline } = rawPayload;
45
26
  // Set all nodes as generic nodes
46
27
  const nodes = pipeline.nodes
47
- .filter(([_, node]) => pipeline.connections.some(connection => connection.node1Id === node.id || connection.node2Id === node.id))
28
+ .filter(([_, node]) => pipeline.connections.some((connection) => connection.node1Id === node.id || connection.node2Id === node.id))
48
29
  .map(([id, node]) => ({
49
30
  type: 'generic',
50
31
  id: id.toString(),
51
32
  position: { x: 0, y: 0 },
52
- parentId: node.parentId?.toString() === '-1' ? undefined : node.parentId?.toString(),
53
- extent: node.parentId?.toString() === '-1' ? undefined : 'parent',
33
+ parentId: node.parentId?.toString() === '-1'
34
+ ? undefined
35
+ : node.parentId?.toString(),
36
+ extent: node.parentId?.toString() === '-1' || node.parentId === undefined
37
+ ? undefined
38
+ : 'parent',
54
39
  data: {
55
40
  id: id.toString(),
56
41
  name: node.name,
57
42
  handles: parseHandles(node.ioInfo),
58
43
  },
59
44
  })) ?? [];
45
+ // Check for parent nodes and if there is children with some parent node that doesn't exist the create it as fake node
46
+ const parentNodesIdsRaw = [
47
+ ...new Set(nodes
48
+ .map((node) => {
49
+ if (node.parentId !== undefined && node.parentId !== '-1') {
50
+ return node.parentId;
51
+ }
52
+ else {
53
+ return null;
54
+ }
55
+ })
56
+ .filter((id) => id !== null)),
57
+ ];
58
+ const parentNodesArr = parentNodesIdsRaw.map((id) => pipeline.nodes.find(([_, node]) => node.id.toString() === id) ??
59
+ addFakeNode(id));
60
60
  // Set all parent nodes as group nodes
61
- const mappedParentNodes = nodes.map(node => node.parentId).filter(id => id !== undefined);
62
- const filteredParentNodes = mappedParentNodes.filter(id => id !== '-1');
63
- const parentNodes = [...new Set(filteredParentNodes)];
64
- const parentNodesArr = parentNodes
65
- .map(id => pipeline.nodes.find(([_, node]) => node.id.toString() === id))
66
- .filter(node => node !== undefined);
67
61
  const formattedParentNodes = parentNodesArr.map(([id, node]) => ({
68
62
  type: 'group',
69
63
  id: id.toString(),
70
64
  position: { x: 0, y: 0 },
71
- parentId: node.parentId?.toString() === '-1' ? undefined : node.parentId?.toString(),
65
+ parentId: node.parentId?.toString() === '-1' || node.parentId === undefined
66
+ ? undefined
67
+ : `${node.parentId?.toString()}-parent`,
68
+ extent: node.parentId?.toString() === '-1' || node.parentId === undefined
69
+ ? undefined
70
+ : 'parent',
72
71
  data: {
73
72
  id: id.toString(),
74
73
  name: node.name,
@@ -80,9 +79,10 @@ export function parsePipeline(rawPayload) {
80
79
  background: 'rgba(0,0,0,0.125)',
81
80
  },
82
81
  })) ?? [];
83
- const parentNodesIds = parentNodes.map(id => id.toString());
82
+ // Children nodes
83
+ const parentNodesIds = parentNodesIdsRaw.map((id) => id.toString());
84
84
  const childrenNodes = nodes
85
- .map(node => {
85
+ .map((node) => {
86
86
  if (node.parentId && parentNodesIds.includes(node.parentId)) {
87
87
  return {
88
88
  ...node,
@@ -101,11 +101,15 @@ export function parsePipeline(rawPayload) {
101
101
  return null;
102
102
  }
103
103
  })
104
- .filter(node => node !== null);
104
+ .filter((node) => node !== null && node !== undefined);
105
+ // Non-parent and non-child nodes
105
106
  const filteredNodes = nodes
106
- .filter(node => childrenNodes.find(childNode => childNode.id === node.id) !== undefined ? null : node)
107
- .filter(node => node !== null);
108
- const newFormattedParentNodes = formattedParentNodes.map(node => ({
107
+ .filter((node) => childrenNodes.find((childNode) => childNode.id === node.id) !== undefined
108
+ ? null
109
+ : node)
110
+ .filter((node) => node !== null);
111
+ // After formatting and filtering set the parent nodes ids to match with children parentIds
112
+ const parentNodes = formattedParentNodes.map((node) => ({
109
113
  ...node,
110
114
  id: `${node.id}-parent`,
111
115
  data: {
@@ -113,14 +117,14 @@ export function parsePipeline(rawPayload) {
113
117
  id: `${node.data.id}-parent`,
114
118
  },
115
119
  }));
116
- const edges = pipeline.connections.map(connection => ({
120
+ const edges = pipeline.connections.map((connection) => ({
117
121
  id: `${connection.node1Id}-${connection.node2Id}-${connection.node1Output}-${connection.node2Input}-edge`,
118
122
  source: connection.node1Id.toString(),
119
123
  target: connection.node2Id.toString(),
120
124
  sourceHandle: connection.node1Output,
121
125
  targetHandle: connection.node2Input,
122
126
  })) ?? [];
123
- const bridges = pipeline.bridges?.map(bridge => ({
127
+ const bridges = pipeline.bridges?.map((bridge) => ({
124
128
  id: `${bridge[0]}-${bridge[1]}-bridge`,
125
129
  source: bridge[0].toString(),
126
130
  target: bridge[1].toString(),
@@ -136,9 +140,12 @@ export function parsePipeline(rawPayload) {
136
140
  type: 'step',
137
141
  })) ?? [];
138
142
  // NOTE: Parent nodes should be rendered before child nodes
139
- const groupedNodes = [...newFormattedParentNodes, ...childrenNodes, ...filteredNodes];
143
+ const groupedNodes = [...parentNodes, ...childrenNodes, ...filteredNodes];
140
144
  const nodesWithFilteredHandles = filterNodesHandles(groupedNodes, edges);
141
- return { nodes: [...nodesWithFilteredHandles], edges: [...edges, ...bridges] };
145
+ return {
146
+ nodes: [...nodesWithFilteredHandles],
147
+ edges: [...edges, ...bridges],
148
+ };
142
149
  }
143
150
  export const DOCS_BASE_URL = `https://docs.luxonis.com/software-v3/depthai/depthai-components/nodes`;
144
151
  export const NodesWithLinks = {
package/package.json CHANGED
@@ -1,64 +1,50 @@
1
1
  {
2
- "name": "@luxonis/depthai-pipeline-lib",
3
- "version": "1.11.0",
4
- "type": "module",
5
- "license": "UNLICENSED",
6
- "main": "./dist/src/index.js",
7
- "scripts": {
8
- "build": "./build.sh",
9
- "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
10
- "gen:styles": "panda codegen",
11
- "prepublishOnly": "npm run build"
12
- },
13
- "dependencies": {
14
- "@dagrejs/dagre": "^1.1.3",
15
- "@luxonis/common-fe-components": "1.25.9",
16
- "@xyflow/react": "^12.0.4",
17
- "postcss-import": "^16.1.0",
18
- "postcss-nested": "^6.2.0",
19
- "postcss-preset-env": "^10.0.0",
20
- "rehype-sanitize": "^6.0.0"
21
- },
22
- "peerDependencies": {
23
- "react": "^18.3.1 || ^19.0.0",
24
- "react-dom": "^18.3.1 || ^19.0.0"
25
- },
26
- "devDependencies": {
27
- "@pandacss/dev": "^0.53.0",
28
- "@stylistic/eslint-plugin": "^2.6.1",
29
- "@types/react": "^18.3.1 || ^19.0.0",
30
- "@types/react-dom": "^18.3.1 || ^19.0.0",
31
- "@typescript-eslint/eslint-plugin": "^6.21.0",
32
- "@typescript-eslint/parser": "^6.21.0",
33
- "eslint": "^8.56.0",
34
- "eslint-config-prettier": "^9.1.0",
35
- "eslint-import-resolver-typescript": "^3.6.1",
36
- "eslint-interactive": "^10.8.0",
37
- "eslint-plugin-cypress": "^3.3.0",
38
- "eslint-plugin-functional": "^6.0.0",
39
- "eslint-plugin-github": "^4.10.1",
40
- "eslint-plugin-import": "^2.29.1",
41
- "eslint-plugin-prettier": "^5.1.3",
42
- "eslint-plugin-react": "^7.33.2",
43
- "eslint-plugin-react-hooks": "^4.6.0",
44
- "eslint-plugin-react-refresh": "^0.4.5",
45
- "postcss": "^8.4.31",
46
- "prettier": "^3.2.5",
47
- "prettier-eslint": "^16.3.0",
48
- "typescript": "^5.2.2"
49
- },
50
- "overrides": {
51
- "@radix-ui/react-alert-dialog": "1.0.5"
52
- },
53
- "files": [
54
- "dist/src/*",
55
- "dist/*.css"
56
- ],
57
- "exports": {
58
- ".": "./dist/src/index.js",
59
- "./styles": "./dist/index.css"
60
- },
61
- "publishConfig": {
62
- "access": "public"
63
- }
2
+ "name": "@luxonis/depthai-pipeline-lib",
3
+ "version": "1.12.0",
4
+ "type": "module",
5
+ "license": "UNLICENSED",
6
+ "main": "./dist/src/index.js",
7
+ "scripts": {
8
+ "build": "./build.sh",
9
+ "lint": "biome check .",
10
+ "lint:fix": "biome check . --write",
11
+ "gen:styles": "panda codegen",
12
+ "prepublishOnly": "npm run build"
13
+ },
14
+ "dependencies": {
15
+ "@dagrejs/dagre": "^1.1.3",
16
+ "@luxonis/common-fe-components": "1.25.9",
17
+ "@xyflow/react": "^12.0.4",
18
+ "postcss-import": "^16.1.0",
19
+ "postcss-nested": "^6.2.0",
20
+ "postcss-preset-env": "^10.0.0",
21
+ "rehype-sanitize": "^6.0.0"
22
+ },
23
+ "peerDependencies": {
24
+ "react": "^18.3.1 || ^19.0.0",
25
+ "react-dom": "^18.3.1 || ^19.0.0"
26
+ },
27
+ "devDependencies": {
28
+ "@biomejs/biome": "2.4.6",
29
+ "@pandacss/dev": "^0.53.0",
30
+ "@types/react": "^18.3.1 || ^19.0.0",
31
+ "@types/react-dom": "^18.3.1 || ^19.0.0",
32
+ "postcss": "^8.4.31",
33
+ "prettier": "^3.2.5",
34
+ "typescript": "^5.2.2"
35
+ },
36
+ "overrides": {
37
+ "@radix-ui/react-alert-dialog": "1.0.5"
38
+ },
39
+ "files": [
40
+ "dist/src/*",
41
+ "dist/*.css"
42
+ ],
43
+ "exports": {
44
+ ".": "./dist/src/index.js",
45
+ "./styles": "./dist/index.css"
46
+ },
47
+ "publishConfig": {
48
+ "access": "public"
49
+ }
64
50
  }