Merge branch 'dev'

This commit is contained in:
codetypes committed 2025-04-13 14:34:26 +08:00
commit 826b69a587
31 files changed
+4781 -2907

No files matched your search

+1
View File
@@ -137,6 +137,7 @@ async function createWindow(projectPath?: string) {
trafficLightPosition: { x: 10, y: 10 }, trafficLightPosition: { x: 10, y: 10 },
webPreferences: { webPreferences: {
preload, preload,
webSecurity: false,
// Warning: Enable nodeIntegration and disable contextIsolation is not secure in production // Warning: Enable nodeIntegration and disable contextIsolation is not secure in production
// Consider using contextBridge.exposeInMainWorld // Consider using contextBridge.exposeInMainWorld
// Read more on https://www.electronjs.org/docs/latest/tutorial/context-isolation // Read more on https://www.electronjs.org/docs/latest/tutorial/context-isolation
+1 -1
View File
@@ -4,7 +4,7 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/x-icon" href="/favicon.ico" /> <link rel="icon" type="image/x-icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline';" /> <!-- <meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline';" /> -->
<title>Behavior3 Editor</title> <title>Behavior3 Editor</title>
</head> </head>
<body> <body>
+2516 -882
View File
File diff suppressed because it is too large. Load diff
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "behavior3editor", "name": "behavior3editor",
"version": "1.8.1", "version": "1.8.2",
"main": "dist-electron/main/index.js", "main": "dist-electron/main/index.js",
"description": "行为树编辑器", "description": "行为树编辑器",
"author": "zhandouxiaojiji & codetypes", "author": "zhandouxiaojiji & codetypes",
@@ -27,7 +27,7 @@
"homepage": "https://github.com/zhandouxiaojiji/behavior3editor#readme", "homepage": "https://github.com/zhandouxiaojiji/behavior3editor#readme",
"dependencies": { "dependencies": {
"@ant-design/icons": "^5.4.0", "@ant-design/icons": "^5.4.0",
"@antv/g6": "^4.8.24", "@antv/g6": "^5.0.45",
"@electron/remote": "github:electron/remote", "@electron/remote": "github:electron/remote",
"@types/electron-localshortcut": "^3.1.3", "@types/electron-localshortcut": "^3.1.3",
"@types/node": "^20.14.14", "@types/node": "^20.14.14",
+4 -1
View File
@@ -84,7 +84,7 @@
"tree.name": "Tree name", "tree.name": "Tree name",
"tree.desc": "Tree desc", "tree.desc": "Tree desc",
"tree.export": "Export", "tree.export": "Export",
"tree.firstid": "First id", "tree.prefix": "ID prefix",
"tree.group": "Enable Node Grouping", "tree.group": "Enable Node Grouping",
"tree.group.placeholder": "Select node groups", "tree.group.placeholder": "Select node groups",
"tree.vars": "Define Variables", "tree.vars": "Define Variables",
@@ -105,9 +105,11 @@
"node.children": "Num children", "node.children": "Num children",
"node.debug": "Debug", "node.debug": "Debug",
"node.deleteRootNodeDenied": "Unable to delete the root node", "node.deleteRootNodeDenied": "Unable to delete the root node",
"node.dropDenied": "This node position is denied to drop the node",
"node.desc": "Node desc", "node.desc": "Node desc",
"node.disabled": "Disabled", "node.disabled": "Disabled",
"node.editSubtreeDenied": "Unable to edit the subtree node", "node.editSubtreeDenied": "Unable to edit the subtree node",
"node.editFailed": "Node '{{name}}' edit failed",
"node.group": "Node group", "node.group": "Node group",
"node.groupNotEnabled": "One of '{{group}}' node group is not enabled in current file", "node.groupNotEnabled": "One of '{{group}}' node group is not enabled in current file",
"node.id": "Node id", "node.id": "Node id",
@@ -126,6 +128,7 @@
"node.rootNodeAsSubtree": "Root node cann't be save as subtree", "node.rootNodeAsSubtree": "Root node cann't be save as subtree",
"node.subtree": "Subtree", "node.subtree": "Subtree",
"node.subtreePathError": "You should save the subtree inside the current workspace", "node.subtreePathError": "You should save the subtree inside the current workspace",
"node.subtreeSaveRootError": "Root node cann't be save as subtree",
"node.type": "Node type", "node.type": "Node type",
"node.undefinedVariable": "Variable '{{variable}}' is not defined", "node.undefinedVariable": "Variable '{{variable}}' is not defined",
+4 -1
View File
@@ -84,7 +84,7 @@
"tree.name": "行为树", "tree.name": "行为树",
"tree.desc": "说明", "tree.desc": "说明",
"tree.export": "导出", "tree.export": "导出",
"tree.firstid": "起始标识", "tree.prefix": "标识前缀",
"tree.group": "启用节点分组", "tree.group": "启用节点分组",
"tree.group.placeholder": "选择节点分组", "tree.group.placeholder": "选择节点分组",
"tree.vars": "定义变量", "tree.vars": "定义变量",
@@ -105,9 +105,11 @@
"node.children": "子节点数量", "node.children": "子节点数量",
"node.debug": "调试", "node.debug": "调试",
"node.deleteRootNodeDenied": "不能删除根节点", "node.deleteRootNodeDenied": "不能删除根节点",
"node.dropDenied": "此节点位置不允许放置拖拽的节点",
"node.desc": "节点说明", "node.desc": "节点说明",
"node.disabled": "禁用", "node.disabled": "禁用",
"node.editSubtreeDenied": "不能编辑子树节点", "node.editSubtreeDenied": "不能编辑子树节点",
"node.editFailed": "节点 {{name}} 编辑失败",
"node.group": "节点分组", "node.group": "节点分组",
"node.groupNotEnabled": "节点分组 “{{group}}” 任意一个未在当前文件中启用", "node.groupNotEnabled": "节点分组 “{{group}}” 任意一个未在当前文件中启用",
"node.id": "节点标识", "node.id": "节点标识",
@@ -126,6 +128,7 @@
"node.rootNodeAsSubtree": "根节点不能保存为子树", "node.rootNodeAsSubtree": "根节点不能保存为子树",
"node.subtree": "子树", "node.subtree": "子树",
"node.subtreePathError": "子树必须保存在当前工作区的目录内", "node.subtreePathError": "子树必须保存在当前工作区的目录内",
"node.subtreeSaveRootError": "根节点不能保存为子树",
"node.type": "节点类型", "node.type": "节点类型",
"node.undefinedVariable": "变量 “{{variable}}” 未定义", "node.undefinedVariable": "变量 “{{variable}}” 未定义",
"node.unknown.desc": "新建节点" "node.unknown.desc": "新建节点"
+1 -1
View File
@@ -11,7 +11,7 @@
firstid: number; firstid: number;
group: string[]; group: string[];
import: string[]; import: string[];
declvar: VarDef[]; vars: { name: string; desc: string }[];
root: NodeModel; root: NodeModel;
} }
+1 -1
View File
@@ -11,7 +11,7 @@
firstid: number; firstid: number;
group: string[]; group: string[];
import: string[]; import: string[];
declvar: VarDef[]; vars: { name: string; desc: string }[];
root: NodeModel; root: NodeModel;
} }
+2 -4
View File
@@ -6,7 +6,7 @@
"export": true, "export": true,
"group": [], "group": [],
"import": [], "import": [],
"declvar": [ "vars": [
{ {
"name": "context", "name": "context",
"desc": "上下文" "desc": "上下文"
@@ -22,8 +22,6 @@
"args": { "args": {
"time": 1 "time": 1
}, },
"input": [ "input": ["version"]
"version"
]
} }
} }
+6 -7
View File
@@ -1,12 +1,12 @@
{ {
"version": "1.8.1",
"name": "declare-vars", "name": "declare-vars",
"desc": "", "desc": "",
"firstid": 1, "firstid": 1,
"export": true, "export": true,
"import": [ "group": [],
"vars/declare-core.json" "import": ["vars/declare-core.json"],
], "vars": [
"declvar": [
{ {
"name": "owner", "name": "owner",
"desc": "所有者" "desc": "所有者"
@@ -17,8 +17,7 @@
} }
], ],
"root": { "root": {
"id": 1, "id": "1",
"name": "Sequence" "name": "Sequence"
}, }
"group": []
} }
+5 -3
View File
@@ -1,21 +1,23 @@
{ {
"version": "1.8.1",
"name": "test-subtree", "name": "test-subtree",
"desc": "", "desc": "",
"firstid": 1, "firstid": 1,
"export": true, "export": true,
"group": [],
"import": ["vars/declare-core.json"], "import": ["vars/declare-core.json"],
"declvar": [ "vars": [
{ {
"name": "test", "name": "test",
"desc": "测试" "desc": "测试"
} }
], ],
"root": { "root": {
"id": 1, "id": "1",
"name": "Sequence", "name": "Sequence",
"children": [ "children": [
{ {
"id": 2, "id": "2",
"name": "Log", "name": "Log",
"args": { "args": {
"message": "hello" "message": "hello"
+18 -16
View File
@@ -1,8 +1,8 @@
{ {
"version": "1.8.0", "version": "1.8.1",
"name": "test-vars", "name": "test-vars",
"desc": "", "desc": "",
"firstid": 1, "prefix": "A",
"export": true, "export": true,
"group": [ "group": [
"Client", "Client",
@@ -12,7 +12,7 @@
"vars/declare-core.json", "vars/declare-core.json",
"vars/declare-vars.json" "vars/declare-vars.json"
], ],
"declvar": [ "vars": [
{ {
"name": "target", "name": "target",
"desc": "目标" "desc": "目标"
@@ -23,52 +23,52 @@
} }
], ],
"root": { "root": {
"id": 1, "id": "1",
"name": "Sequence", "name": "Sequence",
"children": [ "children": [
{ {
"id": 2, "id": "2",
"name": "Sequence", "name": "Sequence",
"path": "vars/test-subtree.json" "path": "vars/test-subtree.json"
}, },
{ {
"id": 4, "id": "4",
"name": "FindEnemy", "name": "FindEnemy",
"args": {},
"output": [ "output": [
"target1" "target1"
] ],
"args": {}
}, },
{ {
"id": 5, "id": "5",
"name": "Attack", "name": "Attack",
"input": [ "input": [
"target1" "target1"
] ]
}, },
{ {
"id": 6, "id": "6",
"name": "Check", "name": "Check",
"args": { "args": {
"value": "target[0].x > 0 " "value": "target[0].x > 0 "
} }
}, },
{ {
"id": 7, "id": "7",
"name": "MoveToTarget", "name": "MoveToTarget",
"input": [ "input": [
"target" "target"
] ]
}, },
{ {
"id": 8, "id": "8",
"name": "Clear", "name": "Clear",
"output": [ "output": [
"test" "test"
] ]
}, },
{ {
"id": 9, "id": "9",
"name": "TestB3", "name": "TestB3",
"args": { "args": {
"time": 1, "time": 1,
@@ -79,7 +79,7 @@
} }
}, },
{ {
"id": 10, "id": "10",
"name": "TestB3", "name": "TestB3",
"args": { "args": {
"time": 1, "time": 1,
@@ -90,15 +90,17 @@
} }
}, },
{ {
"id": 11, "id": "11",
"name": "Clear", "name": "Clear",
"output": [ "output": [
"context" "context"
] ]
}, },
{ {
"id": 12, "id": "12",
"name": "Listen", "name": "Listen",
"input": [],
"output": [],
"args": { "args": {
"builtin": "afterRunFailure" "builtin": "afterRunFailure"
} }
+8 -23
View File
@@ -4,12 +4,9 @@
"desc": "英雄测试AI", "desc": "英雄测试AI",
"firstid": 1, "firstid": 1,
"export": true, "export": true,
"group": [ "group": ["Client", "Server"],
"Client",
"Server"
],
"import": [], "import": [],
"declvar": [], "vars": [],
"root": { "root": {
"id": 1, "id": 1,
"name": "Selector", "name": "Selector",
@@ -29,16 +26,12 @@
"w": 100, "w": 100,
"h": 50 "h": 50
}, },
"output": [ "output": ["enemy"]
"enemy"
]
}, },
{ {
"id": 4, "id": 4,
"name": "Attack", "name": "Attack",
"input": [ "input": ["enemy"]
"enemy"
]
}, },
{ {
"id": 5, "id": 5,
@@ -63,16 +56,12 @@
"x": 0, "x": 0,
"y": 0 "y": 0
}, },
"output": [ "output": ["enemy"]
"enemy"
]
}, },
{ {
"id": 8, "id": 8,
"name": "MoveToTarget", "name": "MoveToTarget",
"input": [ "input": ["enemy"]
"enemy"
]
} }
] ]
}, },
@@ -84,9 +73,7 @@
{ {
"id": 10, "id": 10,
"name": "GetHp", "name": "GetHp",
"output": [ "output": ["hp"]
"hp"
]
}, },
{ {
"id": 11, "id": 11,
@@ -94,9 +81,7 @@
"args": { "args": {
"lt": 50 "lt": 50
}, },
"input": [ "input": ["hp"]
"hp"
]
}, },
{ {
"id": 12, "id": 12,
+4 -10
View File
@@ -4,11 +4,9 @@
"desc": "怪物测试AI", "desc": "怪物测试AI",
"firstid": 1, "firstid": 1,
"export": true, "export": true,
"group": [ "group": ["Test"],
"Test"
],
"import": [], "import": [],
"declvar": [], "vars": [],
"root": { "root": {
"id": 1, "id": 1,
"name": "Sequence", "name": "Sequence",
@@ -22,9 +20,7 @@
{ {
"id": 3, "id": 3,
"name": "GetHp", "name": "GetHp",
"output": [ "output": ["hp"],
"hp"
],
"children": [ "children": [
{ {
"id": 4, "id": 4,
@@ -41,9 +37,7 @@
"value": "hp", "value": "hp",
"gt": 50 "gt": 50
}, },
"input": [ "input": ["hp"]
"hp"
]
}, },
{ {
"id": 7, "id": 7,
+1 -1
View File
@@ -5,7 +5,7 @@
"firstid": 1, "firstid": 1,
"group": [], "group": [],
"import": [], "import": [],
"declvar": [], "vars": [],
"root": { "root": {
"id": 1, "id": 1,
"name": "Sequence", "name": "Sequence",
+3 -7
View File
@@ -5,7 +5,7 @@
"firstid": 1, "firstid": 1,
"group": [], "group": [],
"import": [], "import": [],
"declvar": [], "vars": [],
"root": { "root": {
"id": 1, "id": 1,
"name": "Sequence", "name": "Sequence",
@@ -14,16 +14,12 @@
{ {
"id": 2, "id": 2,
"name": "Cmp", "name": "Cmp",
"input": [ "input": ["d"]
"d"
]
}, },
{ {
"id": 3, "id": 3,
"name": "Attack", "name": "Attack",
"input": [ "input": ["in"]
"in"
]
} }
] ]
} }
+1 -1
View File
@@ -5,7 +5,7 @@
"firstid": 1, "firstid": 1,
"group": [], "group": [],
"import": [], "import": [],
"declvar": [], "vars": [],
"root": { "root": {
"id": 1, "id": 1,
"name": "Sequence", "name": "Sequence",
+2 -4
View File
@@ -5,7 +5,7 @@
"firstid": 1, "firstid": 1,
"group": [], "group": [],
"import": [], "import": [],
"declvar": [], "vars": [],
"root": { "root": {
"id": 1, "id": 1,
"name": "Sequence", "name": "Sequence",
@@ -15,9 +15,7 @@
"id": 2, "id": 2,
"name": "Cmp", "name": "Cmp",
"args": {}, "args": {},
"input": [ "input": ["x"]
"x"
]
} }
] ]
} }
+145 -1124
View File
File diff suppressed because it is too large. Load diff
+2
View File
@@ -857,6 +857,7 @@ export const Explorer: FC = () => {
<Dropdown menu={{ items: contextMenu, onClick }} trigger={["contextMenu"]}> <Dropdown menu={{ items: contextMenu, onClick }} trigger={["contextMenu"]}>
<div> <div>
<DirectoryTree <DirectoryTree
virtual
tabIndex={-1} tabIndex={-1}
treeData={workspace.fileTree ? [workspace.fileTree] : []} treeData={workspace.fileTree ? [workspace.fileTree] : []}
fieldNames={{ key: "path", title: "path" }} fieldNames={{ key: "path", title: "path" }}
@@ -955,6 +956,7 @@ export const Explorer: FC = () => {
</div> </div>
</Dropdown> </Dropdown>
<DirectoryTree <DirectoryTree
virtual
tabIndex={-1} tabIndex={-1}
fieldNames={{ key: "path", title: "path" }} fieldNames={{ key: "path", title: "path" }}
treeData={[nodeTree]} treeData={[nodeTree]}
File diff suppressed because it is too large. Load diff
+111 -71
View File
@@ -29,7 +29,7 @@ import { ExpressionEvaluator } from "../behavior3/src/behavior3";
import { EditNode, EditTree, useWorkspace } from "../contexts/workspace-context"; import { EditNode, EditTree, useWorkspace } from "../contexts/workspace-context";
import { import {
hasArgOptions, hasArgOptions,
ImportDef, ImportDecl,
isBoolType, isBoolType,
isExprType, isExprType,
isFloatType, isFloatType,
@@ -37,9 +37,8 @@ import {
isJsonType, isJsonType,
isStringType, isStringType,
NodeArg, NodeArg,
NodeModel, NodeData,
TreeGraphData, VarDecl,
VarDef,
} from "../misc/b3type"; } from "../misc/b3type";
import { import {
checkNodeArgValue, checkNodeArgValue,
@@ -47,12 +46,12 @@ import {
getNodeArgRawType, getNodeArgRawType,
isNodeArgArray, isNodeArgArray,
isNodeArgOptional, isNodeArgOptional,
isValidChildren,
isValidVariableName, isValidVariableName,
isVariadic, isVariadic,
parseExpr, parseExpr,
usingGroups,
usingVars,
} from "../misc/b3util"; } from "../misc/b3util";
import { message } from "../misc/hooks";
import i18n from "../misc/i18n"; import i18n from "../misc/i18n";
import { Hotkey, isMacos } from "../misc/keys"; import { Hotkey, isMacos } from "../misc/keys";
import { mergeClassNames } from "../misc/util"; import { mergeClassNames } from "../misc/util";
@@ -61,7 +60,7 @@ interface OptionType extends DefaultOptionType {
value: string; value: string;
} }
interface VarItem extends VarDef { interface VarItem extends VarDecl {
count?: number; count?: number;
} }
@@ -69,11 +68,11 @@ interface VarItemProps {
name: number; name: number;
disabled?: boolean; disabled?: boolean;
value?: VarItem; value?: VarItem;
onChange?: (vardef: VarItem) => void; onChange?: (value: VarItem) => void;
onRemove?: (name: number | number[]) => void; onRemove?: (name: number | number[]) => void;
} }
const VarDefItem: FC<VarItemProps> = ({ name, onChange, onRemove, disabled, ...props }) => { const VarDeclItem: FC<VarItemProps> = ({ name, onChange, onRemove, disabled, ...props }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const form = useFormInstance(); const form = useFormInstance();
const [value, setValue] = useState<VarItem>(props.value ?? { name: "", desc: "" }); const [value, setValue] = useState<VarItem>(props.value ?? { name: "", desc: "" });
@@ -152,6 +151,7 @@ const TreeInspector: FC = () => {
relative: state.relative, relative: state.relative,
open: state.open, open: state.open,
workdir: state.workdir, workdir: state.workdir,
usingVars: state.usingVars,
})) }))
); );
const { t } = useTranslation(); const { t } = useTranslation();
@@ -160,7 +160,7 @@ const TreeInspector: FC = () => {
// using count // using count
const usingCount: Record<string, number> = useMemo(() => { const usingCount: Record<string, number> = useMemo(() => {
const count: Record<string, number> = {}; const count: Record<string, number> = {};
const collect = (node: TreeGraphData) => { const collect = (node: NodeData) => {
const def = workspace.nodeDefs.get(node.name); const def = workspace.nodeDefs.get(node.name);
if (def.input) { if (def.input) {
node.input?.forEach((v) => { node.input?.forEach((v) => {
@@ -195,7 +195,7 @@ const TreeInspector: FC = () => {
}; };
collect(workspace.editingTree.root); collect(workspace.editingTree.root);
return count; return count;
}, [workspace.editingTree, workspace.nodeDefs]); }, [workspace.editingTree, workspace.nodeDefs, workspace.usingVars]);
// auto complete for subtree // auto complete for subtree
const subtreeOptions = useMemo(() => { const subtreeOptions = useMemo(() => {
@@ -218,11 +218,11 @@ const TreeInspector: FC = () => {
form.setFieldValue("name", workspace.editingTree.name); form.setFieldValue("name", workspace.editingTree.name);
form.setFieldValue("desc", workspace.editingTree.desc); form.setFieldValue("desc", workspace.editingTree.desc);
form.setFieldValue("export", workspace.editingTree.export !== false); form.setFieldValue("export", workspace.editingTree.export !== false);
form.setFieldValue("firstid", workspace.editingTree.firstid); form.setFieldValue("prefix", workspace.editingTree.prefix);
form.setFieldValue("group", workspace.editingTree.group); form.setFieldValue("group", workspace.editingTree.group);
form.setFieldValue( form.setFieldValue(
"declvar", "vars",
workspace.editingTree.declvar.map((v) => ({ workspace.editingTree.vars.map((v) => ({
name: v.name, name: v.name,
desc: v.desc, desc: v.desc,
count: usingCount[v.name] ?? 0, count: usingCount[v.name] ?? 0,
@@ -258,16 +258,16 @@ const TreeInspector: FC = () => {
name: values.name, name: values.name,
desc: values.desc, desc: values.desc,
export: values.export, export: values.export,
firstid: Number(values.firstid), prefix: values.prefix,
group: ((values.group ?? []) as string[]).filter((g) => g).sort((a, b) => a.localeCompare(b)), group: ((values.group ?? []) as string[]).filter((g) => g).sort((a, b) => a.localeCompare(b)),
declvar: (values.declvar as VarDef[]) vars: (values.vars as VarDecl[])
.filter((v) => v && v.name) .filter((v) => v && v.name)
.map((v) => ({ .map((v) => ({
name: v.name, name: v.name,
desc: v.desc, desc: v.desc,
})) }))
.sort((a, b) => a.name.localeCompare(b.name)), .sort((a, b) => a.name.localeCompare(b.name)),
import: (values.import as ImportDef[]) import: (values.import as ImportDecl[])
.filter((v) => v && v.path) .filter((v) => v && v.path)
.sort((a, b) => a.path.localeCompare(b.path)) .sort((a, b) => a.path.localeCompare(b.path))
.map((v) => ({ .map((v) => ({
@@ -299,8 +299,8 @@ const TreeInspector: FC = () => {
<Form.Item name="desc" label={t("tree.desc")}> <Form.Item name="desc" label={t("tree.desc")}>
<TextArea autoSize onBlur={form.submit} /> <TextArea autoSize onBlur={form.submit} />
</Form.Item> </Form.Item>
<Form.Item name="firstid" label={t("tree.firstid")}> <Form.Item name="prefix" label={t("tree.prefix")}>
<InputNumber min={1} onBlur={form.submit} /> <Input onBlur={form.submit} />
</Form.Item> </Form.Item>
<Form.Item name="export" label={t("tree.export")} valuePropName="checked"> <Form.Item name="export" label={t("tree.export")} valuePropName="checked">
<Switch onChange={() => form.submit()} /> <Switch onChange={() => form.submit()} />
@@ -326,7 +326,7 @@ const TreeInspector: FC = () => {
<Divider orientation="left"> <Divider orientation="left">
<h4>{t("tree.vars")}</h4> <h4>{t("tree.vars")}</h4>
</Divider> </Divider>
<Form.List name="declvar"> <Form.List name="vars">
{(fields, { add, remove }, { errors }) => ( {(fields, { add, remove }, { errors }) => (
<div style={{ display: "flex", flexDirection: "column", rowGap: 0 }}> <div style={{ display: "flex", flexDirection: "column", rowGap: 0 }}>
{fields.map((item) => ( {fields.map((item) => (
@@ -351,7 +351,7 @@ const TreeInspector: FC = () => {
}, },
]} ]}
> >
<VarDefItem name={item.name} onRemove={remove} /> <VarDeclItem name={item.name} onRemove={remove} />
</Form.Item> </Form.Item>
))} ))}
<Form.Item <Form.Item
@@ -421,7 +421,7 @@ const TreeInspector: FC = () => {
<div style={{ display: "flex", flexDirection: "column", rowGap: 0 }}> <div style={{ display: "flex", flexDirection: "column", rowGap: 0 }}>
{vars.map((v) => ( {vars.map((v) => (
<Form.Item key={v.key} name={v.name} style={{ marginBottom: 2 }}> <Form.Item key={v.key} name={v.name} style={{ marginBottom: 2 }}>
<VarDefItem name={v.name} disabled={true} /> <VarDeclItem name={v.name} disabled={true} />
</Form.Item> </Form.Item>
))} ))}
</div> </div>
@@ -477,7 +477,7 @@ const TreeInspector: FC = () => {
<div style={{ display: "flex", flexDirection: "column", rowGap: 0 }}> <div style={{ display: "flex", flexDirection: "column", rowGap: 0 }}>
{vars.map((v) => ( {vars.map((v) => (
<Form.Item key={v.key} name={v.name} style={{ marginBottom: 2 }}> <Form.Item key={v.key} name={v.name} style={{ marginBottom: 2 }}>
<VarDefItem name={v.name} disabled={true} /> <VarDeclItem name={v.name} disabled={true} />
</Form.Item> </Form.Item>
))} ))}
</div> </div>
@@ -513,7 +513,12 @@ const TreeInspector: FC = () => {
); );
}; };
const validateArg = (node: NodeModel, arg: NodeArg, value: unknown) => { const validateArg = (
node: NodeData,
arg: NodeArg,
value: unknown,
usingVars: Record<string, VarDecl> | null
) => {
const type = getNodeArgRawType(arg); const type = getNodeArgRawType(arg);
const required = !isNodeArgOptional(arg); const required = !isNodeArgOptional(arg);
if (isExprType(type) && value) { if (isExprType(type) && value) {
@@ -559,6 +564,8 @@ const NodeInspector: FC = () => {
fileTree: state.fileTree, fileTree: state.fileTree,
groupDefs: state.groupDefs, groupDefs: state.groupDefs,
nodeDefs: state.nodeDefs, nodeDefs: state.nodeDefs,
usingGroups: state.usingGroups,
usingVars: state.usingVars,
onEditingNode: state.onEditingNode, onEditingNode: state.onEditingNode,
relative: state.relative, relative: state.relative,
})) }))
@@ -572,12 +579,22 @@ const NodeInspector: FC = () => {
100 100
); );
const submit = () => {
if (form.getFieldsError().some((e) => e.errors.length > 0)) {
const data = workspace.editingNode.data;
const editor = workspace.editing!;
const name = `${editor.data.prefix}${data.id} ${data.name}`;
message.error(t("node.editFailed", { name }));
}
form.submit();
};
// set form values // set form values
useEffect(() => { useEffect(() => {
const data = workspace.editingNode.data; const data = workspace.editingNode.data;
const def = workspace.nodeDefs.get(workspace.editingNode.data.name); const def = workspace.nodeDefs.get(workspace.editingNode.data.name);
form.resetFields(); form.resetFields();
form.setFieldValue("id", data.id); form.setFieldValue("id", workspace.editingNode.prefix + data.id);
form.setFieldValue("name", data.name); form.setFieldValue("name", data.name);
form.setFieldValue("type", def.type); form.setFieldValue("type", def.type);
form.setFieldValue("desc", data.desc || def.desc); form.setFieldValue("desc", data.desc || def.desc);
@@ -647,11 +664,12 @@ const NodeInspector: FC = () => {
const inoutVarOptions = useMemo(() => { const inoutVarOptions = useMemo(() => {
const options: OptionType[] = []; const options: OptionType[] = [];
const filter: Record<string, boolean> = {}; const filter: Record<string, boolean> = {};
const collect = (node?: TreeGraphData) => { const collect = (node?: NodeData) => {
if (node) { if (node) {
const def = workspace.nodeDefs.get(node.name);
node.input?.forEach((v, i) => { node.input?.forEach((v, i) => {
let desc: string; let desc: string;
const inputDef = node.def.input; const inputDef = def.input;
if (inputDef && i >= inputDef.length && isVariadic(inputDef, -1)) { if (inputDef && i >= inputDef.length && isVariadic(inputDef, -1)) {
desc = inputDef[inputDef.length - 1]; desc = inputDef[inputDef.length - 1];
} else { } else {
@@ -664,7 +682,7 @@ const NodeInspector: FC = () => {
}); });
node.output?.forEach((v, i) => { node.output?.forEach((v, i) => {
let desc: string; let desc: string;
const outputDef = node.def.output; const outputDef = def.output;
if (outputDef && i >= outputDef.length && isVariadic(outputDef, -1)) { if (outputDef && i >= outputDef.length && isVariadic(outputDef, -1)) {
desc = outputDef[outputDef.length - 1]; desc = outputDef[outputDef.length - 1];
} else { } else {
@@ -678,18 +696,18 @@ const NodeInspector: FC = () => {
node.children?.forEach((child) => collect(child)); node.children?.forEach((child) => collect(child));
} }
}; };
if (usingVars) { if (workspace.usingVars) {
Object.values(usingVars ?? {}).forEach((v) => { Object.values(workspace.usingVars).forEach((v) => {
if (!filter[v.name]) { if (!filter[v.name]) {
options.push({ label: `${v.name}(${v.desc})`, value: v.name }); options.push({ label: `${v.name}(${v.desc})`, value: v.name });
filter[v.name] = true; filter[v.name] = true;
} }
}); });
} else { } else {
collect(workspace.editing?.root); collect(workspace.editing?.data.root);
} }
return options; return options;
}, [workspace.editing, usingVars]); }, [workspace.editing, workspace.usingVars]);
// auto complete for subtree // auto complete for subtree
const subtreeOptions = useMemo(() => { const subtreeOptions = useMemo(() => {
@@ -708,12 +726,12 @@ const NodeInspector: FC = () => {
const editingNode = workspace.editingNode; const editingNode = workspace.editingNode;
const def = workspace.nodeDefs.get(editingNode.data.name); const def = workspace.nodeDefs.get(editingNode.data.name);
const disabled = !editingNode.editable; const disabled = editingNode.disabled;
// update value // update value
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const finish = (values: any) => { const finish = (values: any) => {
const data = {} as NodeModel; const data = {} as NodeData;
data.id = editingNode.data.id; data.id = editingNode.data.id;
data.name = values.name; data.name = values.name;
data.debug = values.debug || undefined; data.debug = values.debug || undefined;
@@ -804,7 +822,8 @@ const NodeInspector: FC = () => {
debug: editingNode.data.debug, debug: editingNode.data.debug,
disabled: editingNode.data.disabled, disabled: editingNode.data.disabled,
}, },
editable: editingNode.editable, prefix: editingNode.prefix,
disabled: editingNode.disabled,
}); });
finish(form.getFieldsValue()); finish(form.getFieldsValue());
} else { } else {
@@ -835,6 +854,9 @@ const NodeInspector: FC = () => {
labelCol={{ span: "auto" }} labelCol={{ span: "auto" }}
onFinish={finish} onFinish={finish}
> >
<Form.Item name="id" label={t("node.id")}>
<Input disabled={true} />
</Form.Item>
<Form.Item name="type" label={t("node.type")}> <Form.Item name="type" label={t("node.type")}>
<Input disabled={true} /> <Input disabled={true} />
</Form.Item> </Form.Item>
@@ -845,7 +867,7 @@ const NodeInspector: FC = () => {
rules={[ rules={[
{ {
validator() { validator() {
if (def.group && !def.group.some((g) => usingGroups[g])) { if (def.group && !def.group.some((g) => workspace.usingGroups?.[g])) {
return Promise.reject( return Promise.reject(
new Error(t("node.groupNotEnabled", { group: def.group })) new Error(t("node.groupNotEnabled", { group: def.group }))
); );
@@ -869,7 +891,7 @@ const NodeInspector: FC = () => {
rules={[ rules={[
{ {
validator() { validator() {
if (editingNode.limitError) { if (!isValidChildren(editingNode.data)) {
return Promise.reject(new Error(t("node.invalidChildren"))); return Promise.reject(new Error(t("node.invalidChildren")));
} }
return Promise.resolve(); return Promise.resolve();
@@ -908,17 +930,17 @@ const NodeInspector: FC = () => {
/> />
</Form.Item> </Form.Item>
<Form.Item name="desc" label={t("node.desc")}> <Form.Item name="desc" label={t("node.desc")}>
<TextArea autoSize disabled={disabled} onBlur={form.submit} /> <TextArea autoSize disabled={disabled} onBlur={submit} />
</Form.Item> </Form.Item>
<Form.Item label={t("node.debug")} name="debug" valuePropName="checked"> <Form.Item label={t("node.debug")} name="debug" valuePropName="checked">
<Switch disabled={disabled && !editingNode.data.path} onChange={form.submit} /> <Switch disabled={disabled && !editingNode.data.path} onChange={submit} />
</Form.Item> </Form.Item>
<Form.Item label={t("node.disabled")} name="disabled" valuePropName="checked"> <Form.Item label={t("node.disabled")} name="disabled" valuePropName="checked">
<Switch disabled={disabled && !editingNode.data.path} onChange={form.submit} /> <Switch disabled={disabled && !editingNode.data.path} onChange={submit} />
</Form.Item> </Form.Item>
<Form.Item label={t("node.subtree")} name="path"> <Form.Item label={t("node.subtree")} name="path">
<AutoComplete <AutoComplete
disabled={disabled && !editingNode.data.path} disabled={disabled && !editingNode.subtreeEditable}
options={subtreeOptions} options={subtreeOptions}
onBlur={changeSubtree} onBlur={changeSubtree}
onInputKeyDown={(e) => e.code === Hotkey.Escape && e.preventDefault()} onInputKeyDown={(e) => e.code === Hotkey.Escape && e.preventDefault()}
@@ -958,7 +980,11 @@ const NodeInspector: FC = () => {
rules={[ rules={[
{ {
validator(_, value) { validator(_, value) {
if (value && usingVars && !usingVars[value]) { if (
value &&
workspace.usingVars &&
!workspace.usingVars[value]
) {
return Promise.reject( return Promise.reject(
new Error( new Error(
t("node.undefinedVariable", { variable: value }) t("node.undefinedVariable", { variable: value })
@@ -978,7 +1004,7 @@ const NodeInspector: FC = () => {
<AutoComplete <AutoComplete
disabled={disabled} disabled={disabled}
options={inoutVarOptions} options={inoutVarOptions}
onBlur={form.submit} onBlur={submit}
onInputKeyDown={(e) => onInputKeyDown={(e) =>
e.code === Hotkey.Escape && e.preventDefault() e.code === Hotkey.Escape && e.preventDefault()
} }
@@ -994,7 +1020,7 @@ const NodeInspector: FC = () => {
style={{ marginBottom: "6px" }} style={{ marginBottom: "6px" }}
onClick={() => { onClick={() => {
remove(field.name); remove(field.name);
form.submit(); submit();
}} }}
/> />
</Flex> </Flex>
@@ -1027,7 +1053,7 @@ const NodeInspector: FC = () => {
{ required, message: t("fieldRequired", { field: desc }) }, { required, message: t("fieldRequired", { field: desc }) },
({ getFieldValue, setFieldValue, isFieldValidating, validateFields }) => ({ ({ getFieldValue, setFieldValue, isFieldValidating, validateFields }) => ({
validator(_, value) { validator(_, value) {
if (value && usingVars && !usingVars[value]) { if (value && workspace.usingVars && !workspace.usingVars[value]) {
return Promise.reject( return Promise.reject(
new Error(t("node.undefinedVariable", { variable: value })) new Error(t("node.undefinedVariable", { variable: value }))
); );
@@ -1067,7 +1093,7 @@ const NodeInspector: FC = () => {
<AutoComplete <AutoComplete
disabled={disabled} disabled={disabled}
options={inoutVarOptions} options={inoutVarOptions}
onBlur={form.submit} onBlur={submit}
onInputKeyDown={(e) => e.code === Hotkey.Escape && e.preventDefault()} onInputKeyDown={(e) => e.code === Hotkey.Escape && e.preventDefault()}
filterOption={(inputValue: string, option?: OptionType) => { filterOption={(inputValue: string, option?: OptionType) => {
const label = option!.label as string; const label = option!.label as string;
@@ -1112,39 +1138,44 @@ const NodeInspector: FC = () => {
}, },
() => ({ () => ({
validator(_, value) { validator(_, value) {
return validateArg(editingNode.data, arg, value); return validateArg(
editingNode.data,
arg,
value,
workspace.usingVars
);
}, },
}), }),
]} ]}
> >
{!hasArgOptions(arg) && isStringType(type) && ( {!hasArgOptions(arg) && isStringType(type) && (
<TextArea autoSize disabled={disabled} onBlur={form.submit} /> <TextArea autoSize disabled={disabled} onBlur={submit} />
)} )}
{!hasArgOptions(arg) && isJsonType(type) && ( {!hasArgOptions(arg) && isJsonType(type) && (
<TextArea autoSize disabled={disabled} onBlur={form.submit} /> <TextArea autoSize disabled={disabled} onBlur={submit} />
)} )}
{!hasArgOptions(arg) && isIntType(type) && ( {!hasArgOptions(arg) && isIntType(type) && (
<InputNumber <InputNumber
disabled={disabled} disabled={disabled}
onBlur={form.submit} onBlur={submit}
precision={0} precision={0}
/> />
)} )}
{!hasArgOptions(arg) && isFloatType(type) && ( {!hasArgOptions(arg) && isFloatType(type) && (
<InputNumber disabled={disabled} onBlur={form.submit} /> <InputNumber disabled={disabled} onBlur={submit} />
)} )}
{!hasArgOptions(arg) && isBoolType(type) && ( {!hasArgOptions(arg) && isBoolType(type) && (
<Switch disabled={disabled} onChange={form.submit} /> <Switch disabled={disabled} onChange={submit} />
)} )}
{!hasArgOptions(arg) && isExprType(type) && ( {!hasArgOptions(arg) && isExprType(type) && (
<Input disabled={disabled} onBlur={form.submit} /> <Input disabled={disabled} onBlur={submit} />
)} )}
{hasArgOptions(arg) && ( {hasArgOptions(arg) && (
<Select <Select
showSearch showSearch
disabled={disabled} disabled={disabled}
onBlur={form.submit} onBlur={submit}
onChange={form.submit} onChange={submit}
options={(arg.options ?? []).map((option) => { options={(arg.options ?? []).map((option) => {
return { return {
value: option.value, value: option.value,
@@ -1164,7 +1195,7 @@ const NodeInspector: FC = () => {
style={{ marginBottom: "6px" }} style={{ marginBottom: "6px" }}
onClick={() => { onClick={() => {
remove(item.name); remove(item.name);
form.submit(); submit();
}} }}
/> />
</Flex> </Flex>
@@ -1182,7 +1213,7 @@ const NodeInspector: FC = () => {
onClick={() => { onClick={() => {
add(arg.default ?? isBoolType(type) ? false : ""); add(arg.default ?? isBoolType(type) ? false : "");
if (isBoolType(type)) { if (isBoolType(type)) {
form.submit(); submit();
} }
}} }}
style={{ width: "100%" }} style={{ width: "100%" }}
@@ -1210,7 +1241,12 @@ const NodeInspector: FC = () => {
{ required, message: t("fieldRequired", { field: arg.desc }) }, { required, message: t("fieldRequired", { field: arg.desc }) },
({ getFieldValue, setFieldValue, isFieldValidating, validateFields }) => ({ ({ getFieldValue, setFieldValue, isFieldValidating, validateFields }) => ({
async validator(_, value) { async validator(_, value) {
return validateArg(editingNode.data, arg, value).then((result) => { return validateArg(
editingNode.data,
arg,
value,
workspace.usingVars
).then((result) => {
value = result; value = result;
if (!arg.oneof) { if (!arg.oneof) {
return Promise.resolve(); return Promise.resolve();
@@ -1247,29 +1283,29 @@ const NodeInspector: FC = () => {
]} ]}
> >
{!hasArgOptions(arg) && isStringType(type) && ( {!hasArgOptions(arg) && isStringType(type) && (
<TextArea autoSize disabled={disabled} onBlur={form.submit} /> <TextArea autoSize disabled={disabled} onBlur={submit} />
)} )}
{!hasArgOptions(arg) && isJsonType(type) && ( {!hasArgOptions(arg) && isJsonType(type) && (
<TextArea autoSize disabled={disabled} onBlur={form.submit} /> <TextArea autoSize disabled={disabled} onBlur={submit} />
)} )}
{!hasArgOptions(arg) && isIntType(type) && ( {!hasArgOptions(arg) && isIntType(type) && (
<InputNumber disabled={disabled} onBlur={form.submit} precision={0} /> <InputNumber disabled={disabled} onBlur={submit} precision={0} />
)} )}
{!hasArgOptions(arg) && isFloatType(type) && ( {!hasArgOptions(arg) && isFloatType(type) && (
<InputNumber disabled={disabled} onBlur={form.submit} /> <InputNumber disabled={disabled} onBlur={submit} />
)} )}
{!hasArgOptions(arg) && isBoolType(type) && ( {!hasArgOptions(arg) && isBoolType(type) && (
<Switch disabled={disabled} onChange={form.submit} /> <Switch disabled={disabled} onChange={submit} />
)} )}
{!hasArgOptions(arg) && isExprType(type) && ( {!hasArgOptions(arg) && isExprType(type) && (
<Input disabled={disabled} onBlur={form.submit} /> <Input disabled={disabled} onBlur={submit} />
)} )}
{hasArgOptions(arg) && ( {hasArgOptions(arg) && (
<Select <Select
showSearch showSearch
disabled={disabled} disabled={disabled}
onBlur={form.submit} onBlur={submit}
onChange={form.submit} onChange={submit}
options={(arg.options ?? []).map((option) => { options={(arg.options ?? []).map((option) => {
return { return {
value: option.value, value: option.value,
@@ -1319,7 +1355,11 @@ const NodeInspector: FC = () => {
rules={[ rules={[
{ {
validator(_, value) { validator(_, value) {
if (value && usingVars && !usingVars[value]) { if (
value &&
workspace.usingVars &&
!workspace.usingVars[value]
) {
return Promise.reject( return Promise.reject(
new Error( new Error(
t("node.undefinedVariable", { variable: value }) t("node.undefinedVariable", { variable: value })
@@ -1339,7 +1379,7 @@ const NodeInspector: FC = () => {
<AutoComplete <AutoComplete
disabled={disabled} disabled={disabled}
options={inoutVarOptions} options={inoutVarOptions}
onBlur={form.submit} onBlur={submit}
onInputKeyDown={(e) => onInputKeyDown={(e) =>
e.code === Hotkey.Escape && e.preventDefault() e.code === Hotkey.Escape && e.preventDefault()
} }
@@ -1355,7 +1395,7 @@ const NodeInspector: FC = () => {
style={{ marginBottom: "6px" }} style={{ marginBottom: "6px" }}
onClick={() => { onClick={() => {
remove(field.name); remove(field.name);
form.submit(); submit();
}} }}
/> />
</Flex> </Flex>
@@ -1388,7 +1428,7 @@ const NodeInspector: FC = () => {
{ required, message: t("fieldRequired", { field: desc }) }, { required, message: t("fieldRequired", { field: desc }) },
{ {
validator(_, value) { validator(_, value) {
if (value && usingVars && !usingVars[value]) { if (value && workspace.usingVars && !workspace.usingVars[value]) {
return Promise.reject( return Promise.reject(
new Error(t("node.undefinedVariable", { variable: value })) new Error(t("node.undefinedVariable", { variable: value }))
); );
@@ -1404,7 +1444,7 @@ const NodeInspector: FC = () => {
<AutoComplete <AutoComplete
disabled={disabled} disabled={disabled}
options={inoutVarOptions} options={inoutVarOptions}
onBlur={form.submit} onBlur={submit}
onInputKeyDown={(e) => e.code === Hotkey.Escape && e.preventDefault()} onInputKeyDown={(e) => e.code === Hotkey.Escape && e.preventDefault()}
filterOption={(value: string, option?: OptionType) => { filterOption={(value: string, option?: OptionType) => {
const label = option!.label as string; const label = option!.label as string;
File diff suppressed because it is too large. Load diff
+5 -5
View File
@@ -64,7 +64,7 @@ export const Workspace: FC = () => {
useKeyDown(Hotkey.CloseEditor, null, (event) => { useKeyDown(Hotkey.CloseEditor, null, (event) => {
if (workspace.editing) { if (workspace.editing) {
event.preventDefault(); event.preventDefault();
if (workspace.editing.unsave) { if (workspace.editing.changed) {
showSaveDialog(workspace.editing); showSaveDialog(workspace.editing);
} else { } else {
workspace.close(workspace.editing.path); workspace.close(workspace.editing.path);
@@ -266,7 +266,7 @@ export const Workspace: FC = () => {
}; };
window.onbeforeunload = (e) => { window.onbeforeunload = (e) => {
const unsaves = workspace.editors.filter((editor) => editor.unsave); const unsaves = workspace.editors.filter((editor) => editor.changed);
if (unsaves.length) { if (unsaves.length) {
showSaveAllDialog(unsaves); showSaveAllDialog(unsaves);
return false; return false;
@@ -441,7 +441,7 @@ export const Workspace: FC = () => {
if (action === "remove") { if (action === "remove") {
const path = activeKey as string; const path = activeKey as string;
const editor = workspace.find(path); const editor = workspace.find(path);
if (editor && editor.unsave) { if (editor && editor.changed) {
showSaveDialog(editor); showSaveDialog(editor);
} else { } else {
workspace.close(path); workspace.close(path);
@@ -470,11 +470,11 @@ export const Workspace: FC = () => {
}} }}
title={<div style={{ width: "max-content" }}>{v.path}</div>} title={<div style={{ width: "max-content" }}>{v.path}</div>}
> >
{`${Path.basename(v.path)}${v.unsave ? "*" : ""}`} {`${Path.basename(v.path)}${v.changed ? "*" : ""}`}
</Tooltip> </Tooltip>
), ),
key: v.path, key: v.path,
children: <Editor data={v} onUpdate={forceUpdate} />, children: <Editor data={v} onChange={forceUpdate} />,
}; };
})} })}
/> />
+2 -1
View File
@@ -1,6 +1,7 @@
import { app } from "@electron/remote"; import { app } from "@electron/remote";
import * as fs from "fs"; import * as fs from "fs";
import { create } from "zustand"; import { create } from "zustand";
import { NodeLayout } from "../misc/b3type";
import { readJson, writeJson } from "../misc/util"; import { readJson, writeJson } from "../misc/util";
import { useWorkspace } from "./workspace-context"; import { useWorkspace } from "./workspace-context";
@@ -9,7 +10,7 @@ const settingPath = app.getPath("userData") + "/settings.json";
export type SettingModel = { export type SettingModel = {
recent: string[]; recent: string[];
buildDir: string; buildDir: string;
layout?: "compact" | "normal"; layout: NodeLayout;
}; };
export type SettingStore = { export type SettingStore = {
+46 -66
View File
@@ -1,18 +1,10 @@
import { Matrix, TreeGraph } from "@antv/g6";
import { BrowserWindow, dialog } from "@electron/remote"; import { BrowserWindow, dialog } from "@electron/remote";
import { ipcRenderer } from "electron"; import { ipcRenderer } from "electron";
import * as fs from "fs"; import * as fs from "fs";
import React from "react"; import React from "react";
import { create } from "zustand"; import { create } from "zustand";
import { NodeDef } from "../behavior3/src/behavior3"; import { NodeDef } from "../behavior3/src/behavior3";
import { import { FileVarDecl, ImportDecl, NodeData, TreeData, VarDecl } from "../misc/b3type";
FileVarDecl,
ImportDef,
NodeModel,
TreeGraphData,
TreeModel,
VarDef,
} from "../misc/b3type";
import * as b3util from "../misc/b3util"; import * as b3util from "../misc/b3util";
import { message } from "../misc/hooks"; import { message } from "../misc/hooks";
import i18n from "../misc/i18n"; import i18n from "../misc/i18n";
@@ -24,6 +16,7 @@ import { useSetting } from "./setting-context";
let buildDir: string | undefined; let buildDir: string | undefined;
export type EditEvent = export type EditEvent =
| "close"
| "save" | "save"
| "copy" | "copy"
| "paste" | "paste"
@@ -45,25 +38,14 @@ export type EditEvent =
export class EditorStore { export class EditorStore {
path: string; path: string;
data: TreeModel; data: TreeData;
root: TreeGraphData;
declare: FileVarDecl; declare: FileVarDecl;
autoId: number = 1; changed: boolean = false;
dragSrcId?: string; mtime: number;
dragDstId?: string;
unsave: boolean = false;
modifiedTime: number = Date.now();
alertReload: boolean = false; alertReload: boolean = false;
searchingText?: string; focusId?: string | null;
historyStack: string[] = [];
historyIndex: number = 0;
selectedId: string | null = null;
graphMatrix?: Matrix;
graph!: TreeGraph;
dispatch!: (event: EditEvent, data?: unknown) => void; dispatch!: (event: EditEvent, data?: unknown) => void;
@@ -71,25 +53,12 @@ export class EditorStore {
this.path = path; this.path = path;
this.data = readTree(path); this.data = readTree(path);
this.data.name = Path.basenameWithoutExt(path); this.data.name = Path.basenameWithoutExt(path);
this.root = b3util.createTreeData(this.data.root); this.mtime = fs.statSync(path).mtimeMs;
this.modifiedTime = fs.statSync(path).mtimeMs;
this.declare = { this.declare = {
import: this.data.import.map((v) => ({ path: v, vars: [], depends: [] })), import: this.data.import.map((v) => ({ path: v, vars: [], depends: [] })),
subtree: [], subtree: [],
declvar: this.data.declvar.map((v) => ({ ...v })), vars: this.data.vars.map((v) => ({ ...v })),
}; };
this.autoId = b3util.refreshTreeDataId(this.root, this.data.firstid);
this.historyStack.push(
JSON.stringify(
{
...this.data,
root: b3util.createNode(this.root),
},
null,
2
)
);
this.historyIndex = 0;
} }
} }
@@ -105,9 +74,11 @@ export type FileTreeType = {
}; };
export type EditNode = { export type EditNode = {
data: NodeModel; data: NodeData;
editable: boolean; error?: boolean;
limitError?: boolean; prefix: string;
disabled: boolean;
subtreeEditable?: boolean;
}; };
export type EditNodeDef = { export type EditNodeDef = {
@@ -119,12 +90,12 @@ export type EditTree = {
name: string; name: string;
desc?: string; desc?: string;
export?: boolean; export?: boolean;
firstid?: number; prefix?: string;
group: string[]; group: string[];
import: ImportDef[]; import: ImportDecl[];
declvar: VarDef[]; vars: VarDecl[];
subtree: ImportDef[]; subtree: ImportDecl[];
root: TreeGraphData; root: NodeData;
}; };
export type FileMeta = { export type FileMeta = {
@@ -170,12 +141,12 @@ export type WorkspaceStore = {
isShowingSearch: boolean; isShowingSearch: boolean;
onShowingSearch: (isShowingSearch: boolean) => void; onShowingSearch: (isShowingSearch: boolean) => void;
open: (path: string, selectedId?: number) => void; open: (path: string, focusId?: string) => void;
edit: (path: string, selectedId?: number) => void; edit: (path: string, focusId?: string) => void;
close: (path: string) => void; close: (path: string) => void;
find: (path: string) => EditorStore | undefined; find: (path: string) => EditorStore | undefined;
relative: (path: string) => string; relative: (path: string) => string;
refresh: (path: string) => boolean; refresh: (path: string) => void;
save: () => void; save: () => void;
saveAs: () => void; saveAs: () => void;
@@ -187,6 +158,8 @@ export type WorkspaceStore = {
loadNodeDefs: () => void; loadNodeDefs: () => void;
nodeDefs: b3util.NodeDefs; nodeDefs: b3util.NodeDefs;
groupDefs: string[]; groupDefs: string[];
usingGroups: typeof b3util.usingGroups;
usingVars: typeof b3util.usingVars;
// edit node // edit node
editingNode?: EditNode | null; editingNode?: EditNode | null;
@@ -238,7 +211,7 @@ const loadFileTree = (workdir: string, filename: string) => {
}; };
const saveFile = (editor?: EditorStore) => { const saveFile = (editor?: EditorStore) => {
if (editor?.unsave) { if (editor?.changed) {
editor.dispatch("save"); editor.dispatch("save");
} }
}; };
@@ -427,7 +400,7 @@ export const useWorkspace = create<WorkspaceStore>((set, get) => ({
nodeDefs: get().nodeDefs, nodeDefs: get().nodeDefs,
}); });
workspace.allFiles.forEach((file) => { workspace.allFiles.forEach((file) => {
let tree: TreeModel | null = readTree(file.path); let tree: TreeData | null = readTree(file.path);
tree = b3util.processBatch(tree, file.path, batch); tree = b3util.processBatch(tree, file.path, batch);
if (tree) { if (tree) {
batch.onWriteFile?.(file.path, tree); batch.onWriteFile?.(file.path, tree);
@@ -492,7 +465,7 @@ export const useWorkspace = create<WorkspaceStore>((set, get) => ({
set({ isShowingSearch }); set({ isShowingSearch });
}, },
open: (path, selectedId) => { open: (path, focusId) => {
path = Path.posixPath(path); path = Path.posixPath(path);
const workspace = get(); const workspace = get();
let editor = workspace.editors.find((v) => v.path === path); let editor = workspace.editors.find((v) => v.path === path);
@@ -502,7 +475,7 @@ export const useWorkspace = create<WorkspaceStore>((set, get) => ({
workspace.editors.push(editor); workspace.editors.push(editor);
set({ editors: workspace.editors }); set({ editors: workspace.editors });
workspace.updateFileMeta(editor); workspace.updateFileMeta(editor);
workspace.edit(editor.path, selectedId); workspace.edit(editor.path, focusId);
if (b3util.isNewVersion(editor.data.version)) { if (b3util.isNewVersion(editor.data.version)) {
message.warning(i18n.t("alertNewVersion", { version: editor.data.version })); message.warning(i18n.t("alertNewVersion", { version: editor.data.version }));
@@ -512,19 +485,18 @@ export const useWorkspace = create<WorkspaceStore>((set, get) => ({
message.error(`invalid file: ${path}`); message.error(`invalid file: ${path}`);
} }
} else if (workspace.editing !== editor) { } else if (workspace.editing !== editor) {
workspace.edit(editor.path, selectedId); workspace.edit(editor.path, focusId);
} }
}, },
edit: (path, selectedId) => { edit: (path, focusId) => {
const workspace = get(); const workspace = get();
const editor = workspace.editors.find((v) => v.path === path); const editor = workspace.editors.find((v) => v.path === path);
if (editor && selectedId) { if (editor) {
editor.selectedId = selectedId.toString(); editor.focusId = focusId;
} }
set({ editing: editor, editingNode: null, editingTree: null }); set({ editing: editor, editingNode: null, editingTree: null });
if (editor) { if (editor) {
workspace.refresh(editor.path);
workspace.onEditingTree(editor); workspace.onEditingTree(editor);
} }
}, },
@@ -533,7 +505,9 @@ export const useWorkspace = create<WorkspaceStore>((set, get) => ({
const workspace = get(); const workspace = get();
const idx = workspace.editors.findIndex((v) => v.path === path); const idx = workspace.editors.findIndex((v) => v.path === path);
const editors = workspace.editors.filter((v) => v.path !== path); const editors = workspace.editors.filter((v) => v.path !== path);
const editor = workspace.editors.find((v) => v.path === path);
let editting = workspace.editing; let editting = workspace.editing;
editor?.dispatch?.("close");
if (editors.length && path === editting?.path) { if (editors.length && path === editting?.path) {
editting = editors[idx === editors.length ? idx - 1 : idx]; editting = editors[idx === editors.length ? idx - 1 : idx];
workspace.onEditingTree(editting); workspace.onEditingTree(editting);
@@ -561,7 +535,11 @@ export const useWorkspace = create<WorkspaceStore>((set, get) => ({
if (!editor) { if (!editor) {
return false; return false;
} }
return b3util.refreshDeclare(editor.root, editor.data.group, editor.declare); b3util.refreshVarDecl(editor.data.root, editor.data.group, editor.declare);
set({
usingGroups: b3util.usingGroups,
usingVars: b3util.usingVars,
});
}, },
save: () => { save: () => {
@@ -592,7 +570,7 @@ export const useWorkspace = create<WorkspaceStore>((set, get) => ({
hasEvent = true; hasEvent = true;
} }
} }
if (event === "change" && filename) { if (filename && (event === "change" || workspace.allFiles.has(filename))) {
if (filename === "node-config.b3-setting") { if (filename === "node-config.b3-setting") {
workspace.loadNodeDefs(); workspace.loadNodeDefs();
} else { } else {
@@ -600,8 +578,8 @@ export const useWorkspace = create<WorkspaceStore>((set, get) => ({
const editor = workspace.find(fullpath); const editor = workspace.find(fullpath);
const modified = fs.statSync(fullpath).mtimeMs; const modified = fs.statSync(fullpath).mtimeMs;
b3util.files[Path.posixPath(filename)] = modified; b3util.files[Path.posixPath(filename)] = modified;
if (editor && editor.modifiedTime + 500 < modified) { if (editor && editor.mtime + 500 < modified) {
if (editor.unsave) { if (editor.changed) {
editor.alertReload = true; editor.alertReload = true;
set({ modifiedTime: Date.now() }); set({ modifiedTime: Date.now() });
} else { } else {
@@ -642,7 +620,7 @@ export const useWorkspace = create<WorkspaceStore>((set, get) => ({
} }
fileMeta.exists = fs.existsSync(fileNode.path); fileMeta.exists = fs.existsSync(fileNode.path);
if (fileMeta.desc === undefined) { if (fileMeta.desc === undefined) {
const file = readJson(fileNode.path) as TreeModel; const file = readJson(fileNode.path) as TreeData;
fileMeta.desc = file.desc ?? ""; fileMeta.desc = file.desc ?? "";
updated = true; updated = true;
} }
@@ -668,6 +646,8 @@ export const useWorkspace = create<WorkspaceStore>((set, get) => ({
nodeDefs: new b3util.NodeDefs(), nodeDefs: new b3util.NodeDefs(),
groupDefs: [], groupDefs: [],
usingGroups: null,
usingVars: null,
loadNodeDefs: () => { loadNodeDefs: () => {
const workspace = get(); const workspace = get();
b3util.initWorkdir(workspace.workdir, message.error.bind(message)); b3util.initWorkdir(workspace.workdir, message.error.bind(message));
@@ -691,10 +671,10 @@ export const useWorkspace = create<WorkspaceStore>((set, get) => ({
set({ set({
editingTree: { editingTree: {
...editor.data, ...editor.data,
root: editor.root, root: editor.data.root,
import: editor.declare.import, import: editor.declare.import,
declvar: editor.declare.declvar,
subtree: editor.declare.subtree, subtree: editor.declare.subtree,
vars: editor.declare.vars,
}, },
editingNodeDef: null, editingNodeDef: null,
editingNode: null, editingNode: null,
+76
View File
@@ -0,0 +1,76 @@
export {}; // Make this a module
declare global {
interface Array<T> {
unique(): T[];
first(): T | undefined;
last(): T | undefined;
groupBy<K>(keyFn: (item: T) => K): Map<K, T[]>;
remove(item: T): T[];
removeAt(index: number): T | undefined;
insertAt(index: number, item: T): void;
emplace(item: T, target: T, where: "before" | "after"): void;
}
}
Array.prototype.unique = function <T>(this: T[]): T[] {
return [...new Set(this)];
};
Array.prototype.first = function <T>(this: T[]): T | undefined {
return this[0];
};
Array.prototype.last = function <T>(this: T[]): T | undefined {
return this[this.length - 1];
};
Array.prototype.remove = function <T>(this: T[], item: T): T[] {
const removed: T[] = [];
let i = 0;
while (i < this.length) {
if (this[i] === item) {
removed.push(this[i]);
this.splice(i, 1);
} else {
i++;
}
}
return removed;
};
Array.prototype.removeAt = function <T>(this: T[], index: number): T | undefined {
return this.splice(index, 1)[0];
};
Array.prototype.insertAt = function <T>(this: T[], index: number, item: T): void {
this.splice(index, 0, item);
};
Array.prototype.groupBy = function <T, K>(this: T[], keyFn: (item: T) => K): Map<K, T[]> {
return this.reduce((map, item) => {
const key = keyFn(item);
const group = map.get(key) || [];
group.push(item);
map.set(key, group);
return map;
}, new Map<K, T[]>());
};
Array.prototype.emplace = function <T>(
this: T[],
item: T,
target: T,
where: "before" | "after"
): void {
const index = this.indexOf(target);
if (index !== -1) {
if (where === "before") {
this.splice(index, 0, item);
} else {
this.splice(index + 1, 0, item);
}
} else {
this.push(item);
}
};
+21 -37
View File
@@ -1,4 +1,3 @@
import type { TreeGraphData as G6TreeGraphData } from "@antv/g6";
import { NodeDef } from "../behavior3/src/behavior3"; import { NodeDef } from "../behavior3/src/behavior3";
export const VERSION = "1.8.1"; export const VERSION = "1.8.1";
@@ -14,33 +13,40 @@ export const isExprType = (type: string) => type.startsWith("expr") || type.star
export const isJsonType = (type: string) => type.startsWith("json"); export const isJsonType = (type: string) => type.startsWith("json");
export const hasArgOptions = (arg: NodeArg) => arg.options !== undefined; export const hasArgOptions = (arg: NodeArg) => arg.options !== undefined;
export interface NodeModel { export interface NodeData {
id: number; id: string;
name: string; name: string;
desc?: string; desc?: string;
args?: { [key: string]: unknown }; args?: { [key: string]: unknown };
input?: string[]; input?: string[];
output?: string[]; output?: string[];
children?: NodeModel[]; children?: NodeData[];
debug?: boolean; debug?: boolean;
disabled?: boolean; disabled?: boolean;
path?: string; path?: string;
// for runtime
mtime?: number;
size?: number[];
status?: number;
} }
export interface VarDef { export type NodeLayout = "compact" | "normal";
export interface VarDecl {
name: string; name: string;
desc: string; desc: string;
} }
export interface GroupDef { export interface GroupDecl {
name: string; name: string;
value: boolean; value: boolean;
} }
export interface ImportDef { export interface ImportDecl {
path: string; path: string;
modified?: number; modified?: number;
vars: VarDef[]; vars: VarDecl[];
depends: { depends: {
path: string; path: string;
modified: number; modified: number;
@@ -48,43 +54,21 @@ export interface ImportDef {
} }
export interface FileVarDecl { export interface FileVarDecl {
import: ImportDef[]; import: ImportDecl[];
subtree: ImportDef[]; subtree: ImportDecl[];
declvar: VarDef[]; vars: VarDecl[];
} }
export interface TreeModel { export interface TreeData {
version: string; version: string;
name: string; name: string;
prefix: string;
desc?: string; desc?: string;
export?: boolean; export?: boolean;
firstid: number;
group: string[]; group: string[];
import: string[]; import: string[];
declvar: VarDef[]; vars: VarDecl[];
root: NodeModel; root: NodeData;
}
export interface TreeGraphData extends G6TreeGraphData {
name: string;
desc?: string;
args?: { [key: string]: unknown };
input?: string[];
output?: string[];
children?: TreeGraphData[];
def: NodeDef;
debug?: boolean;
disabled?: boolean;
parent?: string;
path?: string;
lastModified?: number;
size: number[];
highlightInput?: boolean;
highlightOutput?: boolean;
highlightArgs?: boolean;
highlightGray?: boolean;
status?: number;
} }
export const getNodeType = (def: NodeDef): NodeType => { export const getNodeType = (def: NodeDef): NodeType => {
+151 -174
View File
@@ -1,9 +1,11 @@
import assert from "assert";
import * as fs from "fs"; import * as fs from "fs";
import { ExpressionEvaluator, NodeDef } from "../behavior3/src/behavior3"; import { ExpressionEvaluator, NodeDef } from "../behavior3/src/behavior3";
import "./array";
import { import {
FileVarDecl, FileVarDecl,
hasArgOptions, hasArgOptions,
ImportDef, ImportDecl,
isBoolType, isBoolType,
isExprType, isExprType,
isFloatType, isFloatType,
@@ -11,10 +13,9 @@ import {
isJsonType, isJsonType,
isStringType, isStringType,
NodeArg, NodeArg,
NodeModel, NodeData,
TreeGraphData, TreeData,
TreeModel, VarDecl,
VarDef,
VERSION, VERSION,
} from "./b3type"; } from "./b3type";
import Path from "./path"; import Path from "./path";
@@ -35,19 +36,20 @@ type Env = {
export interface BatchScript { export interface BatchScript {
onSetup?(env: Env): void; onSetup?(env: Env): void;
onProcessTree?(tree: TreeModel, path: string): TreeModel | null; onProcessTree?(tree: TreeData, path: string): TreeData | null;
onProcessNode?(node: NodeModel): NodeModel | null; onProcessNode?(node: NodeData): NodeData | null;
onWriteFile?(path: string, tree: TreeModel): void; onWriteFile?(path: string, tree: TreeData): void;
onComplete?(status: "success" | "failure"): void; onComplete?(status: "success" | "failure"): void;
} }
export let calcSize: (d: NodeData) => number[] = () => [0, 0];
export let nodeDefs: NodeDefs = new NodeDefs(); export let nodeDefs: NodeDefs = new NodeDefs();
export let groupDefs: string[] = []; export let groupDefs: string[] = [];
export let usingGroups: Record<string, boolean> = {}; export let usingGroups: Record<string, boolean> | null = null;
export let usingVars: Record<string, VarDef> | null = null; export let usingVars: Record<string, VarDecl> | null = null;
export const files: Record<string, number> = {}; export const files: Record<string, number> = {};
const parsedVarDefs: Record<string, ImportDef> = {}; const parsedVarDecl: Record<string, ImportDecl> = {};
const parsedExprs: Record<string, string[]> = {}; const parsedExprs: Record<string, string[]> = {};
let checkExpr: boolean = false; let checkExpr: boolean = false;
let workdir: string = ""; let workdir: string = "";
@@ -72,15 +74,19 @@ export const initWorkdir = (path: string, handler: typeof alertError) => {
groupDefs = Array.from(groups).sort(); groupDefs = Array.from(groups).sort();
}; };
export const setSizeCalculator = (calc: (d: NodeData) => number[]) => {
calcSize = calc;
};
export const updateUsingGroups = (group: string[]) => { export const updateUsingGroups = (group: string[]) => {
usingGroups = {}; usingGroups = null;
for (const g of group) { for (const g of group) {
usingGroups ??= {}; usingGroups ??= {};
usingGroups[g] = true; usingGroups[g] = true;
} }
}; };
export const updateUsingVars = (vars: VarDef[]) => { export const updateUsingVars = (vars: VarDecl[]) => {
usingVars = null; usingVars = null;
for (const v of vars) { for (const v of vars) {
usingVars ??= {}; usingVars ??= {};
@@ -104,6 +110,26 @@ export const parseExpr = (expr: string) => {
return result; return result;
}; };
export const dfs = <T extends { children?: T[] }>(
node: T,
visitor: (node: T, depth: number) => unknown,
depth: number = 0
) => {
const traverse = (n: T, d: number) => {
if (visitor(n, d) === false) {
return false;
}
if (n.children) {
for (const child of n.children) {
if (traverse(child, d + 1) === false) {
return false;
}
}
}
};
traverse(node, depth);
};
export const isNewVersion = (version: string) => { export const isNewVersion = (version: string) => {
const [major, minor, patch] = version.split(".").map(Number); const [major, minor, patch] = version.split(".").map(Number);
const [major2, minor2, patch2] = VERSION.split(".").map(Number); const [major2, minor2, patch2] = VERSION.split(".").map(Number);
@@ -118,11 +144,11 @@ export const isValidVariableName = (name: string) => {
return /^[a-zA-Z_]/.test(name); return /^[a-zA-Z_]/.test(name);
}; };
export const isSubtreeRoot = (data: TreeGraphData) => { export const isSubtreeRoot = (data: NodeData) => {
return data.path && data.id.toString() !== "1"; return data.path && data.id !== "1";
}; };
export const isNodeEqual = (node1: NodeModel, node2: NodeModel) => { export const isNodeEqual = (node1: NodeData, node2: NodeData) => {
if ( if (
node1.name === node2.name && node1.name === node2.name &&
node1.desc === node2.desc && node1.desc === node2.desc &&
@@ -161,7 +187,7 @@ export const isNodeEqual = (node1: NodeModel, node2: NodeModel) => {
return false; return false;
}; };
const error = (data: NodeModel | TreeGraphData, msg: string) => { const error = (data: NodeData, msg: string) => {
console.error(`check ${data.id}|${data.name}: ${msg}`); console.error(`check ${data.id}|${data.name}: ${msg}`);
}; };
@@ -178,7 +204,7 @@ export const isNodeArgOptional = (arg: NodeArg) => {
}; };
export const checkNodeArgValue = ( export const checkNodeArgValue = (
data: NodeModel | TreeGraphData, data: NodeData,
arg: NodeArg, arg: NodeArg,
value: unknown, value: unknown,
verbose?: boolean verbose?: boolean
@@ -257,21 +283,18 @@ export const checkNodeArgValue = (
return !hasError; return !hasError;
}; };
export const checkNodeArg = ( export const checkNodeArg = (data: NodeData, conf: NodeDef, i: number, verbose?: boolean) => {
data: NodeModel | TreeGraphData,
conf: NodeDef,
i: number,
verbose?: boolean
) => {
let hasError = false; let hasError = false;
const arg = conf.args![i] as NodeArg; const arg = conf.args![i] as NodeArg;
const value = data.args?.[arg.name]; const value = data.args?.[arg.name];
if (isNodeArgArray(arg)) { if (isNodeArgArray(arg)) {
if (!Array.isArray(value) || (!isNodeArgOptional(arg) && value.length === 0)) { if (!Array.isArray(value) || value.length === 0) {
if (!isNodeArgOptional(arg)) {
if (verbose) { if (verbose) {
error(data, `'${arg.name}=${JSON.stringify(value)}' is not an array or empty array`); error(data, `'${arg.name}=${JSON.stringify(value)}' is not an array or empty array`);
} }
hasError = true; hasError = true;
}
} else { } else {
for (let j = 0; j < value.length; j++) { for (let j = 0; j < value.length; j++) {
if (!checkNodeArgValue(data, arg, value[j], verbose)) { if (!checkNodeArgValue(data, arg, value[j], verbose)) {
@@ -304,7 +327,7 @@ export const checkOneof = (argValue: unknown, inputValue: unknown) => {
return (argValue !== "" && inputValue === "") || (argValue === "" && inputValue !== ""); return (argValue !== "" && inputValue === "") || (argValue === "" && inputValue !== "");
}; };
export const checkNodeData = (data: NodeModel | null | undefined) => { export const checkNodeData = (data: NodeData | null | undefined) => {
if (!data) { if (!data) {
return false; return false;
} }
@@ -317,7 +340,7 @@ export const checkNodeData = (data: NodeModel | null | undefined) => {
let hasError = false; let hasError = false;
if (conf.group) { if (conf.group) {
if (!conf.group.some((g) => usingGroups[g])) { if (!conf.group.some((g) => usingGroups?.[g])) {
error(data, `node group '${conf.group}' is not enabled`); error(data, `node group '${conf.group}' is not enabled`);
hasError = true; hasError = true;
} }
@@ -487,22 +510,11 @@ export const checkNodeData = (data: NodeModel | null | undefined) => {
return !hasError; return !hasError;
}; };
export const copyFromNode = (data: TreeGraphData, node: NodeModel) => {
data.name = node.name;
data.debug = node.debug;
data.disabled = node.disabled;
data.desc = node.desc;
data.path = node.path;
data.args = node.args;
data.input = node.input;
data.output = node.output;
};
const parsingStack: string[] = []; const parsingStack: string[] = [];
export const createNode = (data: TreeGraphData, includeChildren: boolean = true) => { export const createNode = (data: NodeData, includeChildren: boolean = true) => {
const node: NodeModel = { const node: NodeData = {
id: Number(data.id), id: data.id,
name: data.name, name: data.name,
desc: data.desc, desc: data.desc,
path: data.path, path: data.path,
@@ -547,9 +559,10 @@ const enum StatusFlag {
FAILURE_ZERO = 4, FAILURE_ZERO = 4,
} }
const toStatusFlag = (data: TreeGraphData) => { const toStatusFlag = (data: NodeData) => {
let status = 0; let status = 0;
data.def.status?.forEach((s) => { const def = nodeDefs.get(data.name);
def.status?.forEach((s) => {
switch (s) { switch (s) {
case "success": case "success":
status |= 1 << StatusFlag.SUCCESS; status |= 1 << StatusFlag.SUCCESS;
@@ -578,15 +591,16 @@ const appendStatusFlag = (status: number, childStatus: number) => {
return status; return status;
}; };
const buildStatusFlag = (data: TreeGraphData, childStatus: number) => { const buildStatusFlag = (data: NodeData, childStatus: number) => {
let status = data.status!; let status = data.status!;
if (data.def.status?.length) { const def = nodeDefs.get(data.name);
if (def.status?.length) {
const childSuccess = (childStatus >> StatusFlag.SUCCESS) & 1; const childSuccess = (childStatus >> StatusFlag.SUCCESS) & 1;
const childFailure = (childStatus >> StatusFlag.FAILURE) & 1; const childFailure = (childStatus >> StatusFlag.FAILURE) & 1;
const childRunning = (childStatus >> StatusFlag.RUNNING) & 1; const childRunning = (childStatus >> StatusFlag.RUNNING) & 1;
const childHasZeroSuccess = (childStatus >> StatusFlag.SUCCESS_ZERO) & 1; const childHasZeroSuccess = (childStatus >> StatusFlag.SUCCESS_ZERO) & 1;
const childHasZeroFailure = (childStatus >> StatusFlag.FAILURE_ZERO) & 1; const childHasZeroFailure = (childStatus >> StatusFlag.FAILURE_ZERO) & 1;
data.def.status?.forEach((s) => { def.status?.forEach((s) => {
switch (s) { switch (s) {
case "!success": case "!success":
status |= childFailure << StatusFlag.SUCCESS; status |= childFailure << StatusFlag.SUCCESS;
@@ -625,31 +639,10 @@ const buildStatusFlag = (data: TreeGraphData, childStatus: number) => {
} }
}; };
export const refreshTreeDataId = (data: TreeGraphData, id?: number) => { export const isValidChildren = (data: NodeData) => {
if (!id) { const def = nodeDefs.get(data.name);
id = 1; if (def.children !== undefined && def.children !== -1) {
} return (data.children?.length || 0) === def.children;
const status = toStatusFlag(data);
data.id = (id++).toString();
data.status = status;
if (data.children) {
let childStatus = 0;
data.children.forEach((child) => {
child.parent = data.id;
id = refreshTreeDataId(child, id);
if (child.status && !child.disabled) {
childStatus = appendStatusFlag(childStatus, child.status);
}
});
buildStatusFlag(data, childStatus);
}
return id;
};
export const checkChildrenLimit = (data: TreeGraphData) => {
const conf = data.def;
if (conf.children !== undefined && conf.children !== -1) {
return (data.children?.length || 0) === conf.children;
} }
return true; return true;
}; };
@@ -665,28 +658,28 @@ const isValidInputOrOutput = (def: string[], data: string[] | undefined, index:
return def[index].includes("?") || data?.[index] || isVariadic(def, index); return def[index].includes("?") || data?.[index] || isVariadic(def, index);
}; };
export const checkTreeData = (data: TreeGraphData) => { export const checkTreeData = (data: NodeData) => {
const conf = data.def; const def = nodeDefs.get(data.name);
if (conf.input) { if (def.input) {
for (let i = 0; i < conf.input.length; i++) { for (let i = 0; i < def.input.length; i++) {
if (!isValidInputOrOutput(conf.input, data.input, i)) { if (!isValidInputOrOutput(def.input, data.input, i)) {
return false; return false;
} }
} }
} }
if (conf.output) { if (def.output) {
for (let i = 0; i < conf.output.length; i++) { for (let i = 0; i < def.output.length; i++) {
if (!isValidInputOrOutput(conf.output, data.output, i)) { if (!isValidInputOrOutput(def.output, data.output, i)) {
return false; return false;
} }
} }
} }
if (!checkChildrenLimit(data)) { if (!isValidChildren(data)) {
return false; return false;
} }
if (conf.args) { if (def.args) {
for (let i = 0; i < conf.args.length; i++) { for (let i = 0; i < def.args.length; i++) {
if (!checkNodeArg(data, conf, i, false)) { if (!checkNodeArg(data, def, i, false)) {
return false; return false;
} }
} }
@@ -695,92 +688,80 @@ export const checkTreeData = (data: TreeGraphData) => {
return true; return true;
}; };
export const createTreeData = ( export const refreshNodeData = (node: NodeData, id: number) => {
node: NodeModel, node.id = (id++).toString();
parent?: string, node.size = calcSize(node);
calcSize?: (d: TreeGraphData) => number[]
) => {
let treeData: TreeGraphData = {
id: node.id.toFixed(),
name: node.name,
desc: node.desc,
args: node.args,
input: node.input,
output: node.output,
debug: node.debug,
disabled: node.disabled,
def: nodeDefs.get(node.name),
parent: parent,
size: [0, 0],
};
treeData.def.args?.forEach((arg) => { const def = nodeDefs.get(node.name);
treeData.args ||= {};
if (treeData.args[arg.name] === undefined && arg.default !== undefined) { if (def.args) {
treeData.args[arg.name] = arg.default; node.args ||= {};
def.args.forEach((arg) => {
assert(node.args);
if (node.args[arg.name] === undefined && arg.default !== undefined) {
node.args[arg.name] = arg.default;
} }
}); });
if (calcSize) {
treeData.size = calcSize(treeData);
}
if (!parent) {
parsingStack.length = 0;
} }
if (node.path) { if (node.path) {
if (parsingStack.indexOf(node.path) >= 0) { if (parsingStack.indexOf(node.path) >= 0) {
treeData.path = node.path;
if (calcSize) {
treeData.size = calcSize(treeData);
}
alertError(`循环引用节点:${node.path}`, 4); alertError(`循环引用节点:${node.path}`, 4);
return treeData; return id;
} }
parsingStack.push(node.path); parsingStack.push(node.path);
try { try {
const subtreePath = workdir + "/" + node.path; const subtreePath = workdir + "/" + node.path;
treeData = createTreeData(readTree(subtreePath).root, treeData.id, calcSize); const subtree = readTree(subtreePath).root;
treeData.lastModified = fs.statSync(subtreePath).mtimeMs; id = refreshNodeData(subtree, --id);
treeData.path = node.path; node.name = subtree.name;
treeData.debug = node.debug; node.desc = subtree.desc;
treeData.disabled = node.disabled; node.args = subtree.args;
treeData.parent = parent; node.input = subtree.input;
treeData.id = node.id.toFixed(); node.output = subtree.output;
if (calcSize) { node.children = subtree.children;
treeData.size = calcSize(treeData); node.mtime = fs.statSync(subtreePath).mtimeMs;
} node.size = calcSize(node);
} catch (e) { } catch (e) {
alertError(`解析子树失败:${node.path}`); alertError(`解析子树失败:${node.path}`);
console.log("parse subtree:", e); console.log("parse subtree:", e);
} }
parsingStack.pop(); parsingStack.pop();
} else if (node.children?.length) { } else if (node.children?.length) {
treeData.children = []; for (let i = 0; i < node.children.length; i++) {
node.children.forEach((child) => { id = refreshNodeData(node.children[i], id);
treeData.children!.push(createTreeData(child, treeData.id, calcSize)); }
});
} }
return treeData; node.status = toStatusFlag(node);
if (node.children) {
let childStatus = 0;
node.children.forEach((child) => {
if (child.status && !child.disabled) {
childStatus = appendStatusFlag(childStatus, child.status);
}
});
buildStatusFlag(node, childStatus);
}
return id;
}; };
export const createBuildData = (path: string) => { export const createBuildData = (path: string) => {
try { try {
const treeModel: TreeModel = readTree(path); const treeModel: TreeData = readTree(path);
const data = createTreeData(treeModel.root); refreshNodeData(treeModel.root, 1);
refreshTreeDataId(data, treeModel.firstid ?? 1); dfs(treeModel.root, (node) => (node.id = treeModel.prefix + node.id));
treeModel.name = Path.basenameWithoutExt(path); treeModel.name = Path.basenameWithoutExt(path);
treeModel.root = createFileData(data, true); treeModel.root = createFileData(treeModel.root, true);
return treeModel as TreeModel; return treeModel as TreeData;
} catch (e) { } catch (e) {
console.log("build error:", path, e); console.log("build error:", path, e);
} }
return null; return null;
}; };
export const processBatch = (tree: TreeModel | null, path: string, batch: BatchScript) => { export const processBatch = (tree: TreeData | null, path: string, batch: BatchScript) => {
if (!tree) { if (!tree) {
return null; return null;
} }
@@ -791,9 +772,9 @@ export const processBatch = (tree: TreeModel | null, path: string, batch: BatchS
return null; return null;
} }
if (batch.onProcessNode) { if (batch.onProcessNode) {
const processNode = (node: NodeModel) => { const processNode = (node: NodeData) => {
if (node.children) { if (node.children) {
const children: NodeModel[] = []; const children: NodeData[] = [];
node.children?.forEach((child) => { node.children?.forEach((child) => {
const newChild = processNode(child); const newChild = processNode(child);
if (newChild) { if (newChild) {
@@ -804,7 +785,7 @@ export const processBatch = (tree: TreeModel | null, path: string, batch: BatchS
} }
return batch.onProcessNode?.(node); return batch.onProcessNode?.(node);
}; };
tree.root = processNode(tree.root) ?? ({} as NodeModel); tree.root = processNode(tree.root) ?? ({} as NodeData);
} }
return tree; return tree;
}; };
@@ -850,10 +831,10 @@ export const buildProject = async (project: string, buildDir: string) => {
console.log("build:", buildpath); console.log("build:", buildpath);
const declare: FileVarDecl = { const declare: FileVarDecl = {
import: tree.import.map((v) => ({ path: v, vars: [], depends: [] })), import: tree.import.map((v) => ({ path: v, vars: [], depends: [] })),
declvar: tree.declvar.map((v) => ({ name: v.name, desc: v.desc })), vars: tree.vars.map((v) => ({ name: v.name, desc: v.desc })),
subtree: [], subtree: [],
}; };
refreshDeclare(tree.root, tree.group, declare); refreshVarDecl(tree.root, tree.group, declare);
if (!checkNodeData(tree?.root)) { if (!checkNodeData(tree?.root)) {
hasError = true; hasError = true;
} }
@@ -871,16 +852,16 @@ export const loadModule = async (path: string) => {
if (typeof require !== "undefined" && require.cache) { if (typeof require !== "undefined" && require.cache) {
delete require.cache[require.resolve(path)]; delete require.cache[require.resolve(path)];
} }
return await import(`${path}?t=${Date.now()}`); return await import(/* @vite-ignore */ `${path}?t=${Date.now()}`);
} catch (e) { } catch (e) {
console.error(`failed to load module: ${path}`, e); console.error(`failed to load module: ${path}`, e);
return null; return null;
} }
}; };
export const createFileData = (data: TreeGraphData, includeSubtree?: boolean) => { export const createFileData = (data: NodeData, includeSubtree?: boolean) => {
const nodeData: NodeModel = { const nodeData: NodeData = {
id: Number(data.id), id: data.id,
name: data.name, name: data.name,
desc: data.desc || undefined, desc: data.desc || undefined,
args: data.args || undefined, args: data.args || undefined,
@@ -911,15 +892,15 @@ export const createFileData = (data: TreeGraphData, includeSubtree?: boolean) =>
}; };
export const createNewTree = (path: string) => { export const createNewTree = (path: string) => {
const tree: TreeModel = { const tree: TreeData = {
version: VERSION, version: VERSION,
name: Path.basenameWithoutExt(path), name: Path.basenameWithoutExt(path),
firstid: 1, prefix: "",
group: [], group: [],
import: [], import: [],
declvar: [], vars: [],
root: { root: {
id: 1, id: "1",
name: "Sequence", name: "Sequence",
}, },
}; };
@@ -930,7 +911,7 @@ export const isTreeFile = (path: string) => {
return path.toLocaleLowerCase().endsWith(".json"); return path.toLocaleLowerCase().endsWith(".json");
}; };
const loadVarDef = (list: ImportDef[], arr: Array<VarDef>) => { const loadVarDecl = (list: ImportDecl[], arr: Array<VarDecl>) => {
for (const entry of list) { for (const entry of list) {
if (!files[entry.path]) { if (!files[entry.path]) {
console.warn(`file not found:${workdir}/${entry.path}`); console.warn(`file not found:${workdir}/${entry.path}`);
@@ -954,14 +935,14 @@ const loadVarDef = (list: ImportDef[], arr: Array<VarDef>) => {
entry.depends = []; entry.depends = [];
entry.modified = files[entry.path]; entry.modified = files[entry.path];
const vars: Set<VarDef> = new Set(); const vars: Set<VarDecl> = new Set();
const depends: Set<string> = new Set(); const depends: Set<string> = new Set();
const load = (path: string) => { const load = (path: string) => {
if (parsingStack.includes(path)) { if (parsingStack.includes(path)) {
return; return;
} }
const parsedEntry: ImportDef | undefined = parsedVarDefs[path]; const parsedEntry: ImportDecl | undefined = parsedVarDecl[path];
if (parsedEntry && files[path] === parsedEntry.modified) { if (parsedEntry && files[path] === parsedEntry.modified) {
parsedEntry.depends.forEach((v) => depends.add(v.path)); parsedEntry.depends.forEach((v) => depends.add(v.path));
parsedEntry.vars.forEach((v) => vars.add(v)); parsedEntry.vars.forEach((v) => vars.add(v));
@@ -970,8 +951,8 @@ const loadVarDef = (list: ImportDef[], arr: Array<VarDef>) => {
parsingStack.push(path); parsingStack.push(path);
try { try {
const model: TreeModel = readTree(`${workdir}/${path}`); const model: TreeData = readTree(`${workdir}/${path}`);
model.declvar.forEach((v) => vars.add(v)); model.vars.forEach((v) => vars.add(v));
model.import.forEach((v) => { model.import.forEach((v) => {
load(v); load(v);
depends.add(v); depends.add(v);
@@ -985,7 +966,7 @@ const loadVarDef = (list: ImportDef[], arr: Array<VarDef>) => {
load(entry.path); load(entry.path);
entry.vars = Array.from(vars).sort((a, b) => a.name.localeCompare(b.name)); entry.vars = Array.from(vars).sort((a, b) => a.name.localeCompare(b.name));
entry.depends = Array.from(depends).map((v) => ({ path: v, modified: files[v] })); entry.depends = Array.from(depends).map((v) => ({ path: v, modified: files[v] }));
parsedVarDefs[entry.path] = { parsedVarDecl[entry.path] = {
path: entry.path, path: entry.path,
vars: entry.vars.map((v) => ({ name: v.name, desc: v.desc })), vars: entry.vars.map((v) => ({ name: v.name, desc: v.desc })),
depends: entry.depends.slice(), depends: entry.depends.slice(),
@@ -995,24 +976,20 @@ const loadVarDef = (list: ImportDef[], arr: Array<VarDef>) => {
list.forEach((entry) => arr.push(...entry.vars)); list.forEach((entry) => arr.push(...entry.vars));
}; };
const collectSubtree = (data: TreeGraphData | NodeModel, list: string[] = []) => { const collectSubtree = (data: NodeData) => {
if (data.path) { const list: string[] = [];
list.push(data.path); dfs(data, (node) => {
} if (node.path) {
if (data.children) { list.push(node.path);
data.children.forEach((child) => collectSubtree(child, list));
} }
});
return list; return list;
}; };
export const refreshDeclare = ( export const refreshVarDecl = (root: NodeData, group: string[], declare: FileVarDecl) => {
root: TreeGraphData | NodeModel,
group: string[],
declare: FileVarDecl
) => {
const filter: Record<string, boolean> = {}; const filter: Record<string, boolean> = {};
const vars: Array<VarDef> = new (class extends Array { const vars: Array<VarDecl> = new (class extends Array {
push(...items: VarDef[]): number { push(...items: VarDecl[]): number {
for (const v of items) { for (const v of items) {
if (filter[v.name]) { if (filter[v.name]) {
continue; continue;
@@ -1023,18 +1000,18 @@ export const refreshDeclare = (
return this.length; return this.length;
} }
})(); })();
vars.push(...declare.declvar); vars.push(...declare.vars);
parsingStack.length = 0; parsingStack.length = 0;
declare.subtree = collectSubtree(root).map((v) => ({ declare.subtree = collectSubtree(root).map((v) => ({
path: v, path: v,
vars: [], vars: [],
depends: [], depends: [],
})); }));
loadVarDef(declare.import, vars); loadVarDecl(declare.import, vars);
loadVarDef(declare.subtree, vars); loadVarDecl(declare.subtree, vars);
let changed = false; let changed = false;
const lastGroup = Array.from(Object.keys(usingGroups)).sort(); const lastGroup = Array.from(Object.keys(usingGroups ?? {})).sort();
group.sort(); group.sort();
if (lastGroup.length !== group.length || lastGroup.some((v, i) => v !== group[i])) { if (lastGroup.length !== group.length || lastGroup.some((v, i) => v !== group[i])) {
changed = true; changed = true;
+20 -14
View File
@@ -1,12 +1,19 @@
import * as fs from "fs"; import * as fs from "fs";
import { type WorkspaceModel } from "../contexts/workspace-context"; import { type WorkspaceModel } from "../contexts/workspace-context";
import { VERSION, type TreeModel } from "./b3type"; import { VarDecl, VERSION, type TreeData } from "./b3type";
import { dfs } from "./b3util";
import Path from "./path";
export const readJson = <T>(path: string): T => { export const readJson = <T>(path: string): T => {
const str = fs.readFileSync(path, "utf-8"); const str = fs.readFileSync(path, "utf-8");
return JSON.parse(str); return JSON.parse(str);
}; };
export const writeJson = <T>(path: string, data: T) => {
const str = JSON.stringify(data, undefined, 2);
fs.writeFileSync(path, str, "utf-8");
};
export const readWorkspace = (path: string) => { export const readWorkspace = (path: string) => {
const data = readJson(path) as WorkspaceModel; const data = readJson(path) as WorkspaceModel;
data.settings = data.settings ?? {}; data.settings = data.settings ?? {};
@@ -14,35 +21,34 @@ export const readWorkspace = (path: string) => {
}; };
export const readTree = (path: string) => { export const readTree = (path: string) => {
const data = readJson(path) as TreeModel; const data = readJson(path) as TreeData & { declvar?: VarDecl[] };
data.version = data.version ?? VERSION; data.version = data.version ?? VERSION;
data.firstid = data.firstid ?? 1; data.prefix = data.prefix ?? "";
data.group = data.group || []; data.group = data.group || [];
data.import = data.import || []; data.import = data.import || [];
data.declvar = data.declvar || []; data.vars = data.vars || data.declvar || [];
data.root = data.root || {}; data.root = data.root || {};
// compatible with old version
dfs(data.root, (node) => (node.id = node.id.toString()));
return data; return data;
}; };
export const writeTree = (path: string, data: TreeModel) => { export const writeTree = (path: string, data: TreeData) => {
writeJson(path, { writeJson<TreeData>(path, {
version: VERSION, version: VERSION,
name: data.name, name: Path.basenameWithoutExt(path),
desc: data.desc, desc: data.desc,
firstid: data.firstid, prefix: data.prefix,
export: data.export, export: data.export,
group: data.group, group: data.group,
import: data.import, import: data.import,
declvar: data.declvar, vars: data.vars,
root: data.root, root: data.root,
}); });
}; };
export const writeJson = (path: string, data: unknown) => {
const str = JSON.stringify(data, undefined, 2);
fs.writeFileSync(path, str, "utf-8");
};
export function mergeClassNames(...cls: (string | boolean)[]): string { export function mergeClassNames(...cls: (string | boolean)[]): string {
return cls.filter((v) => !!v).join(" "); return cls.filter((v) => !!v).join(" ");
} }