implement expression validation and build script setup in workspace

This commit is contained in:
codetypes committed 2025-03-23 12:30:32 +08:00
1 parent d595ce4a3a
commit 2483606ef4
24 files changed
+572 -317

No files matched your search

+32 -14
View File
@@ -5,9 +5,11 @@ import os from "node:os";
import path from "node:path"; import path from "node:path";
import { argv } from "node:process"; import { argv } from "node:process";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
import { type WorkspaceModel } from "../../src/contexts/workspace-context";
import { VERSION, type FileVarDecl } from "../../src/misc/b3type"; import { VERSION, type FileVarDecl } from "../../src/misc/b3type";
import * as b3util from "../../src/misc/b3util"; import * as b3util from "../../src/misc/b3util";
import Path from "../../src/misc/path"; import Path from "../../src/misc/path";
import { readJson } from "../../src/misc/util";
const require = createRequire(import.meta.url); const require = createRequire(import.meta.url);
const __dirname = path.dirname(fileURLToPath(import.meta.url)); const __dirname = path.dirname(fileURLToPath(import.meta.url));
@@ -19,13 +21,13 @@ let buildHelp: boolean = false;
for (let i = 0; i < argv.length; i++) { for (let i = 0; i < argv.length; i++) {
const arg = argv[i]; const arg = argv[i];
if (arg === "--project") { if (arg === "-p") {
buildProject = argv[i + 1]; buildProject = argv[i + 1];
i++; i++;
} else if (arg === "--build") { } else if (arg === "-o") {
buildOutput = argv[i + 1]; buildOutput = argv[i + 1];
i++; i++;
} else if (arg === "--help") { } else if (arg === "-h" || arg === "-v") {
buildHelp = true; buildHelp = true;
} }
} }
@@ -33,9 +35,9 @@ for (let i = 0; i < argv.length; i++) {
const printHelp = () => { const printHelp = () => {
console.log(`Usage: Behavior3 Editor ${VERSION} [options]`); console.log(`Usage: Behavior3 Editor ${VERSION} [options]`);
console.log("Options:"); console.log("Options:");
console.log(" --project <path> Set the project path"); console.log(" -p <path> Set the project path");
console.log(" --build <path> Set the build output path"); console.log(" -o <path> Set the build output path");
console.log(" --help Print this help"); console.log(" -h -v Print this help");
}; };
if (buildOutput || buildProject || buildHelp) { if (buildOutput || buildProject || buildHelp) {
@@ -61,29 +63,45 @@ if (buildOutput || buildProject || buildHelp) {
b3util.initWorkdir(workdir, (msg) => { b3util.initWorkdir(workdir, (msg) => {
console.error(`${msg}`); console.error(`${msg}`);
}); });
const settings = readJson<WorkspaceModel>(project).settings;
let buildScript: b3util.BuildScript | undefined;
if (settings.checkExpr) {
b3util.setCheckExpr(true);
}
if (settings.buildScript) {
const scriptPath = workdir + "/" + settings.buildScript;
try {
buildScript = eval(fs.readFileSync(scriptPath, "utf8"));
} catch (error) {
console.error(`'${scriptPath}' is not a valid build script`);
}
}
for (const path of Path.ls(Path.dirname(project), true)) { for (const path of Path.ls(Path.dirname(project), true)) {
if (path.endsWith(".json")) { if (path.endsWith(".json")) {
const buildpath = buildDir + "/" + path.substring(workdir.length + 1); const buildpath = buildDir + "/" + path.substring(workdir.length + 1);
const treeModel = b3util.createBuildData(path); let tree = b3util.createBuildData(path);
if (!treeModel) { if (buildScript) {
tree = b3util.processBatch(tree, path, buildScript);
}
if (!tree) {
continue; continue;
} }
if (treeModel.export === false) { if (tree.export === false) {
console.log("skip:", buildpath); console.log("skip:", buildpath);
continue; continue;
} }
console.log("build:", buildpath); console.log("build:", buildpath);
const declare: FileVarDecl = { const declare: FileVarDecl = {
import: treeModel.import.map((v) => ({ path: v, vars: [], depends: [] })), import: tree.import.map((v) => ({ path: v, vars: [], depends: [] })),
declvar: treeModel.declvar.map((v) => ({ name: v.name, desc: v.desc })), declvar: tree.declvar.map((v) => ({ name: v.name, desc: v.desc })),
subtree: [], subtree: [],
}; };
b3util.refreshDeclare(treeModel.root, treeModel.group, declare); b3util.refreshDeclare(tree.root, tree.group, declare);
if (!b3util.checkNodeData(treeModel?.root)) { if (!b3util.checkNodeData(tree?.root)) {
hasError = true; hasError = true;
} }
fs.mkdirSync(Path.dirname(buildpath), { recursive: true }); fs.mkdirSync(Path.dirname(buildpath), { recursive: true });
fs.writeFileSync(buildpath, JSON.stringify(treeModel, null, 2)); fs.writeFileSync(buildpath, JSON.stringify(tree, null, 2));
} }
} }
if (hasError) { if (hasError) {
+1 -1
View File
@@ -1 +1 @@
<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 512 512" height="200px" width="200px" xmlns="http://www.w3.org/2000/svg"><path d="M256 8C119.043 8 8 119.083 8 256c0 136.997 111.043 248 248 248s248-111.003 248-248C504 119.083 392.957 8 256 8zm0 448c-110.532 0-200-89.431-200-200 0-110.495 89.472-200 200-200 110.491 0 200 89.471 200 200 0 110.53-89.431 200-200 200zm107.244-255.2c0 67.052-72.421 68.084-72.421 92.863V300c0 6.627-5.373 12-12 12h-45.647c-6.627 0-12-5.373-12-12v-8.659c0-35.745 27.1-50.034 47.579-61.516 17.561-9.845 28.324-16.541 28.324-29.579 0-17.246-21.999-28.693-39.784-28.693-23.189 0-33.894 10.977-48.942 29.969-4.057 5.12-11.46 6.071-16.666 2.124l-27.824-21.098c-5.107-3.872-6.251-11.066-2.644-16.363C184.846 131.491 214.94 112 261.794 112c49.071 0 101.45 38.304 101.45 88.8zM298 368c0 23.159-18.841 42-42 42s-42-18.841-42-42 18.841-42 42-42 42 18.841 42 42z"></path></svg> <svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 24 24" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path fill="none" d="M0 0h24v24H0z"></path><path d="M11.07 12.85c.77-1.39 2.25-2.21 3.11-3.44.91-1.29.4-3.7-2.18-3.7-1.69 0-2.52 1.28-2.87 2.34L6.54 6.96C7.25 4.83 9.18 3 11.99 3c2.35 0 3.96 1.07 4.78 2.41.7 1.15 1.11 3.3.03 4.9-1.2 1.77-2.35 2.31-2.97 3.45-.25.46-.35.76-.35 2.24h-2.89c-.01-.78-.13-2.05.48-3.15zM14 20c0 1.1-.9 2-2 2s-2-.9-2-2 .9-2 2-2 2 .9 2 2z"></path></svg>

Before

Width:  |  Height:  |  Size: 923 B

After

Width:  |  Height:  |  Size: 522 B

+1 -1
View File
@@ -1 +1 @@
<svg stroke="currentColor" stroke-width="0" viewBox="0 0 16 16" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg" style="width:12px;height:12px" color="#fff"><path d="M2.273 9.53a2.273 2.273 0 100-4.546 2.273 2.273 0 000 4.547zm9.467-4.984a2.273 2.273 0 100-4.546 2.273 2.273 0 000 4.546M7.4 13.108a5.54 5.54 0 01-3.775-2.88 3.27 3.27 0 01-1.944.24 7.4 7.4 0 005.328 4.465c.53.113 1.072.169 1.614.166a3.25 3.25 0 01-.666-1.9 6 6 0 01-.557-.091m3.828 2.285a2.273 2.273 0 100-4.546 2.273 2.273 0 000 4.546m3.163-3.108a7.44 7.44 0 00.373-8.726 3.3 3.3 0 01-1.278 1.498 5.57 5.57 0 01-.183 5.535 3.26 3.26 0 011.088 1.693M2.098 3.998a3.3 3.3 0 011.897.486 5.54 5.54 0 014.464-2.388c.037-.67.277-1.313.69-1.843a7.47 7.47 0 00-7.051 3.745" stroke="none"/></svg> <svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 512 512" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="48" d="M200.66 352H144a96 96 0 0 1 0-192h55.41m113.18 0H368a96 96 0 0 1 0 192h-56.66m-142.27-96h175.86"></path></svg>

Before

Width:  |  Height:  |  Size: 765 B

After

Width:  |  Height:  |  Size: 342 B

+1 -10
View File
@@ -1,10 +1 @@
<?xml version="1.0" ?> <svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 16 16" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path d="M7.938 2.016A.13.13 0 0 1 8.002 2a.13.13 0 0 1 .063.016.15.15 0 0 1 .054.057l6.857 11.667c.036.06.035.124.002.183a.2.2 0 0 1-.054.06.1.1 0 0 1-.066.017H1.146a.1.1 0 0 1-.066-.017.2.2 0 0 1-.054-.06.18.18 0 0 1 .002-.183L7.884 2.073a.15.15 0 0 1 .054-.057m1.044-.45a1.13 1.13 0 0 0-1.96 0L.165 13.233c-.457.778.091 1.767.98 1.767h13.713c.889 0 1.438-.99.98-1.767z"></path><path d="M7.002 12a1 1 0 1 1 2 0 1 1 0 0 1-2 0M7.1 5.995a.905.905 0 1 1 1.8 0l-.35 3.507a.552.552 0 0 1-1.1 0z"></path></svg>
<svg height="512" viewBox="0 0 512 512" width="512" xmlns="http://www.w3.org/2000/svg">
<title />
<path
d="M85.57,446.25H426.43a32,32,0,0,0,28.17-47.17L284.18,82.58c-12.09-22.44-44.27-22.44-56.36,0L57.4,399.08A32,32,0,0,0,85.57,446.25Z"
style="fill:none;stroke:#000;stroke-linecap:round;stroke-linejoin:round;stroke-width:32px" />
<path d="M250.26,195.39l5.74,122,5.73-121.95a5.74,5.74,0,0,0-5.79-6h0A5.74,5.74,0,0,0,250.26,195.39Z"
style="fill:none;stroke:#000;stroke-linecap:round;stroke-linejoin:round;stroke-width:32px" />
<path d="M256,397.25a20,20,0,1,1,20-20A20,20,0,0,1,256,397.25Z" />
</svg>

Before

Width:  |  Height:  |  Size: 663 B

After

Width:  |  Height:  |  Size: 649 B

+4 -1
View File
@@ -14,6 +14,7 @@
"console": "Console", "console": "Console",
"copy": "Copy", "copy": "Copy",
"createProject": "Create Project", "createProject": "Create Project",
"checkExpr": "Check expression",
"cut": "Cut", "cut": "Cut",
"delete": "Delete", "delete": "Delete",
"deleteNode": "Delete Node", "deleteNode": "Delete Node",
@@ -56,6 +57,7 @@
"searchNode": "Search Node", "searchNode": "Search Node",
"selectAll": "Select All", "selectAll": "Select All",
"services": "Services", "services": "Services",
"setupBuildScript": "Setup Build Script",
"start": "Start", "start": "Start",
"undo": "Undo", "undo": "Undo",
"unhide": "Show All", "unhide": "Show All",
@@ -106,10 +108,11 @@
"node.disabled": "Disabled", "node.disabled": "Disabled",
"node.editSubtreeDenied": "Unable to edit the subtree node", "node.editSubtreeDenied": "Unable to edit the subtree node",
"node.group": "Node group", "node.group": "Node group",
"node.groupNotEnabled": "One of '{{group}}' node group is not enabled in current file",
"node.id": "Node id", "node.id": "Node id",
"node.inputVariable": "Input variables", "node.inputVariable": "Input variables",
"node.invalidChildren": "Invalid number of children", "node.invalidChildren": "Invalid number of children",
"node.invalidGroup": "Node group '{{group}}' is not enabled", "node.invalidExpression": "Invalid expression",
"node.invalidValue": "Invalid value", "node.invalidValue": "Invalid value",
"node.invalidVariableName": "Invalid variable name, should start with a letter or underscore", "node.invalidVariableName": "Invalid variable name, should start with a letter or underscore",
"node.name": "Node name", "node.name": "Node name",
+5 -3
View File
@@ -12,6 +12,7 @@
"cancel": "取消", "cancel": "取消",
"compact": "紧凑", "compact": "紧凑",
"console": "控制台", "console": "控制台",
"checkExpr": "检查表达式",
"copy": "复制", "copy": "复制",
"createProject": "新建项目", "createProject": "新建项目",
"cut": "剪切", "cut": "剪切",
@@ -56,6 +57,7 @@
"searchNode": "搜索节点", "searchNode": "搜索节点",
"selectAll": "全选", "selectAll": "全选",
"services": "服务", "services": "服务",
"setupBuildScript": "设置构建脚本",
"start": "开始", "start": "开始",
"undo": "撤销", "undo": "撤销",
"unhide": "显示所有", "unhide": "显示所有",
@@ -106,10 +108,11 @@
"node.disabled": "禁用", "node.disabled": "禁用",
"node.editSubtreeDenied": "不能编辑子树节点", "node.editSubtreeDenied": "不能编辑子树节点",
"node.group": "节点分组", "node.group": "节点分组",
"node.groupNotEnabled": "节点分组 “{{group}}” 任意一个未在当前文件中启用",
"node.id": "节点标识", "node.id": "节点标识",
"node.invalidChildren": "子节点数量不合法",
"node.inputVariable": "输入变量", "node.inputVariable": "输入变量",
"node.invalidGroup": "节点分组 “{{group}}” 未启用", "node.invalidChildren": "子节点数量不合法",
"node.invalidExpression": "表达式不合法",
"node.invalidValue": "无效的值", "node.invalidValue": "无效的值",
"node.invalidVariableName": "变量名必须以字母或下划线开头", "node.invalidVariableName": "变量名必须以字母或下划线开头",
"node.name": "节点名称", "node.name": "节点名称",
@@ -124,6 +127,5 @@
"node.subtreePathError": "子树必须保存在当前工作区的目录内", "node.subtreePathError": "子树必须保存在当前工作区的目录内",
"node.type": "节点类型", "node.type": "节点类型",
"node.undefinedVariable": "变量 “{{variable}}” 未定义", "node.undefinedVariable": "变量 “{{variable}}” 未定义",
"node.unknown.desc": "新建节点" "node.unknown.desc": "新建节点"
} }
+48
View File
@@ -0,0 +1,48 @@
/* 批处理脚本 */
/*
如果有processTree,则先运行prcessTree,如果有返回,则进行后面的针对每个节点运行processNode
如果没有定义processTree,则直接针对每个节点运行processNode
export interface TreeModel {
version: string;
name: string;
desc?: string;
export?: boolean;
firstid: number;
group: string[];
import: string[];
declvar: VarDef[];
root: NodeModel;
}
export interface NodeModel {
id: number;
name: string;
desc?: string;
args?: { [key: string]: any };
input?: string[];
output?: string[];
children?: NodeModel[];
debug?: boolean;
disabled?: boolean;
path?: string;
}
interface BuildScript {
processTree?(tree: TreeModel, path: string): TreeModel | null;
processNode?(node: NodeModel, tree: TreeModel): NodeModel | null;
writeFile?(path: string, tree: TreeModel): void;
}
*/
({
processTree: (tree) => {
console.log(`processTree ${tree.name}`);
return tree;
},
processNode: (node, tree) => {
console.log(`processNode ${tree.name} ${node.name}#${node.id}`);
return node;
},
});
+19 -14
View File
@@ -4,23 +4,28 @@
如果没有定义processTree,则直接针对每个节点运行processNode 如果没有定义processTree,则直接针对每个节点运行processNode
export interface TreeModel { export interface TreeModel {
name: string; version: string;
desc?: string; name: string;
export?: boolean; desc?: string;
root: NodeModel; export?: boolean;
firstid: number;
group: string[];
import: string[];
declvar: VarDef[];
root: NodeModel;
} }
export interface NodeModel { export interface NodeModel {
id: number; id: number;
name: string; name: string;
desc?: string; desc?: string;
args?: { [key: string]: any }; args?: { [key: string]: any };
input?: string[]; input?: string[];
output?: string[]; output?: string[];
children?: NodeModel[]; children?: NodeModel[];
debug?: boolean; debug?: boolean;
disabled?: boolean; disabled?: boolean;
path?: string; path?: string;
} }
interface BatchScript { interface BatchScript {
+1 -1
View File
@@ -50,7 +50,7 @@
"id": 6, "id": 6,
"name": "Check", "name": "Check",
"args": { "args": {
"value": "target1.x > 0" "value": "target[0].x > 0 |"
} }
}, },
{ {
+114 -49
View File
@@ -1,55 +1,120 @@
{ {
"name": "hero", "name": "hero",
"root": { "root": {
"id": 1, "id": 1,
"name": "Selector", "name": "Selector",
"desc": "英雄测试AI", "desc": "英雄测试AI",
"args": {},
"children": [
{
"id": 2,
"name": "Sequence",
"desc": "攻击",
"args": {}, "args": {},
"children": [ "children": [
{ {
"id": 2, "id": 3,
"name": "Sequence", "name": "FindEnemy",
"desc": "攻击", "args": {
"args": {}, "x": 0,
"children": [ "y": 0,
{ "w": 100,
"id": 3, "h": 50
"name": "FindEnemy",
"args": { "x": 0, "y": 0, "w": 100, "h": 50 },
"output": ["enemy"]
},
{ "id": 4, "name": "Attack", "args": {}, "input": ["enemy"] },
{ "id": 5, "name": "Wait", "args": { "time": 10 } }
]
}, },
{ "output": [
"id": 6, "enemy"
"name": "Sequence", ]
"desc": "移动", },
"args": {}, {
"children": [ "id": 4,
{ "name": "Attack",
"id": 7, "args": {},
"name": "FindEnemy", "input": [
"args": { "w": 1000, "h": 500, "x": 0, "y": 0 }, "enemy"
"output": ["enemy"] ]
}, },
{ "id": 8, "name": "MoveToTarget", "args": {}, "input": ["enemy"] } {
] "id": 5,
}, "name": "Wait",
{ "args": {
"id": 9, "time": 10
"name": "Sequence", }
"desc": "逃跑", }
"args": {},
"children": [
{ "id": 10, "name": "GetHp", "args": {}, "output": ["hp"] },
{ "id": 11, "name": "Cmp", "args": { "lt": 50 }, "input": ["hp"] },
{ "id": 12, "name": "MoveToPos", "args": { "x": 0, "y": 0 } }
]
},
{ "id": 13, "name": "Idle" }
] ]
}, },
"desc": "英雄测试AI" {
"id": 6,
"name": "Sequence",
"desc": "移动",
"args": {},
"children": [
{
"id": 7,
"name": "FindEnemy",
"args": {
"w": 1000,
"h": 500,
"x": 0,
"y": 0
},
"output": [
"enemy"
]
},
{
"id": 8,
"name": "MoveToTarget",
"args": {},
"input": [
"enemy"
]
}
]
},
{
"id": 9,
"name": "Sequence",
"desc": "逃跑",
"args": {},
"children": [
{
"id": 10,
"name": "GetHp",
"args": {},
"output": [
"hp"
]
},
{
"id": 11,
"name": "Cmp",
"args": {
"lt": 50
},
"input": [
"hp"
]
},
{
"id": 12,
"name": "MoveToPos",
"args": {
"x": 0,
"y": 0
}
}
]
},
{
"id": 13,
"name": "Idle"
}
]
},
"desc": "英雄测试AI",
"version": "1.8.0",
"firstid": 1,
"group": [],
"import": [],
"declvar": []
} }
+9 -10
View File
@@ -1,5 +1,11 @@
{ {
"version": "1.8.0",
"name": "subtree1", "name": "subtree1",
"desc": "子树1",
"firstid": 1,
"group": [],
"import": [],
"declvar": [],
"root": { "root": {
"id": 1, "id": 1,
"name": "Sequence", "name": "Sequence",
@@ -9,7 +15,7 @@
"id": 2, "id": 2,
"name": "Log", "name": "Log",
"args": { "args": {
"str": "hello" "message": "d"
} }
}, },
{ {
@@ -17,15 +23,8 @@
"name": "Wait", "name": "Wait",
"args": { "args": {
"time": 1 "time": 1
}, }
"children": [
{
"id": 4,
"name": "unknow"
}
]
} }
] ]
}, }
"desc": "子树1"
} }
+8 -4
View File
@@ -1,5 +1,11 @@
{ {
"version": "1.8.0",
"name": "subtree2", "name": "subtree2",
"desc": "子树2",
"firstid": 1,
"group": [],
"import": [],
"declvar": [],
"root": { "root": {
"id": 1, "id": 1,
"name": "Sequence", "name": "Sequence",
@@ -8,9 +14,8 @@
{ {
"id": 2, "id": 2,
"name": "Cmp", "name": "Cmp",
"args": {},
"input": [ "input": [
"" "d"
] ]
}, },
{ {
@@ -21,6 +26,5 @@
] ]
} }
] ]
}, }
"desc": "子树2"
} }
+8 -7
View File
@@ -1,5 +1,11 @@
{ {
"version": "1.8.0",
"name": "subtree2", "name": "subtree2",
"desc": "子树2",
"firstid": 1,
"group": [],
"import": [],
"declvar": [],
"root": { "root": {
"id": 1, "id": 1,
"name": "Sequence", "name": "Sequence",
@@ -10,14 +16,9 @@
"name": "Cmp", "name": "Cmp",
"args": {}, "args": {},
"input": [ "input": [
"" "x"
] ]
},
{
"id": 3,
"name": "unknow"
} }
] ]
}, }
"desc": "子树2"
} }
+5 -1
View File
@@ -40,5 +40,9 @@
"path": "workdir/subtree2.json", "path": "workdir/subtree2.json",
"desc": "子树2" "desc": "子树2"
} }
] ],
"settings": {
"checkExpr": true,
"buildScript": "scripts/build.js"
}
} }
+6 -16
View File
@@ -1253,32 +1253,22 @@ export const Editor: FC<EditorProps> = ({ onUpdate: updateState, data: editor, .
editor.size.height = editorSize.height; editor.size.height = editorSize.height;
} }
if (typeof editor.editNode === "number") { if (editor.selectedId) {
const id = editor.editNode.toString(); const id = editor.selectedId;
selectNode(null);
setTimeout(() => { setTimeout(() => {
editor.graph.focusItem(id); editor.graph.focusItem(id);
selectNode(id); selectNode(id);
}, 100); }, 50);
} }
}); }, [editorSize, workspace.editing]);
// check should subtree // check should repaint node
useEffect(() => { useEffect(() => {
if (workspace.editing === editor) { if (workspace.editing === editor) {
checkSubtree(); checkSubtree();
editor.graph && refresh(); editor.graph && refresh();
} }
}, [workspace.editing]); }, [workspace.editing, t]);
// check should repaint node
useEffect(() => {
if (editor.graph) {
editor.graph.changeData(editor.root);
editor.graph.layout();
restoreViewport();
}
}, [t]);
const nextResult = () => { const nextResult = () => {
const { results, index } = filterOption; const { results, index } = filterOption;
-12
View File
@@ -876,18 +876,6 @@ export const Explorer: FC = () => {
if (node && !node.editing) { if (node && !node.editing) {
dispatch("open", node); dispatch("open", node);
setSelectedKeys([node.path]); setSelectedKeys([node.path]);
setTimeout(() => {
const element = document.querySelector(`[title="${node.title}"]`);
const container =
element?.closest(".b3-overflow") || element?.closest(".b3-explorer");
if (element && container && !isElementInViewport(element, container)) {
element.scrollIntoView({
behavior: "smooth",
block: "center",
});
}
}, 100);
} }
}} }}
onDrop={(info) => { onDrop={(info) => {
+75 -79
View File
@@ -26,6 +26,7 @@ import { useTranslation } from "react-i18next";
import Markdown from "react-markdown"; import Markdown from "react-markdown";
import { useDebounceCallback } from "usehooks-ts"; import { useDebounceCallback } from "usehooks-ts";
import { useShallow } from "zustand/react/shallow"; import { useShallow } from "zustand/react/shallow";
import { ExpressionEvaluator } from "../behavior3/src/behavior3";
import { EditNode, EditTree, useWorkspace } from "../contexts/workspace-context"; import { EditNode, EditTree, useWorkspace } from "../contexts/workspace-context";
import { import {
ImportDef, ImportDef,
@@ -36,6 +37,7 @@ import {
isIntType, isIntType,
isJsonType, isJsonType,
isStringType, isStringType,
NodeArg,
NodeModel, NodeModel,
TreeGraphData, TreeGraphData,
VarDef, VarDef,
@@ -53,6 +55,7 @@ import {
usingGroups, usingGroups,
usingVars, usingVars,
} from "../misc/b3util"; } from "../misc/b3util";
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";
@@ -550,6 +553,43 @@ const TreeInspector: FC = () => {
); );
}; };
const validateArg = (node: NodeModel, arg: NodeArg, value: unknown) => {
const type = getNodeArgRawType(arg);
const required = !isNodeArgOptional(arg);
if (isExprType(type) && value) {
for (const v of parseExpr(value as string)) {
if (usingVars && !usingVars[v]) {
return Promise.reject(new Error(i18n.t("node.undefinedVariable", { variable: v })));
}
}
if (useWorkspace.getState().settings.checkExpr) {
try {
if (!new ExpressionEvaluator(value as string).dryRun()) {
return Promise.reject(new Error(i18n.t("node.invalidExpression")));
}
} catch (e) {
console.error(e);
return Promise.reject(new Error(i18n.t("node.invalidExpression")));
}
}
}
if (value && isJsonType(type)) {
try {
if (value !== "null") {
JSON.parse(value as string);
}
} catch (e) {
return Promise.reject(new Error(i18n.t("node.invalidValue")));
}
} else if (value === null && !required) {
value = undefined;
}
if (!checkNodeArgValue(node, arg, value, true)) {
return Promise.reject(new Error(i18n.t("node.invalidValue")));
}
return Promise.resolve(value);
};
const NodeInspector: FC = () => { const NodeInspector: FC = () => {
const workspace = useWorkspace( const workspace = useWorkspace(
useShallow((state) => ({ useShallow((state) => ({
@@ -842,7 +882,9 @@ const NodeInspector: FC = () => {
{ {
validator() { validator() {
if (def.group && !def.group.some((g) => usingGroups[g])) { if (def.group && !def.group.some((g) => usingGroups[g])) {
return Promise.reject(new Error(t("node.invalidGroup", { group: def.group }))); return Promise.reject(
new Error(t("node.groupNotEnabled", { group: def.group }))
);
} }
return Promise.resolve(); return Promise.resolve();
}, },
@@ -1105,33 +1147,7 @@ const NodeInspector: FC = () => {
}, },
() => ({ () => ({
validator(_, value) { validator(_, value) {
if (usingVars && isExprType(type) && value) { return validateArg(editingNode.data, arg, value);
for (const v of parseExpr(value)) {
if (!usingVars![v]) {
return Promise.reject(
new Error(
t("node.undefinedVariable", { variable: v })
)
);
}
}
}
if (isJsonType(type)) {
try {
if (value !== "null") {
JSON.parse(value);
}
} catch (e) {
return Promise.reject(
new Error(t("node.invalidValue"))
);
}
} else if (
!checkNodeArgValue(editingNode.data, arg, value, true)
) {
return Promise.reject(new Error(t("node.invalidValue")));
}
return Promise.resolve();
}, },
}), }),
]} ]}
@@ -1228,59 +1244,39 @@ const NodeInspector: FC = () => {
rules={[ rules={[
{ required, message: t("fieldRequired", { field: arg.desc }) }, { required, message: t("fieldRequired", { field: arg.desc }) },
({ getFieldValue, setFieldValue, isFieldValidating, validateFields }) => ({ ({ getFieldValue, setFieldValue, isFieldValidating, validateFields }) => ({
validator(_, value) { async validator(_, value) {
if (usingVars && isExprType(type) && value) { return validateArg(editingNode.data, arg, value).then((result) => {
for (const v of parseExpr(value)) { value = result;
if (!usingVars![v]) { if (!arg.oneof) {
return Promise.reject( return Promise.resolve();
new Error(t("node.undefinedVariable", { variable: v }))
);
}
} }
} const idx = def.input?.findIndex(
if (value && isJsonType(type)) { (input) => input.replace("?", "") === arg.oneof
try { );
if (value !== "null") { if (idx === undefined || idx < 0) {
JSON.parse(value); return Promise.reject(
} new Error(t("node.oneof.inputNotfound", { input: arg.oneof }))
} catch (e) { );
return Promise.reject(new Error(t("node.invalidValue")));
} }
} else if (value === null && !required) { const inputName = `input.${idx}`;
value = undefined; if (!isFieldValidating(inputName)) {
} setFieldValue(`args.${arg.name}`, value);
if (!checkNodeArgValue(editingNode.data, arg, value)) { validateFields([inputName]);
return Promise.reject(new Error(t("node.invalidValue"))); }
} if (!checkOneof(getFieldValue(inputName) ?? "", value)) {
if (!arg.oneof) { return Promise.reject(
return Promise.resolve(); new Error(
} t("node.oneof.error", {
const idx = def.input?.findIndex( input: def.input![idx],
(input) => input.replace("?", "") === arg.oneof arg: arg.name,
); desc: arg.desc ?? "",
if (idx === undefined || idx < 0) { })
return Promise.reject( )
new Error(t("node.oneof.inputNotfound", { input: arg.oneof })) );
); } else {
} return Promise.resolve();
const inputName = `input.${idx}`; }
if (!isFieldValidating(inputName)) { });
setFieldValue(`args.${arg.name}`, value);
validateFields([inputName]);
}
if (!checkOneof(getFieldValue(inputName) ?? "", value)) {
return Promise.reject(
new Error(
t("node.oneof.error", {
input: def.input![idx],
arg: arg.name,
desc: arg.desc ?? "",
})
)
);
} else {
return Promise.resolve();
}
}, },
}), }),
]} ]}
+24 -2
View File
@@ -54,6 +54,9 @@ export const Menu: FC<LayoutProps> = () => {
save: state.save, save: state.save,
saveAll: state.saveAll, saveAll: state.saveAll,
workdir: state.workdir, workdir: state.workdir,
settings: state.settings,
setCheckExpr: state.setCheckExpr,
setupBuildScript: state.setupBuildScript,
})) }))
); );
const enabled = !!workspace.workdir; const enabled = !!workspace.workdir;
@@ -165,6 +168,14 @@ export const Menu: FC<LayoutProps> = () => {
workspace.buildProject(); workspace.buildProject();
}, },
}, },
{
id: "menu.file.buildScript",
label: t("setupBuildScript"),
enabled: enabled,
click: () => {
workspace.setupBuildScript();
},
},
{ {
id: "menu.file.batch", id: "menu.file.batch",
label: t("batch"), label: t("batch"),
@@ -255,6 +266,17 @@ export const Menu: FC<LayoutProps> = () => {
// workspace.editing?.dispatch("delete"); // workspace.editing?.dispatch("delete");
}, },
}, },
{ type: "separator" },
{
id: "menu.edit.checkExpr",
label: t("checkExpr"),
enabled: enabled,
type: "checkbox",
checked: workspace.settings.checkExpr,
click: () => {
workspace.setCheckExpr(!workspace.settings.checkExpr);
},
},
], ],
}, },
// { role: 'viewMenu' } // { role: 'viewMenu' }
@@ -394,7 +416,7 @@ export const Menu: FC<LayoutProps> = () => {
], ],
}, },
] as MenuItemConstructorOptions[]; ] as MenuItemConstructorOptions[];
}, [t, workspace.workdir, settings.recent, workspace.editing, settings]); }, [t, workspace.workdir, settings.recent, workspace.editing, workspace.settings, settings]);
if (isMacos) { if (isMacos) {
const menu = AppMenu.buildFromTemplate(menuTemplate); const menu = AppMenu.buildFromTemplate(menuTemplate);
@@ -446,7 +468,7 @@ export const Menu: FC<LayoutProps> = () => {
label: ( label: (
<MenuItemLabel> <MenuItemLabel>
<div> <div>
{option.type === "radio" && option.checked && ( {(option.type === "radio" || option.type === "checkbox") && option.checked && (
<CheckOutlined style={{ paddingRight: "5px" }} /> <CheckOutlined style={{ paddingRight: "5px" }} />
)} )}
{option.type === "radio" && !option.checked && ( {option.type === "radio" && !option.checked && (
+40 -15
View File
@@ -1,5 +1,6 @@
import G6 from "@antv/g6"; import G6 from "@antv/g6";
import { NodeDef } from "../behavior3/src/behavior3"; import { ExpressionEvaluator, NodeDef } from "../behavior3/src/behavior3";
import { useSetting } from "../contexts/setting-context";
import { useWorkspace } from "../contexts/workspace-context"; import { useWorkspace } from "../contexts/workspace-context";
import { TreeGraphData, getNodeType, isExprType } from "../misc/b3type"; import { TreeGraphData, getNodeType, isExprType } from "../misc/b3type";
import { checkTreeData, nodeDefs, parseExpr, usingGroups, usingVars } from "../misc/b3util"; import { checkTreeData, nodeDefs, parseExpr, usingGroups, usingVars } from "../misc/b3util";
@@ -11,9 +12,6 @@ let defaultFontSize = "";
const textWidthMap = new Map<string, number>(); const textWidthMap = new Map<string, number>();
const textLines: Record<string, string[]> = {}; const textLines: Record<string, string[]> = {};
let layoutWidth = 220;
let layoutStyle: "compact" | "normal" = "compact";
const calcTextWith = (text: string, fontSize?: string) => { const calcTextWith = (text: string, fontSize?: string) => {
fontSize = fontSize ?? defaultFontSize; fontSize = fontSize ?? defaultFontSize;
let b3Workspace: HTMLDivElement | null; let b3Workspace: HTMLDivElement | null;
@@ -133,7 +131,40 @@ const foundUndefinedInArgs = (def: NodeDef, data: TreeGraphData) => {
return false; return false;
}; };
const hasErrorInArgExpr = (def: NodeDef, data: TreeGraphData) => {
const checkExpr = useWorkspace.getState().settings.checkExpr;
if (!checkExpr || !def.args || !data.args) {
return false;
}
for (const arg of def.args) {
if (!isExprType(arg.type)) {
continue;
}
const expr = data.args[arg.name] as string | string[] | undefined;
if (!expr) {
continue;
}
try {
if (typeof expr === "string") {
if (!new ExpressionEvaluator(expr).dryRun()) {
return true;
}
} else if (Array.isArray(expr)) {
for (const str of expr) {
if (!new ExpressionEvaluator(str).dryRun()) {
return true;
}
}
}
} catch (e) {
return true;
}
}
return false;
};
export const calcTreeDataSize = (data: TreeGraphData) => { export const calcTreeDataSize = (data: TreeGraphData) => {
const width = useSetting.getState().data.layout === "compact" ? 220 : 260;
let height = 50 + 2; let height = 50 + 2;
const updateHeight = (obj: unknown) => { const updateHeight = (obj: unknown) => {
if ((Array.isArray(obj) && obj.length) || (obj && Object.keys(obj).length > 0)) { if ((Array.isArray(obj) && obj.length) || (obj && Object.keys(obj).length > 0)) {
@@ -147,16 +178,7 @@ export const calcTreeDataSize = (data: TreeGraphData) => {
updateHeight(data.args); updateHeight(data.args);
updateHeight(data.input); updateHeight(data.input);
updateHeight(data.output); updateHeight(data.output);
return [layoutWidth, height]; return [width, height];
};
export const setLayoutStyle = (style: "compact" | "normal") => {
layoutStyle = style;
if (style === "compact") {
layoutWidth = 220;
} else {
layoutWidth = 260;
}
}; };
const NODE_COLORS = { const NODE_COLORS = {
@@ -217,6 +239,8 @@ G6.registerNode(
let classify = getNodeType(nodeDef); let classify = getNodeType(nodeDef);
let color = nodeDef.color || NODE_COLORS[classify] || NODE_COLORS["Other"]; let color = nodeDef.color || NODE_COLORS[classify] || NODE_COLORS["Other"];
const layoutStyle = useSetting.getState().data.layout;
if ( if (
!nodeDefs.has(data.name) || !nodeDefs.has(data.name) ||
(data.path && !data.children?.length) || (data.path && !data.children?.length) ||
@@ -224,7 +248,8 @@ G6.registerNode(
!checkTreeData(data) || !checkTreeData(data) ||
foundUndefined(data.input) || foundUndefined(data.input) ||
foundUndefined(data.output) || foundUndefined(data.output) ||
foundUndefinedInArgs(nodeDef, data) foundUndefinedInArgs(nodeDef, data) ||
hasErrorInArgExpr(nodeDef, data)
) { ) {
classify = "Error"; classify = "Error";
color = NODE_COLORS[classify]; color = NODE_COLORS[classify];
+1 -3
View File
@@ -1,7 +1,6 @@
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 { setLayoutStyle } from "../components/register-node";
import { readJson, writeJson } from "../misc/util"; import { readJson, writeJson } from "../misc/util";
import { useWorkspace } from "./workspace-context"; import { useWorkspace } from "./workspace-context";
@@ -29,6 +28,7 @@ export const useSetting = create<SettingStore>((set, get) => ({
recent: [], recent: [],
buildDir: "", buildDir: "",
layout: "compact", layout: "compact",
checkExpr: true,
}, },
load: () => { load: () => {
try { try {
@@ -36,7 +36,6 @@ export const useSetting = create<SettingStore>((set, get) => ({
const settings = readJson(settingPath) as SettingModel; const settings = readJson(settingPath) as SettingModel;
settings.layout = settings.layout || "compact"; settings.layout = settings.layout || "compact";
set({ data: settings }); set({ data: settings });
setLayoutStyle(settings.layout);
} }
} catch (error) { } catch (error) {
console.error(error); console.error(error);
@@ -67,7 +66,6 @@ export const useSetting = create<SettingStore>((set, get) => ({
const { data, save } = get(); const { data, save } = get();
set({ data: { ...data, layout } }); set({ data: { ...data, layout } });
save(); save();
setLayoutStyle(layout);
useWorkspace.getState().editing?.dispatch("refresh"); useWorkspace.getState().editing?.dispatch("refresh");
}, },
})); }));
+87 -66
View File
@@ -18,17 +18,11 @@ import { message } from "../misc/hooks";
import i18n from "../misc/i18n"; import i18n from "../misc/i18n";
import Path from "../misc/path"; import Path from "../misc/path";
import { zhNodeDef } from "../misc/template"; import { zhNodeDef } from "../misc/template";
import { readJson, readTree, writeJson } from "../misc/util"; import { readJson, readTree, readWorkspace, writeJson } from "../misc/util";
import { useSetting } from "./setting-context"; import { useSetting } from "./setting-context";
let buildDir: string | undefined; let buildDir: string | undefined;
interface BatchScript {
processTree?(tree: TreeModel, path: string): TreeModel;
processNode?(node: NodeModel, tree: TreeModel): NodeModel;
}
export type EditEvent = export type EditEvent =
| "save" | "save"
| "copy" | "copy"
@@ -72,8 +66,6 @@ export class EditorStore {
graphMatrix?: Matrix; graphMatrix?: Matrix;
graph!: TreeGraph; graph!: TreeGraph;
editNode: EditNode | number | null = null;
dispatch!: (event: EditEvent, data?: unknown) => void; dispatch!: (event: EditEvent, data?: unknown) => void;
constructor(path: string) { constructor(path: string) {
@@ -142,8 +134,12 @@ export type FileMeta = {
exists?: boolean; exists?: boolean;
}; };
interface WorkspaceModel { export interface WorkspaceModel {
files?: { path: string; desc: string }[]; files?: { path: string; desc: string }[];
settings: {
checkExpr?: boolean;
buildScript?: string;
};
} }
export type WorkspaceStore = { export type WorkspaceStore = {
@@ -153,9 +149,14 @@ export type WorkspaceStore = {
batchProject: () => void; batchProject: () => void;
buildProject: () => void; buildProject: () => void;
settings: WorkspaceModel["settings"];
workdir: string; workdir: string;
path: string; path: string;
// settings
setCheckExpr: (checkExpr: boolean) => void;
setupBuildScript: () => void;
loadWorkspace: () => void; loadWorkspace: () => void;
saveWorkspace: () => void; saveWorkspace: () => void;
updateFileMeta: (editor: EditorStore) => void; updateFileMeta: (editor: EditorStore) => void;
@@ -170,8 +171,8 @@ export type WorkspaceStore = {
isShowingSearch: boolean; isShowingSearch: boolean;
onShowingSearch: (isShowingSearch: boolean) => void; onShowingSearch: (isShowingSearch: boolean) => void;
open: (path: string, selectedNode?: number) => void; open: (path: string, selectedId?: number) => void;
edit: (path: string, selectedNode?: number) => void; edit: (path: string, selectedId?: number) => 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;
@@ -250,6 +251,7 @@ export const useWorkspace = create<WorkspaceStore>((set, get) => ({
modifiedEditors: [], modifiedEditors: [],
workdir: "", workdir: "",
path: "", path: "",
settings: {},
init: (path) => { init: (path) => {
const workspace = get(); const workspace = get();
@@ -272,6 +274,40 @@ export const useWorkspace = create<WorkspaceStore>((set, get) => ({
} }
}, },
// setting
setCheckExpr: (checkExpr: boolean) => {
const { settings, saveWorkspace } = get();
set({
settings: {
...settings,
checkExpr,
},
});
b3util.setCheckExpr(checkExpr);
saveWorkspace();
useWorkspace.getState().editing?.dispatch("refresh");
},
setupBuildScript: () => {
const workspace = get();
let buildScript = dialog.showOpenDialogSync({
properties: ["openFile"],
defaultPath: workspace.workdir,
filters: [{ name: "Javascript", extensions: ["js"] }],
})?.[0];
if (buildScript) {
buildScript = Path.posixPath(buildScript);
const { settings, saveWorkspace, relative } = get();
set({
settings: {
...settings,
buildScript: relative(buildScript),
},
});
saveWorkspace();
}
},
createProject: () => { createProject: () => {
const path = dialog.showSaveDialogSync({ const path = dialog.showSaveDialogSync({
properties: ["showOverwriteConfirmation", "createDirectory"], properties: ["showOverwriteConfirmation", "createDirectory"],
@@ -355,30 +391,46 @@ export const useWorkspace = create<WorkspaceStore>((set, get) => ({
for (const editor of workspace.editors) { for (const editor of workspace.editors) {
editor.dispatch("save"); editor.dispatch("save");
} }
const debug = console.debug;
console.debug = () => {};
try { try {
let hasError = false; let hasError = false;
let buildScript: b3util.BuildScript | undefined;
if (workspace.settings.buildScript) {
const scriptPath = workspace.workdir + "/" + workspace.settings.buildScript;
if (fs.existsSync(scriptPath)) {
buildScript = eval(fs.readFileSync(scriptPath, "utf8"));
}
}
workspace.allFiles.forEach((file) => { workspace.allFiles.forEach((file) => {
const buildpath = buildDir + "/" + workspace.relative(file.path); const buildpath = buildDir + "/" + workspace.relative(file.path);
const treeModel = b3util.createBuildData(file.path); let tree = b3util.createBuildData(file.path);
if (!treeModel) { if (buildScript) {
tree = b3util.processBatch(tree, file.path, buildScript);
}
if (!tree) {
return; return;
} }
if (treeModel.export === false) {
if (tree.export === false) {
console.log("skip:", buildpath); console.log("skip:", buildpath);
return; return;
} }
console.log("build:", buildpath); console.log("build:", buildpath);
const declare: FileVarDecl = { const declare: FileVarDecl = {
import: treeModel.import.map((v) => ({ path: v, vars: [], depends: [] })), import: tree.import.map((v) => ({ path: v, vars: [], depends: [] })),
declvar: treeModel.declvar.map((v) => ({ name: v.name, desc: v.desc })), declvar: tree.declvar.map((v) => ({ name: v.name, desc: v.desc })),
subtree: [], subtree: [],
}; };
b3util.refreshDeclare(treeModel.root, treeModel.group, declare); b3util.refreshDeclare(tree.root, tree.group, declare);
if (!b3util.checkNodeData(treeModel?.root)) { if (!b3util.checkNodeData(tree?.root)) {
hasError = true; hasError = true;
} }
fs.mkdirSync(Path.dirname(buildpath), { recursive: true }); fs.mkdirSync(Path.dirname(buildpath), { recursive: true });
fs.writeFileSync(buildpath, JSON.stringify(treeModel, null, 2)); fs.writeFileSync(buildpath, JSON.stringify(tree, null, 2));
}); });
if (hasError) { if (hasError) {
message.error(i18n.t("buildFailed")); message.error(i18n.t("buildFailed"));
@@ -390,6 +442,7 @@ export const useWorkspace = create<WorkspaceStore>((set, get) => ({
console.error(error); console.error(error);
message.error(i18n.t("buildFailed")); message.error(i18n.t("buildFailed"));
} }
console.debug = debug;
} }
}, },
@@ -405,28 +458,10 @@ export const useWorkspace = create<WorkspaceStore>((set, get) => ({
try { try {
console.log("run script", scriptPath); console.log("run script", scriptPath);
const str = fs.readFileSync(scriptPath, "utf8"); const str = fs.readFileSync(scriptPath, "utf8");
const batch = eval(str) as BatchScript; const batch = eval(str) as b3util.BuildScript;
workspace.allFiles.forEach((file) => { workspace.allFiles.forEach((file) => {
let tree: TreeModel | undefined = readTree(file.path); let tree: TreeModel | null = readTree(file.path);
if (batch.processTree && tree) { tree = b3util.processBatch(tree, file.path, batch);
tree = batch.processTree(tree, file.path);
}
if (tree && batch.processNode) {
const processNode = (node: NodeModel) => {
if (node.children) {
const children: NodeModel[] = [];
node.children?.forEach((child) => {
const newChild = processNode(child);
if (newChild) {
children.push(newChild);
}
});
node.children = children;
}
return batch.processNode?.(node, tree!);
};
tree.root = processNode(tree.root) ?? ({} as NodeModel);
}
if (tree) { if (tree) {
fs.writeFileSync(file.path, JSON.stringify(tree, null, 2)); fs.writeFileSync(file.path, JSON.stringify(tree, null, 2));
} }
@@ -445,7 +480,9 @@ export const useWorkspace = create<WorkspaceStore>((set, get) => ({
loadWorkspace: () => { loadWorkspace: () => {
const workspace = get(); const workspace = get();
const data = readJson(workspace.path) as WorkspaceModel; const data = readWorkspace(workspace.path);
set({ settings: data.settings });
b3util.setCheckExpr(data.settings?.checkExpr ?? true);
data.files?.forEach((file) => { data.files?.forEach((file) => {
workspace.allFiles.set(file.path, { path: file.path, desc: file.desc, exists: false }); workspace.allFiles.set(file.path, { path: file.path, desc: file.desc, exists: false });
}); });
@@ -455,6 +492,7 @@ export const useWorkspace = create<WorkspaceStore>((set, get) => ({
const workspace = get(); const workspace = get();
const data: WorkspaceModel = { const data: WorkspaceModel = {
files: [], files: [],
settings: workspace.settings,
}; };
workspace.allFiles.forEach((file) => { workspace.allFiles.forEach((file) => {
data.files?.push({ data.files?.push({
@@ -484,7 +522,7 @@ export const useWorkspace = create<WorkspaceStore>((set, get) => ({
set({ isShowingSearch }); set({ isShowingSearch });
}, },
open: (path, selectedNode) => { open: (path, selectedId) => {
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);
@@ -494,7 +532,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, selectedNode); workspace.edit(editor.path, selectedId);
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 }));
@@ -504,26 +542,20 @@ 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, selectedNode); workspace.edit(editor.path, selectedId);
} }
}, },
edit: (path, selectedNode) => { edit: (path, selectedId) => {
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 && selectedNode) { if (editor && selectedId) {
editor.editNode = selectedNode; editor.selectedId = selectedId.toString();
} }
set({ editing: editor, editingNode: null, editingTree: null }); set({ editing: editor, editingNode: null, editingTree: null });
if (editor) { if (editor) {
workspace.refresh(editor.path); workspace.refresh(editor.path);
if (!editor.editNode) { workspace.onEditingTree(editor);
workspace.onEditingTree(editor);
} else if (typeof editor.editNode === "object") {
workspace.onEditingNode(editor.editNode);
}
} else {
set({ editingNode: null, editingTree: null });
} }
}, },
@@ -675,27 +707,16 @@ export const useWorkspace = create<WorkspaceStore>((set, get) => ({
// node edit // node edit
onEditingNode: (node) => { onEditingNode: (node) => {
const workspace = get();
if (workspace.editing) {
workspace.editing.editNode = node;
}
set({ editingNode: node, editingNodeDef: null, editingTree: null }); set({ editingNode: node, editingNodeDef: null, editingTree: null });
}, },
onEditingNodeDef: (nodeDef) => { onEditingNodeDef: (nodeDef) => {
const workspace = get();
if (workspace.editing) {
workspace.editing.editNode = null;
}
set({ editingNodeDef: nodeDef, editingNode: null, editingTree: null }); set({ editingNodeDef: nodeDef, editingNode: null, editingTree: null });
}, },
// tree edit // tree edit
onEditingTree: (editor) => { onEditingTree: (editor) => {
const workspace = get(); const workspace = get();
if (workspace.editing) {
workspace.editing.editNode = null;
}
workspace.refresh(editor.path); workspace.refresh(editor.path);
set({ set({
editingTree: { editingTree: {
+73 -5
View File
@@ -1,5 +1,5 @@
import * as fs from "fs"; import * as fs from "fs";
import { NodeDef } from "../behavior3/src/behavior3"; import { ExpressionEvaluator, NodeDef } from "../behavior3/src/behavior3";
import { import {
FileVarDecl, FileVarDecl,
ImportDef, ImportDef,
@@ -20,6 +20,16 @@ import {
import Path from "./path"; import Path from "./path";
import { readJson, readTree } from "./util"; import { readJson, readTree } from "./util";
export interface BatchScript {
processTree?(tree: TreeModel, path: string): TreeModel | null;
processNode?(node: NodeModel, tree: TreeModel): NodeModel | null;
}
export interface BuildScript extends BatchScript {
writeFile?(path: string, tree: TreeModel): void;
}
export class NodeDefs extends Map<string, NodeDef> { export class NodeDefs extends Map<string, NodeDef> {
get(key: string): NodeDef { get(key: string): NodeDef {
return super.get(key) ?? unknownNodeDef; return super.get(key) ?? unknownNodeDef;
@@ -34,6 +44,7 @@ export const files: Record<string, number> = {};
const parsedVarDefs: Record<string, ImportDef> = {}; const parsedVarDefs: Record<string, ImportDef> = {};
const parsedExprs: Record<string, string[]> = {}; const parsedExprs: Record<string, string[]> = {};
let checkExpr: boolean = false;
let workdir: string = ""; let workdir: string = "";
let alertError: (msg: string, duration?: number) => void = () => {}; let alertError: (msg: string, duration?: number) => void = () => {};
@@ -78,6 +89,10 @@ export const updateUsingVars = (vars: VarDef[]) => {
} }
}; };
export const setCheckExpr = (check: boolean) => {
checkExpr = check;
};
export const parseExpr = (expr: string) => { export const parseExpr = (expr: string) => {
if (parsedExprs[expr]) { if (parsedExprs[expr]) {
return parsedExprs[expr]; return parsedExprs[expr];
@@ -323,11 +338,13 @@ export const checkNodeData = (data: NodeModel | null | undefined) => {
} }
} }
} }
}
if (data.args && conf.args) { if (data.args && conf.args) {
for (const arg of conf.args) { for (const arg of conf.args) {
const value = data.args?.[arg.name] as string | string[] | undefined; const value = data.args?.[arg.name] as string | string[] | undefined;
if (isExprType(arg.type)) { if (isExprType(arg.type) && value) {
if (usingVars) {
const vars: string[] = []; const vars: string[] = [];
if (typeof value === "string") { if (typeof value === "string") {
vars.push(...parseExpr(value)); vars.push(...parseExpr(value));
@@ -343,6 +360,27 @@ export const checkNodeData = (data: NodeModel | null | undefined) => {
} }
} }
} }
if (checkExpr) {
const exprs: string[] = [];
if (typeof value === "string") {
exprs.push(value);
} else if (Array.isArray(value)) {
for (const v of value) {
exprs.push(v);
}
}
for (const expr of exprs) {
try {
if (!new ExpressionEvaluator(expr).dryRun()) {
error(data, `expr '${expr}' is not valid`);
hasError = true;
}
} catch (e) {
error(data, `expr '${expr}' is not valid`);
hasError = true;
}
}
}
} }
} }
} }
@@ -740,6 +778,35 @@ export const createBuildData = (path: string) => {
return null; return null;
}; };
export const processBatch = (tree: TreeModel | null, path: string, batch: BatchScript) => {
if (!tree) {
return null;
}
if (batch.processTree) {
tree = batch.processTree(tree, path);
}
if (!tree) {
return null;
}
if (batch.processNode) {
const processNode = (node: NodeModel) => {
if (node.children) {
const children: NodeModel[] = [];
node.children?.forEach((child) => {
const newChild = processNode(child);
if (newChild) {
children.push(newChild);
}
});
node.children = children;
}
return batch.processNode?.(node, tree);
};
tree.root = processNode(tree.root) ?? ({} as NodeModel);
}
return tree;
};
export const createFileData = (data: TreeGraphData, includeSubtree?: boolean) => { export const createFileData = (data: TreeGraphData, includeSubtree?: boolean) => {
const nodeData: NodeModel = { const nodeData: NodeModel = {
id: Number(data.id), id: Number(data.id),
@@ -887,4 +954,5 @@ export const refreshDeclare = (
}); });
updateUsingGroups(group); updateUsingGroups(group);
updateUsingVars(Array.from(vars)); updateUsingVars(Array.from(vars));
console.debug("refresh declare", group, vars);
}; };
+9 -2
View File
@@ -1,13 +1,20 @@
import * as fs from "fs"; import * as fs from "fs";
import { type WorkspaceModel } from "../contexts/workspace-context";
import { VERSION, type TreeModel } from "./b3type"; import { VERSION, type TreeModel } from "./b3type";
export const readJson = (path: string) => { 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 readWorkspace = (path: string) => {
const data = readJson(path) as WorkspaceModel;
data.settings = data.settings ?? {};
return data;
};
export const readTree = (path: string) => { export const readTree = (path: string) => {
const data = readJson(path); const data = readJson(path) as TreeModel;
data.version = data.version ?? VERSION; data.version = data.version ?? VERSION;
data.firstid = data.firstid ?? 1; data.firstid = data.firstid ?? 1;
data.group = data.group || []; data.group = data.group || [];