@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 +38 -6
- package/dist/src/components/BridgeEdge.d.ts +7 -0
- package/dist/src/components/BridgeEdge.js +58 -0
- package/dist/src/components/Node.js +44 -9
- package/dist/src/components/PipelineCanvas.d.ts +3 -3
- package/dist/src/components/PipelineCanvas.js +15 -230
- package/dist/src/components/PipelineLegend.d.ts +6 -0
- package/dist/src/components/PipelineLegend.js +181 -0
- package/dist/src/services/pipeline-handles.d.ts +6 -0
- package/dist/src/services/pipeline-state.d.ts +25 -2
- package/dist/src/services/pipeline-state.js +83 -2
- package/dist/src/services/pipeline.d.ts +4 -26
- package/dist/src/services/pipeline.js +7 -34
- package/dist/src/services/utils.d.ts +38 -0
- package/dist/src/services/utils.js +255 -0
- package/package.json +1 -1
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
|
-
.
|
|
1301
|
-
width:
|
|
1316
|
+
.w_20px {
|
|
1317
|
+
width: 20px;
|
|
1302
1318
|
}
|
|
1303
1319
|
|
|
1304
|
-
.
|
|
1305
|
-
height:
|
|
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\.
|
|
1325
|
-
background-color: var(--colors-dark-
|
|
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 {
|
|
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:
|
|
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
|
-
})
|
|
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: '
|
|
52
|
-
height: '
|
|
53
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
|
3
|
-
import {
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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(
|
|
241
|
-
|
|
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 && (
|
|
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,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
|
|
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
|
-
|
|
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
|
+
};
|