@q1k-oss/btree-workflows 0.0.1
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/.claude/settings.local.json +31 -0
- package/CLAUDE.md +181 -0
- package/LICENSE +21 -0
- package/README.md +920 -0
- package/behaviour-tree-workflows-landing/index.html +16 -0
- package/behaviour-tree-workflows-landing/package-lock.json +2074 -0
- package/behaviour-tree-workflows-landing/package.json +31 -0
- package/behaviour-tree-workflows-landing/public/favicon.svg +17 -0
- package/behaviour-tree-workflows-landing/src/App.css +103 -0
- package/behaviour-tree-workflows-landing/src/App.tsx +176 -0
- package/behaviour-tree-workflows-landing/src/components/BlackboardInspector.css +89 -0
- package/behaviour-tree-workflows-landing/src/components/BlackboardInspector.tsx +64 -0
- package/behaviour-tree-workflows-landing/src/components/ExampleSelector.css +64 -0
- package/behaviour-tree-workflows-landing/src/components/ExampleSelector.tsx +34 -0
- package/behaviour-tree-workflows-landing/src/components/ExecutionLog.css +107 -0
- package/behaviour-tree-workflows-landing/src/components/ExecutionLog.tsx +85 -0
- package/behaviour-tree-workflows-landing/src/components/Header.css +50 -0
- package/behaviour-tree-workflows-landing/src/components/Header.tsx +26 -0
- package/behaviour-tree-workflows-landing/src/components/StatusBadge.css +45 -0
- package/behaviour-tree-workflows-landing/src/components/StatusBadge.tsx +15 -0
- package/behaviour-tree-workflows-landing/src/components/Toolbar.css +74 -0
- package/behaviour-tree-workflows-landing/src/components/Toolbar.tsx +53 -0
- package/behaviour-tree-workflows-landing/src/components/TreeVisualizer.css +67 -0
- package/behaviour-tree-workflows-landing/src/components/TreeVisualizer.tsx +192 -0
- package/behaviour-tree-workflows-landing/src/components/YamlEditor.css +18 -0
- package/behaviour-tree-workflows-landing/src/components/YamlEditor.tsx +96 -0
- package/behaviour-tree-workflows-landing/src/lib/count-nodes.ts +11 -0
- package/behaviour-tree-workflows-landing/src/lib/execution-engine.ts +96 -0
- package/behaviour-tree-workflows-landing/src/lib/tree-layout.ts +136 -0
- package/behaviour-tree-workflows-landing/src/lib/yaml-examples.ts +549 -0
- package/behaviour-tree-workflows-landing/src/main.tsx +9 -0
- package/behaviour-tree-workflows-landing/src/stubs/activepieces.ts +18 -0
- package/behaviour-tree-workflows-landing/src/stubs/fs.ts +24 -0
- package/behaviour-tree-workflows-landing/src/stubs/path.ts +16 -0
- package/behaviour-tree-workflows-landing/src/stubs/temporal-activity.ts +6 -0
- package/behaviour-tree-workflows-landing/src/stubs/temporal-workflow.ts +22 -0
- package/behaviour-tree-workflows-landing/tsconfig.json +25 -0
- package/behaviour-tree-workflows-landing/vite.config.ts +40 -0
- package/demo-google-sheets.ts +181 -0
- package/demo-runtime-variables.ts +174 -0
- package/demo-template.ts +208 -0
- package/docs/ARCHITECTURE_SUMMARY.md +613 -0
- package/docs/NODE_REFERENCE.md +504 -0
- package/docs/README.md +53 -0
- package/docs/custom-nodes-architecture.md +826 -0
- package/docs/observability.md +175 -0
- package/docs/yaml-specification.md +990 -0
- package/examples/temporal/README.md +117 -0
- package/examples/temporal/activities.ts +373 -0
- package/examples/temporal/client.ts +115 -0
- package/examples/temporal/python-worker/activities.py +339 -0
- package/examples/temporal/python-worker/requirements.txt +12 -0
- package/examples/temporal/python-worker/worker.py +106 -0
- package/examples/temporal/worker.ts +66 -0
- package/examples/temporal/workflows.ts +6 -0
- package/examples/temporal/yaml-workflow-loader.ts +105 -0
- package/examples/yaml-test.ts +97 -0
- package/examples/yaml-workflows/01-simple-sequence.yaml +25 -0
- package/examples/yaml-workflows/02-parallel-timeout.yaml +45 -0
- package/examples/yaml-workflows/03-ecommerce-checkout.yaml +94 -0
- package/examples/yaml-workflows/04-ai-agent-workflow.yaml +346 -0
- package/examples/yaml-workflows/05-order-processing.yaml +146 -0
- package/examples/yaml-workflows/06-activity-test.yaml +71 -0
- package/examples/yaml-workflows/07-activity-simple-test.yaml +43 -0
- package/examples/yaml-workflows/08-file-processing.yaml +141 -0
- package/examples/yaml-workflows/09-http-request.yaml +137 -0
- package/examples/yaml-workflows/README.md +211 -0
- package/package.json +38 -0
- package/src/actions/code-execution.schema.ts +27 -0
- package/src/actions/code-execution.ts +218 -0
- package/src/actions/generate-file.test.ts +516 -0
- package/src/actions/generate-file.ts +166 -0
- package/src/actions/http-request.test.ts +784 -0
- package/src/actions/http-request.ts +228 -0
- package/src/actions/index.ts +20 -0
- package/src/actions/parse-file.test.ts +448 -0
- package/src/actions/parse-file.ts +139 -0
- package/src/actions/python-script.test.ts +439 -0
- package/src/actions/python-script.ts +154 -0
- package/src/base-node.test.ts +511 -0
- package/src/base-node.ts +605 -0
- package/src/behavior-tree.test.ts +431 -0
- package/src/behavior-tree.ts +283 -0
- package/src/blackboard.test.ts +222 -0
- package/src/blackboard.ts +192 -0
- package/src/composites/conditional.schema.ts +19 -0
- package/src/composites/conditional.test.ts +309 -0
- package/src/composites/conditional.ts +129 -0
- package/src/composites/for-each.schema.ts +23 -0
- package/src/composites/for-each.test.ts +254 -0
- package/src/composites/for-each.ts +132 -0
- package/src/composites/index.ts +15 -0
- package/src/composites/memory-sequence.schema.ts +19 -0
- package/src/composites/memory-sequence.test.ts +223 -0
- package/src/composites/memory-sequence.ts +98 -0
- package/src/composites/parallel.schema.ts +28 -0
- package/src/composites/parallel.test.ts +502 -0
- package/src/composites/parallel.ts +157 -0
- package/src/composites/reactive-sequence.schema.ts +19 -0
- package/src/composites/reactive-sequence.test.ts +170 -0
- package/src/composites/reactive-sequence.ts +85 -0
- package/src/composites/recovery.schema.ts +19 -0
- package/src/composites/recovery.test.ts +366 -0
- package/src/composites/recovery.ts +90 -0
- package/src/composites/selector.schema.ts +19 -0
- package/src/composites/selector.test.ts +387 -0
- package/src/composites/selector.ts +85 -0
- package/src/composites/sequence.schema.ts +19 -0
- package/src/composites/sequence.test.ts +337 -0
- package/src/composites/sequence.ts +72 -0
- package/src/composites/sub-tree.schema.ts +21 -0
- package/src/composites/sub-tree.test.ts +893 -0
- package/src/composites/sub-tree.ts +177 -0
- package/src/composites/while.schema.ts +24 -0
- package/src/composites/while.test.ts +381 -0
- package/src/composites/while.ts +149 -0
- package/src/data-store/index.ts +10 -0
- package/src/data-store/memory-store.ts +161 -0
- package/src/data-store/types.ts +94 -0
- package/src/debug/breakpoint.test.ts +47 -0
- package/src/debug/breakpoint.ts +30 -0
- package/src/debug/index.ts +17 -0
- package/src/debug/resume-point.test.ts +49 -0
- package/src/debug/resume-point.ts +29 -0
- package/src/decorators/delay.schema.ts +21 -0
- package/src/decorators/delay.test.ts +261 -0
- package/src/decorators/delay.ts +140 -0
- package/src/decorators/force-result.schema.ts +32 -0
- package/src/decorators/force-result.test.ts +133 -0
- package/src/decorators/force-result.ts +63 -0
- package/src/decorators/index.ts +13 -0
- package/src/decorators/invert.schema.ts +19 -0
- package/src/decorators/invert.test.ts +135 -0
- package/src/decorators/invert.ts +42 -0
- package/src/decorators/keep-running.schema.ts +20 -0
- package/src/decorators/keep-running.test.ts +105 -0
- package/src/decorators/keep-running.ts +49 -0
- package/src/decorators/precondition.schema.ts +19 -0
- package/src/decorators/precondition.test.ts +351 -0
- package/src/decorators/precondition.ts +139 -0
- package/src/decorators/repeat.schema.ts +21 -0
- package/src/decorators/repeat.test.ts +187 -0
- package/src/decorators/repeat.ts +94 -0
- package/src/decorators/run-once.schema.ts +19 -0
- package/src/decorators/run-once.test.ts +140 -0
- package/src/decorators/run-once.ts +61 -0
- package/src/decorators/soft-assert.schema.ts +19 -0
- package/src/decorators/soft-assert.test.ts +107 -0
- package/src/decorators/soft-assert.ts +68 -0
- package/src/decorators/timeout.schema.ts +21 -0
- package/src/decorators/timeout.test.ts +274 -0
- package/src/decorators/timeout.ts +159 -0
- package/src/errors.test.ts +63 -0
- package/src/errors.ts +34 -0
- package/src/events.test.ts +347 -0
- package/src/events.ts +183 -0
- package/src/index.ts +80 -0
- package/src/integrations/index.ts +30 -0
- package/src/integrations/integration-action.test.ts +571 -0
- package/src/integrations/integration-action.ts +233 -0
- package/src/integrations/piece-executor.ts +320 -0
- package/src/observability/execution-tracker.ts +320 -0
- package/src/observability/index.ts +23 -0
- package/src/observability/sinks.ts +138 -0
- package/src/observability/types.ts +130 -0
- package/src/registry-utils.ts +147 -0
- package/src/registry.test.ts +466 -0
- package/src/registry.ts +334 -0
- package/src/schemas/base.schema.ts +104 -0
- package/src/schemas/index.ts +223 -0
- package/src/schemas/integration.test.ts +238 -0
- package/src/schemas/tree-definition.schema.ts +170 -0
- package/src/schemas/validation.test.ts +146 -0
- package/src/schemas/validation.ts +122 -0
- package/src/scripting/index.ts +22 -0
- package/src/templates/template-loader.test.ts +281 -0
- package/src/templates/template-loader.ts +152 -0
- package/src/temporal-integration.test.ts +213 -0
- package/src/test-nodes.ts +259 -0
- package/src/types.ts +503 -0
- package/src/utilities/index.ts +17 -0
- package/src/utilities/log-message.test.ts +275 -0
- package/src/utilities/log-message.ts +134 -0
- package/src/utilities/regex-extract.test.ts +138 -0
- package/src/utilities/regex-extract.ts +108 -0
- package/src/utilities/variable-resolver.test.ts +416 -0
- package/src/utilities/variable-resolver.ts +318 -0
- package/src/utils/error-handler.test.ts +117 -0
- package/src/utils/error-handler.ts +48 -0
- package/src/utils/signal-check.test.ts +234 -0
- package/src/utils/signal-check.ts +140 -0
- package/src/yaml/errors.ts +143 -0
- package/src/yaml/index.ts +30 -0
- package/src/yaml/loader.ts +39 -0
- package/src/yaml/parser.ts +286 -0
- package/src/yaml/validation/semantic-validator.ts +196 -0
- package/templates/google-sheets/insert-row.yaml +76 -0
- package/templates/notification-sender.yaml +33 -0
- package/templates/order-validation.yaml +44 -0
- package/tsconfig.json +24 -0
- package/vitest.config.ts +25 -0
- package/workflows/order-processor.yaml +59 -0
- package/workflows/process-order-workflow.yaml +142 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import React, { useRef, useEffect } from 'react';
|
|
2
|
+
import { EditorView, keymap } from '@codemirror/view';
|
|
3
|
+
import { EditorState } from '@codemirror/state';
|
|
4
|
+
import { basicSetup } from 'codemirror';
|
|
5
|
+
import { yaml } from '@codemirror/lang-yaml';
|
|
6
|
+
import { oneDark } from '@codemirror/theme-one-dark';
|
|
7
|
+
import { linter, type Diagnostic } from '@codemirror/lint';
|
|
8
|
+
import { validateYamlString } from '../lib/execution-engine';
|
|
9
|
+
import { indentWithTab } from '@codemirror/commands';
|
|
10
|
+
import './YamlEditor.css';
|
|
11
|
+
|
|
12
|
+
interface YamlEditorProps {
|
|
13
|
+
value: string;
|
|
14
|
+
onChange: (value: string) => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const YamlEditor: React.FC<YamlEditorProps> = ({ value, onChange }) => {
|
|
18
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
19
|
+
const viewRef = useRef<EditorView | null>(null);
|
|
20
|
+
const valueRef = useRef(value);
|
|
21
|
+
|
|
22
|
+
// Keep ref current
|
|
23
|
+
valueRef.current = value;
|
|
24
|
+
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
if (!containerRef.current) return;
|
|
27
|
+
|
|
28
|
+
const yamlLinter = linter((view) => {
|
|
29
|
+
const text = view.state.doc.toString();
|
|
30
|
+
if (!text.trim()) return [];
|
|
31
|
+
|
|
32
|
+
const result = validateYamlString(text);
|
|
33
|
+
if (result.valid) return [];
|
|
34
|
+
|
|
35
|
+
const diagnostics: Diagnostic[] = result.errors.map((errMsg) => ({
|
|
36
|
+
from: 0,
|
|
37
|
+
to: Math.min(text.length, 1),
|
|
38
|
+
severity: 'error' as const,
|
|
39
|
+
message: errMsg,
|
|
40
|
+
}));
|
|
41
|
+
return diagnostics;
|
|
42
|
+
}, { delay: 500 });
|
|
43
|
+
|
|
44
|
+
const state = EditorState.create({
|
|
45
|
+
doc: value,
|
|
46
|
+
extensions: [
|
|
47
|
+
basicSetup,
|
|
48
|
+
yaml(),
|
|
49
|
+
oneDark,
|
|
50
|
+
yamlLinter,
|
|
51
|
+
keymap.of([indentWithTab]),
|
|
52
|
+
EditorView.updateListener.of((update) => {
|
|
53
|
+
if (update.docChanged) {
|
|
54
|
+
const newVal = update.state.doc.toString();
|
|
55
|
+
onChange(newVal);
|
|
56
|
+
}
|
|
57
|
+
}),
|
|
58
|
+
EditorView.theme({
|
|
59
|
+
'&': { height: '100%', fontSize: '13px' },
|
|
60
|
+
'.cm-scroller': { fontFamily: "'JetBrains Mono', monospace" },
|
|
61
|
+
'.cm-content': { padding: '8px 0' },
|
|
62
|
+
}),
|
|
63
|
+
],
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const view = new EditorView({
|
|
67
|
+
state,
|
|
68
|
+
parent: containerRef.current,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
viewRef.current = view;
|
|
72
|
+
|
|
73
|
+
return () => {
|
|
74
|
+
view.destroy();
|
|
75
|
+
viewRef.current = null;
|
|
76
|
+
};
|
|
77
|
+
// Only run on mount
|
|
78
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
79
|
+
}, []);
|
|
80
|
+
|
|
81
|
+
// Sync external value changes
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
const view = viewRef.current;
|
|
84
|
+
if (!view) return;
|
|
85
|
+
const currentText = view.state.doc.toString();
|
|
86
|
+
if (currentText !== value) {
|
|
87
|
+
view.dispatch({
|
|
88
|
+
changes: { from: 0, to: currentText.length, insert: value },
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}, [value]);
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
<div className="yaml-editor" ref={containerRef} />
|
|
95
|
+
);
|
|
96
|
+
};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { Registry, registerStandardNodes } from '@btree/index';
|
|
2
|
+
import { ScopedBlackboard } from '@btree/blackboard';
|
|
3
|
+
import { NodeEventEmitter } from '@btree/events';
|
|
4
|
+
import { loadTreeFromYaml, validateYaml } from '@btree/yaml/index';
|
|
5
|
+
import type { TreeNode, TemporalContext } from '@btree/types';
|
|
6
|
+
import { NodeStatus } from '@btree/types';
|
|
7
|
+
import type { NodeEvent } from '@btree/events';
|
|
8
|
+
import { countNodes } from './count-nodes';
|
|
9
|
+
|
|
10
|
+
let _registry: Registry | null = null;
|
|
11
|
+
|
|
12
|
+
export function getRegistry(): Registry {
|
|
13
|
+
if (!_registry) {
|
|
14
|
+
_registry = new Registry();
|
|
15
|
+
registerStandardNodes(_registry);
|
|
16
|
+
}
|
|
17
|
+
return _registry;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface ParseResult {
|
|
21
|
+
tree: TreeNode | null;
|
|
22
|
+
error: string | null;
|
|
23
|
+
nodeCount: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function parseYamlToTree(yamlString: string): ParseResult {
|
|
27
|
+
const registry = getRegistry();
|
|
28
|
+
try {
|
|
29
|
+
const tree = loadTreeFromYaml(yamlString, registry);
|
|
30
|
+
return { tree, error: null, nodeCount: countNodes(tree) };
|
|
31
|
+
} catch (err) {
|
|
32
|
+
return {
|
|
33
|
+
tree: null,
|
|
34
|
+
error: err instanceof Error ? err.message : String(err),
|
|
35
|
+
nodeCount: 0,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function validateYamlString(yamlString: string): { valid: boolean; errors: string[] } {
|
|
41
|
+
const registry = getRegistry();
|
|
42
|
+
const result = validateYaml(yamlString, registry, { collectAllErrors: true });
|
|
43
|
+
return {
|
|
44
|
+
valid: result.valid,
|
|
45
|
+
errors: result.errors.map((e) => e.message),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface ExecutionResult {
|
|
50
|
+
status: NodeStatus;
|
|
51
|
+
blackboard: Record<string, unknown>;
|
|
52
|
+
events: NodeEvent<unknown>[];
|
|
53
|
+
error?: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export type EventCallback = (event: NodeEvent<unknown>) => void;
|
|
57
|
+
|
|
58
|
+
export async function executeTree(
|
|
59
|
+
tree: TreeNode,
|
|
60
|
+
onEvent?: EventCallback,
|
|
61
|
+
): Promise<ExecutionResult> {
|
|
62
|
+
const blackboard = new ScopedBlackboard();
|
|
63
|
+
const eventEmitter = new NodeEventEmitter();
|
|
64
|
+
const events: NodeEvent<unknown>[] = [];
|
|
65
|
+
|
|
66
|
+
eventEmitter.onAll((event: NodeEvent<unknown>) => {
|
|
67
|
+
events.push(event);
|
|
68
|
+
onEvent?.(event);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const registry = getRegistry();
|
|
72
|
+
|
|
73
|
+
const context: TemporalContext = {
|
|
74
|
+
blackboard,
|
|
75
|
+
treeRegistry: registry,
|
|
76
|
+
timestamp: Date.now(),
|
|
77
|
+
eventEmitter,
|
|
78
|
+
sessionId: `playground-${Date.now()}`,
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
const status = await tree.tick(context);
|
|
83
|
+
return {
|
|
84
|
+
status,
|
|
85
|
+
blackboard: blackboard.toJSON(),
|
|
86
|
+
events,
|
|
87
|
+
};
|
|
88
|
+
} catch (err) {
|
|
89
|
+
return {
|
|
90
|
+
status: NodeStatus.FAILURE,
|
|
91
|
+
blackboard: blackboard.toJSON(),
|
|
92
|
+
events,
|
|
93
|
+
error: err instanceof Error ? err.message : String(err),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import type { TreeNode } from '@btree/types';
|
|
2
|
+
|
|
3
|
+
export interface LayoutNode {
|
|
4
|
+
id: string;
|
|
5
|
+
name: string;
|
|
6
|
+
type: string;
|
|
7
|
+
category: 'composite' | 'decorator' | 'action' | 'condition';
|
|
8
|
+
x: number;
|
|
9
|
+
y: number;
|
|
10
|
+
width: number;
|
|
11
|
+
height: number;
|
|
12
|
+
children: LayoutNode[];
|
|
13
|
+
status: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface LayoutEdge {
|
|
17
|
+
fromX: number;
|
|
18
|
+
fromY: number;
|
|
19
|
+
toX: number;
|
|
20
|
+
toY: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const NODE_WIDTH = 160;
|
|
24
|
+
const NODE_HEIGHT = 44;
|
|
25
|
+
const H_GAP = 16;
|
|
26
|
+
const V_GAP = 56;
|
|
27
|
+
|
|
28
|
+
const COMPOSITE_TYPES = new Set([
|
|
29
|
+
'Sequence', 'Selector', 'Parallel', 'Conditional', 'ForEach',
|
|
30
|
+
'While', 'Recovery', 'ReactiveSequence', 'MemorySequence', 'SubTree',
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
const DECORATOR_TYPES = new Set([
|
|
34
|
+
'Timeout', 'Delay', 'Repeat', 'Invert', 'ForceSuccess', 'ForceFailure',
|
|
35
|
+
'RunOnce', 'KeepRunningUntilFailure', 'Precondition', 'SoftAssert',
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
const CONDITION_TYPES = new Set([
|
|
39
|
+
'CheckCondition', 'AlwaysCondition',
|
|
40
|
+
]);
|
|
41
|
+
|
|
42
|
+
function getCategory(type: string): LayoutNode['category'] {
|
|
43
|
+
if (COMPOSITE_TYPES.has(type)) return 'composite';
|
|
44
|
+
if (DECORATOR_TYPES.has(type)) return 'decorator';
|
|
45
|
+
if (CONDITION_TYPES.has(type)) return 'condition';
|
|
46
|
+
return 'action';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function computeSubtreeWidth(node: TreeNode): number {
|
|
50
|
+
if (!node.children || node.children.length === 0) {
|
|
51
|
+
return NODE_WIDTH;
|
|
52
|
+
}
|
|
53
|
+
const childrenWidth = node.children.reduce(
|
|
54
|
+
(sum, child) => sum + computeSubtreeWidth(child),
|
|
55
|
+
0,
|
|
56
|
+
);
|
|
57
|
+
return Math.max(NODE_WIDTH, childrenWidth + H_GAP * (node.children.length - 1));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function layoutNode(
|
|
61
|
+
node: TreeNode,
|
|
62
|
+
x: number,
|
|
63
|
+
y: number,
|
|
64
|
+
nodeStates: Map<string, string>,
|
|
65
|
+
): LayoutNode {
|
|
66
|
+
const subtreeWidth = computeSubtreeWidth(node);
|
|
67
|
+
const nodeX = x + subtreeWidth / 2 - NODE_WIDTH / 2;
|
|
68
|
+
|
|
69
|
+
const layoutChildren: LayoutNode[] = [];
|
|
70
|
+
if (node.children && node.children.length > 0) {
|
|
71
|
+
let childX = x;
|
|
72
|
+
const childY = y + NODE_HEIGHT + V_GAP;
|
|
73
|
+
for (const child of node.children) {
|
|
74
|
+
const childWidth = computeSubtreeWidth(child);
|
|
75
|
+
layoutChildren.push(layoutNode(child, childX, childY, nodeStates));
|
|
76
|
+
childX += childWidth + H_GAP;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
id: node.id,
|
|
82
|
+
name: node.name || node.id,
|
|
83
|
+
type: node.type,
|
|
84
|
+
category: getCategory(node.type),
|
|
85
|
+
x: nodeX,
|
|
86
|
+
y,
|
|
87
|
+
width: NODE_WIDTH,
|
|
88
|
+
height: NODE_HEIGHT,
|
|
89
|
+
children: layoutChildren,
|
|
90
|
+
status: nodeStates.get(node.id) || 'idle',
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function layoutTree(
|
|
95
|
+
root: TreeNode,
|
|
96
|
+
nodeStates: Map<string, string> = new Map(),
|
|
97
|
+
): { root: LayoutNode; width: number; height: number; edges: LayoutEdge[] } {
|
|
98
|
+
const layoutRoot = layoutNode(root, 0, 0, nodeStates);
|
|
99
|
+
|
|
100
|
+
function getMaxY(node: LayoutNode): number {
|
|
101
|
+
let maxY = node.y + node.height;
|
|
102
|
+
for (const child of node.children) {
|
|
103
|
+
maxY = Math.max(maxY, getMaxY(child));
|
|
104
|
+
}
|
|
105
|
+
return maxY;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function getMaxX(node: LayoutNode): number {
|
|
109
|
+
let maxX = node.x + node.width;
|
|
110
|
+
for (const child of node.children) {
|
|
111
|
+
maxX = Math.max(maxX, getMaxX(child));
|
|
112
|
+
}
|
|
113
|
+
return maxX;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const edges: LayoutEdge[] = [];
|
|
117
|
+
function collectEdges(node: LayoutNode) {
|
|
118
|
+
for (const child of node.children) {
|
|
119
|
+
edges.push({
|
|
120
|
+
fromX: node.x + node.width / 2,
|
|
121
|
+
fromY: node.y + node.height,
|
|
122
|
+
toX: child.x + child.width / 2,
|
|
123
|
+
toY: child.y,
|
|
124
|
+
});
|
|
125
|
+
collectEdges(child);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
collectEdges(layoutRoot);
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
root: layoutRoot,
|
|
132
|
+
width: getMaxX(layoutRoot),
|
|
133
|
+
height: getMaxY(layoutRoot),
|
|
134
|
+
edges,
|
|
135
|
+
};
|
|
136
|
+
}
|