diff --git a/public/locales/en.json b/public/locales/en.json index ac67896..7137bd0 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -116,6 +116,7 @@ "node.subtree": "Subtree", "node.subtreePathError": "You should save the subtree inside the current workspace", "node.type": "Node type", + "node.undefinedVariable": "Variable '{{variable}}' is not defined", "node.unknown.desc": "New node" } diff --git a/public/locales/zh.json b/public/locales/zh.json index be9c5c5..ce978cf 100644 --- a/public/locales/zh.json +++ b/public/locales/zh.json @@ -116,6 +116,7 @@ "node.subtree": "子树", "node.subtreePathError": "子树必须保存在当前工作区的目录内", "node.type": "节点类型", + "node.undefinedVariable": "变量 “{{variable}}” 未定义", "node.unknown.desc": "新建节点" } diff --git a/sample/vars/test-vars.json b/sample/vars/test-vars.json index 81ef870..a098f59 100644 --- a/sample/vars/test-vars.json +++ b/sample/vars/test-vars.json @@ -15,6 +15,14 @@ { "name": "target", "desc": "目标" + }, + { + "name": "target1", + "desc": "其它目标" + }, + { + "name": "test", + "desc": "测试" } ] }, @@ -32,21 +40,21 @@ "name": "FindEnemy", "args": {}, "output": [ - "target" + "target1" ] }, { "id": 5, "name": "Attack", "input": [ - "target" + "target1" ] }, { "id": 6, "name": "Check", "args": { - "value": "target.x > 0" + "value": "target1.x > 0" } }, { diff --git a/src/components/editor.tsx b/src/components/editor.tsx index 672f438..696150e 100644 --- a/src/components/editor.tsx +++ b/src/components/editor.tsx @@ -21,8 +21,9 @@ import { EditTree, useWorkspace, } from "../contexts/workspace-context"; -import { isExprType, NodeModel, TreeGraphData, TreeModel } from "../misc/b3type"; +import { ImportDef, isExprType, NodeModel, TreeGraphData, TreeModel, VarDef } from "../misc/b3type"; import * as b3util from "../misc/b3util"; +import { loadSubtreeVarDef, loadVarDef } from "../misc/b3util"; import { message } from "../misc/hooks"; import i18n from "../misc/i18n"; import { Hotkey, isMacos, useKeyDown } from "../misc/keys"; @@ -47,6 +48,13 @@ interface FilterOption { isFocused: boolean; } +const collectVarDefs = (editor: EditorStore) => { + const vars: VarDef[] = editor.declare.vars.slice(); + loadVarDef(editor.declare.imports).forEach((v) => vars.push(v)); + loadSubtreeVarDef(editor.data).forEach((v) => vars.push(v)); + return vars; +}; + const createTreeData = (node: NodeModel, parent?: string) => { return b3util.createTreeData(node, parent, calcTreeDataSize); }; @@ -60,6 +68,42 @@ const refreshTreeData = (data: TreeGraphData) => { } }; +const isTreeUpdated = (editor: EditorStore, editTree: EditTree) => { + const data = editTree.data; + if ( + editor.firstid !== data.firstid || + editor.export !== data.export || + editor.name !== data.name || + editor.desc !== data.desc + ) { + return true; + } + + for (let i = 0; i < Math.max(editor.declare.vars.length, data.declare.vars.length); i++) { + const v1: VarDef | undefined = editor.declare.vars[i]; + const v2: VarDef | undefined = data.declare.vars[i]; + if (v1?.name !== v2?.name || v1?.desc !== v2?.desc) { + return true; + } + } + + for (let i = 0; i < Math.max(editor.group.length, data.group.length); i++) { + if (editor.group[i] !== data.group[i]) { + return true; + } + } + + for (let i = 0; i < Math.max(editor.declare.imports.length, data.declare.imports.length); i++) { + const v1: ImportDef | undefined = editor.declare.imports[i]; + const v2: ImportDef | undefined = data.declare.imports[i]; + if (v1?.path !== v2?.path) { + return true; + } + } + + return false; +}; + const isSubtreeUpdated = (data: TreeGraphData) => { if (data.path) { try { @@ -491,17 +535,16 @@ export const Editor: FC = ({ onUpdate: updateState, data: editor, . }; const updateTree = (editTree: EditTree) => { - if (editor.desc !== editTree.data.desc || editor.export !== editTree.data.export) { + if (isTreeUpdated(editor, editTree)) { editor.desc = editTree.data.desc || ""; editor.export = editTree.data.export !== false; editor.group = editTree.data.group || []; + editor.firstid = editTree.data.firstid ?? 1; + editor.declare.vars = editTree.data.declare.vars || []; + editor.declare.imports = editTree.data.declare.imports || []; b3util.updateUsingGroups(editor.group); - if (editor.data.firstid !== editTree.data.firstid) { - editor.firstid = editTree.data.firstid ?? 1; - editor.declare.vars = editTree.data.declare.vars || []; - editor.declare.imports = editTree.data.declare.imports || []; - refresh(); - } + b3util.updateUsingVars(collectVarDefs(editor)); + refresh(); onChange(); } }; @@ -1207,6 +1250,7 @@ export const Editor: FC = ({ onUpdate: updateState, data: editor, . if (workspace.editing === editor) { checkSubtree(); b3util.updateUsingGroups(editor.group); + b3util.updateUsingVars(collectVarDefs(editor)); } }, [workspace.editing]); diff --git a/src/components/inspector.tsx b/src/components/inspector.tsx index a8adc8c..b7ffb84 100644 --- a/src/components/inspector.tsx +++ b/src/components/inspector.tsx @@ -43,7 +43,9 @@ import { isValidVariableName, isVariadic, loadVarDef, + parseExpr, usingGroups, + usingVars, } from "../misc/b3util"; import { Hotkey, isMacos } from "../misc/keys"; import { mergeClassNames } from "../misc/util"; @@ -108,7 +110,7 @@ const TreeInspector: FC = () => { workspace.editing?.dispatch("updateTree", { data: { name: values.name, - desc: values.desc || undefined, + desc: values.desc, export: values.export, firstid: Number(values.firstid), group: (values.group as GroupDef[]) @@ -568,6 +570,7 @@ const NodeInspector: FC = () => { // auto complete for input and output const inoutVarOptions = useMemo(() => { const options: OptionType[] = []; + const filter: Record = {}; const collect = (node?: TreeGraphData) => { if (node) { node.input?.forEach((v, i) => { @@ -578,8 +581,9 @@ const NodeInspector: FC = () => { } else { desc = inputDef?.[i] ?? ""; } - if (v && !options.find((option) => option.value === v)) { + if (!filter[v]) { options.push({ label: `${v}(${desc})`, value: v }); + filter[v] = true; } }); node.output?.forEach((v, i) => { @@ -590,16 +594,26 @@ const NodeInspector: FC = () => { } else { desc = outputDef?.[i] ?? ""; } - if (v && !options.find((option) => option.value === v)) { + if (!filter[v]) { options.push({ label: `${v}(${desc})`, value: v }); + filter[v] = true; } }); node.children?.forEach((child) => collect(child)); } }; - collect(workspace.editing?.data); + if (usingVars) { + Object.values(usingVars ?? {}).forEach((v) => { + if (!filter[v.name]) { + options.push({ label: `${v.name}(${v.desc})`, value: v.name }); + filter[v.name] = true; + } + }); + } else { + collect(workspace.editing?.data); + } return options; - }, [workspace.editing]); + }, [workspace.editing, usingVars]); // auto complete for subtree const subtreeOptions = useMemo(() => { @@ -753,7 +767,7 @@ const NodeInspector: FC = () => { rules={[ { validator() { - if (def.group && !usingGroups[def.group]) { + if (def.group && usingGroups && !usingGroups[def.group]) { return Promise.reject(new Error(t("node.invalidGroup", { group: def.group }))); } return Promise.resolve(); @@ -833,6 +847,13 @@ const NodeInspector: FC = () => { rules={[ { validator(_, value) { + if (value && usingVars && !usingVars[value]) { + return Promise.reject( + new Error( + t("node.undefinedVariable", { variable: value }) + ) + ); + } if (value && !isValidVariableName(value)) { return Promise.reject( new Error(t("node.invalidVariableName")) @@ -895,6 +916,11 @@ const NodeInspector: FC = () => { { required, message: t("fieldRequired", { field: desc }) }, ({ getFieldValue, setFieldValue, isFieldValidating, validateFields }) => ({ validator(_, value) { + if (value && usingVars && !usingVars[value]) { + return Promise.reject( + new Error(t("node.undefinedVariable", { variable: value })) + ); + } if (value && !isValidVariableName(value)) { return Promise.reject(new Error(t("node.invalidVariableName"))); } @@ -975,6 +1001,17 @@ const NodeInspector: FC = () => { }, () => ({ validator(_, value) { + if (usingVars && isExprType(type) && 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") { @@ -1088,6 +1125,15 @@ const NodeInspector: FC = () => { { required, message: t("fieldRequired", { field: arg.desc }) }, ({ getFieldValue, setFieldValue, isFieldValidating, validateFields }) => ({ validator(_, value) { + if (usingVars && isExprType(type) && value) { + for (const v of parseExpr(value)) { + if (!usingVars![v]) { + return Promise.reject( + new Error(t("node.undefinedVariable", { variable: v })) + ); + } + } + } if (value && isJsonType(type)) { try { if (value !== "null") { @@ -1204,6 +1250,13 @@ const NodeInspector: FC = () => { rules={[ { validator(_, value) { + if (value && usingVars && !usingVars[value]) { + return Promise.reject( + new Error( + t("node.undefinedVariable", { variable: value }) + ) + ); + } if (value && !isValidVariableName(value)) { return Promise.reject( new Error(t("node.invalidVariableName")) @@ -1266,6 +1319,11 @@ const NodeInspector: FC = () => { { required, message: t("fieldRequired", { field: desc }) }, { validator(_, value) { + if (value && usingVars && !usingVars[value]) { + return Promise.reject( + new Error(t("node.undefinedVariable", { variable: value })) + ); + } if (value && !isValidVariableName(value)) { return Promise.reject(new Error(t("node.invalidVariableName"))); } diff --git a/src/components/register-node.ts b/src/components/register-node.ts index 0a59ecd..7dd14cc 100644 --- a/src/components/register-node.ts +++ b/src/components/register-node.ts @@ -1,7 +1,7 @@ import G6 from "@antv/g6"; import { useWorkspace } from "../contexts/workspace-context"; -import { TreeGraphData, getNodeType } from "../misc/b3type"; -import { checkTreeData, nodeDefs, usingGroups } from "../misc/b3util"; +import { TreeGraphData, getNodeType, isExprType } from "../misc/b3type"; +import { checkTreeData, nodeDefs, parseExpr, usingGroups, usingVars } from "../misc/b3util"; import i18n from "../misc/i18n"; import { isMacos } from "../misc/keys"; @@ -145,11 +145,20 @@ G6.registerNode( const nodeDef = nodeDefs.get(data.name); let classify = getNodeType(nodeDef); let color = nodeDef.color || NODE_COLORS[classify] || NODE_COLORS["Other"]; + if ( !nodeDefs.has(data.name) || (data.path && !data.children?.length) || !checkTreeData(data) || - (nodeDef.group && !usingGroups[nodeDef.group]) + (nodeDef.group && usingGroups && !usingGroups[nodeDef.group]) || + (usingVars && data.input?.some((v) => !usingVars![v])) || + (usingVars && data.output?.some((v) => !usingVars![v])) || + (usingVars && + nodeDef.args?.some( + (v) => + isExprType(v.type) && + parseExpr(data.args?.[v.name] ?? "").some((vv) => !usingVars![vv]) + )) ) { classify = "Error"; color = NODE_COLORS[classify]; diff --git a/src/misc/b3util.ts b/src/misc/b3util.ts index ab51ce4..4c92499 100644 --- a/src/misc/b3util.ts +++ b/src/misc/b3util.ts @@ -26,10 +26,10 @@ export class NodeDefs extends Map { export let nodeDefs: NodeDefs = new NodeDefs(); export let groupDefs: string[] = []; +export let usingGroups: Record | null = null; +export let usingVars: Record | null = null; -export let usingGroups: Record = {}; -export let usingVars: Record = {}; - +const parsedExprs: Record = {}; let workdir: string = ""; let alertError: (msg: string, duration?: number) => void = () => {}; @@ -58,16 +58,18 @@ export const initWorkdir = (path: string, handler: typeof alertError) => { }; export const updateUsingGroups = (group: string[]) => { - usingGroups = {}; + usingGroups = null; for (const g of group) { + usingGroups ??= {}; usingGroups[g] = true; } }; export const updateUsingVars = (vars: VarDef[]) => { - usingVars = {}; + usingVars = null; for (const v of vars) { - usingVars[v.name] = true; + usingVars ??= {}; + usingVars[v.name] = v; } }; @@ -711,6 +713,7 @@ export const isTreeFile = (path: string) => { }; export const loadVarDef = (list: ImportDef[]) => { + const all: Set = new Set(); for (const entry of list) { // TODO: check file date // TODO: use cache @@ -743,7 +746,9 @@ export const loadVarDef = (list: ImportDef[]) => { return false; }) .sort((a, b) => a.name.localeCompare(b.name)); + entry.vars.forEach((v) => all.add(v)); } + return Array.from(all); }; export const loadSubtreeVarDef = (data: TreeGraphData) => { @@ -757,21 +762,17 @@ export const loadSubtreeVarDef = (data: TreeGraphData) => { } }; traverse(data); - loadVarDef(list); - const filter: Map = new Map(); - const result: VarDef[] = []; - list.forEach((entry) => { - entry.vars.forEach((v) => { - if (!filter.has(v.name)) { - filter.set(v.name, true); - result.push(v); - } - }); - }); - return result; + return loadVarDef(list); }; export const parseExpr = (expr: string) => { - const result = expr.split(/[^a-zA-Z0-9_.]/); - return result.map((v) => v.split(".")[0]).filter((v) => v); + if (parsedExprs[expr]) { + return parsedExprs[expr]; + } + const result = expr + .split(/[^a-zA-Z0-9_.]/) + .map((v) => v.split(".")[0]) + .filter((v) => isValidVariableName(v)); + parsedExprs[expr] = result; + return result; };