diff --git a/public/icons/Action.svg b/public/icons/Action.svg
index 96808ad..3e6376e 100644
--- a/public/icons/Action.svg
+++ b/public/icons/Action.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/public/icons/Composite.svg b/public/icons/Composite.svg
index be106d7..981bd44 100644
--- a/public/icons/Composite.svg
+++ b/public/icons/Composite.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/public/locales/en.json b/public/locales/en.json
index c7ff136..2a1048b 100644
--- a/public/locales/en.json
+++ b/public/locales/en.json
@@ -94,41 +94,5 @@
"node.subtree": "Subtree",
"node.subtreePathError": "You should save the subtree inside the current workspace",
- "node.unknown.desc": "New node",
- "node.alwaysFail.desc": "Always return failure",
- "node.alwaysFail.doc": "+ Only one child node, multiple child nodes will be executed first\n+ If any child node fails, return failure",
- "node.alwaysSuccess.desc": "Always return success",
- "node.alwaysSuccess.doc": "+ Only one child node, multiple child nodes will be executed first\n+ If any child node fails, return success",
- "node.check.arg1.desc": "Expression",
- "node.check.desc": "Evaluate expression",
- "node.check.doc": "+ Do simple calculation, return success or failure",
- "node.clear.desc": "Clear variable",
- "node.clear.output1": "Variable name",
- "node.forEach.desc": "Iterate array",
- "node.forEach.input1": "Array",
- "node.forEach.output1": "Variable name",
- "node.forEach.doc": "+ Before executing each child node, set the current variable to the output\n+ Execute all child nodes\n+ Return success or running",
- "node.getTime.desc": "Get current time",
- "node.getTime.output1": "Current time",
- "node.isNull.desc": "Check variable is null",
- "node.isNull.input1": "Variable",
- "node.log.desc": "Print log",
- "node.log.arg1.desc": "Log content",
- "node.loop.desc": "Loop execute",
- "node.loop.arg1.desc": "Loop times",
- "node.loop.input1": "Loop times",
- "node.not.desc": "Invert",
- "node.not.doc": "+ Invert the return value",
- "node.notNull.desc": "Check variable is not null",
- "node.notNull.input1": "Variable",
- "node.once.desc": "Execute once",
- "node.once.doc": "+ Execute all child nodes\n+ Return success or running",
- "node.parallel.desc": "Parallel execute",
- "node.parallel.doc": "+ Execute all child nodes\n+ Return success",
- "node.selector.desc": "Selector execute",
- "node.selector.doc": "+ Execute all child nodes\n+ Return success or failure",
- "node.sequence.desc": "Sequence execute",
- "node.sequence.doc": "+ Execute all child nodes\n+ Return success or failure",
- "node.wait.desc": "Wait",
- "node.wait.arg1.desc": "Wait time"
+ "node.unknown.desc": "New node"
}
diff --git a/public/locales/zh.json b/public/locales/zh.json
index f9db29f..51497a8 100644
--- a/public/locales/zh.json
+++ b/public/locales/zh.json
@@ -94,41 +94,5 @@
"node.subtree": "子树",
"node.subtreePathError": "子树必须保存在当前工作区的目录内",
- "node.unknown.desc": "新建节点",
- "node.alwaysFail.desc": "始终返回失败",
- "node.alwaysFail.doc": "+ 只能有一个子节点,多个仅执行第一个\n+ 不管子节点是否成功都返回失败",
- "node.alwaysSuccess.desc": "始终返回成功",
- "node.alwaysSuccess.doc": "+ 只能有一个子节点,多个仅执行第一个\n+ 不管子节点是否成功都返回成功",
- "node.check.arg1.desc": "表达式",
- "node.check.desc": "对表达式进行求值",
- "node.check.doc": "+ 做简单数值公式判定,返回成功或失败",
- "node.clear.desc": "清除变量",
- "node.clear.output1": "清除的变量名",
- "node.forEach.desc": "遍历数组",
- "node.forEach.input1": "数组",
- "node.forEach.output1": "变量名",
- "node.forEach.doc": "+ 每次执行子节点前会设置当前遍历到的变量\n+ 会执行所有子节点\n+ 永远返回成功/正在运行",
- "node.getTime.desc": "获取当前时间",
- "node.getTime.output1": "当前时间",
- "node.isNull.desc": "判断变量是否为空",
- "node.isNull.input1": "判断的变量",
- "node.log.desc": "打印日志",
- "node.log.arg1.desc": "日志内容",
- "node.loop.desc": "循环执行",
- "node.loop.arg1.desc": "循环次数",
- "node.loop.input1": "循环次数",
- "node.not.desc": "取反",
- "node.not.doc": "+ 将子节点的返回值取反",
- "node.notNull.desc": "判断变量是否存在",
- "node.notNull.input1": "判断的变量",
- "node.once.desc": "只执行一次",
- "node.once.doc": "+ 可以接多个子节点,子节点默认全部执行\n+ 被打断后该节点后的子节点依旧不会执行\n+ 该节点执行后永远返回成功",
- "node.parallel.desc": "并行执行",
- "node.parallel.doc": "+ 执行所有子节点并返回成功",
- "node.selector.desc": "选择执行",
- "node.selector.doc": "+ 一直往下执行,有子节点返回成功则返回成功,若全部节点返回失败则返回失败\n+ 子节点是或 (OR) 的关系",
- "node.sequence.desc": "顺序执行",
- "node.sequence.doc": "+ 一直往下执行,只有当所有子节点都返回成功, 才返回成功\n+ 子节点是与(AND)的关系",
- "node.wait.desc": "等待",
- "node.wait.arg1.desc": "等待时间"
+ "node.unknown.desc": "新建节点"
}
diff --git a/src/components/register-node.ts b/src/components/register-node.ts
index 06a5d42..bcfc817 100644
--- a/src/components/register-node.ts
+++ b/src/components/register-node.ts
@@ -7,10 +7,10 @@ import Path from "@/misc/path";
import G6 from "@antv/g6";
const NODE_COLORS: any = {
- ["Composite"]: "#5aed20",
- ["Decorator"]: "#faad14",
+ ["Composite"]: "#34d800",
+ ["Decorator"]: "#ff6700",
["Condition"]: "#e4148b",
- ["Action"]: "#5b90f9",
+ ["Action"]: "#1668dc",
["Other"]: "#707070",
["Error"]: "#ff0000",
};
@@ -193,9 +193,9 @@ G6.registerNode(
addShape("image", {
attrs: {
x: 5,
- y: 3.5,
- height: 16,
- width: 16,
+ y: 3,
+ height: 18,
+ width: 18,
img,
},
name: "node-icon",
@@ -205,7 +205,7 @@ G6.registerNode(
addShape("text", {
attrs: {
textBaseline: "top",
- x: 25,
+ x: 26,
y: 5,
fontWeight: 800,
text: cfg.name,
diff --git a/src/components/workspace.tsx b/src/components/workspace.tsx
index 4f3df38..cfa1a44 100644
--- a/src/components/workspace.tsx
+++ b/src/components/workspace.tsx
@@ -65,6 +65,7 @@ export const Workspace: FC = () => {
workspace.close(workspace.editing.path);
}
}
+ keysRef.current?.focus();
} else if (isHotkeyPressed(Hotkey.SearchTree)) {
event.preventDefault();
workspace.onShowingSearch(true);
diff --git a/src/misc/b3util.ts b/src/misc/b3util.ts
index 662258f..bec3031 100644
--- a/src/misc/b3util.ts
+++ b/src/misc/b3util.ts
@@ -4,6 +4,7 @@ import * as fs from "fs";
import { message } from "./hooks";
import i18n from "./i18n";
import Path from "./path";
+import { zhNodeDef } from "./template";
export const isSubtreeRoot = (data: TreeGraphData) => {
return data.path && data.id.toString() !== "1";
@@ -415,139 +416,7 @@ export const cutWordTo = (str: string, maxlen: number) => {
};
export const createProject = (path: string) => {
- fs.writeFileSync(
- Path.dirname(path) + "/node-config.b3-setting",
- JSON.stringify(
- [
- {
- name: "AlwaysFail",
- type: "Decorator",
- desc: i18n.t("node.alwaysFail.desc"),
- doc: i18n.t("node.alwaysFail.doc"),
- },
- {
- name: "AlwaysSuccess",
- type: "Decorator",
- desc: i18n.t("node.alwaysSuccess.desc"),
- doc: i18n.t("node.alwaysSuccess.doc"),
- },
- {
- name: "Check",
- type: "Condition",
- desc: i18n.t("node.check.desc"),
- args: [
- {
- name: "value",
- type: "code",
- desc: i18n.t("node.check.arg1.desc"),
- },
- ],
- doc: i18n.t("node.check.doc"),
- },
- {
- name: "Clear",
- type: "Action",
- desc: i18n.t("node.clear.desc"),
- output: [i18n.t("node.clear.output1")],
- },
- {
- name: "ForEach",
- type: "Composite",
- desc: i18n.t("node.forEach.desc"),
- input: [i18n.t("node.forEach.input1")],
- output: [i18n.t("node.forEach.output1")],
- doc: i18n.t("node.forEach.doc"),
- },
- {
- name: "GetTime",
- type: "Action",
- desc: i18n.t("node.getTime.desc"),
- output: [i18n.t("node.getTime.output1")],
- },
- {
- name: "IsNull",
- type: "Condition",
- desc: i18n.t("node.isNull.desc"),
- input: [i18n.t("node.isNull.input1")],
- },
- {
- name: "Log",
- type: "Action",
- desc: i18n.t("node.log.desc"),
- args: [
- {
- name: "message",
- type: "string",
- desc: i18n.t("node.log.arg1.desc"),
- },
- ],
- },
- {
- name: "Loop",
- type: "Composite",
- desc: i18n.t("node.loop.desc"),
- args: [
- {
- name: "count",
- type: "int?",
- desc: i18n.t("node.loop.arg1.desc"),
- },
- ],
- input: [i18n.t("node.loop.input1") + "?"],
- },
- {
- name: "Not",
- type: "Decorator",
- desc: i18n.t("node.not.desc"),
- doc: i18n.t("node.not.doc"),
- },
- {
- name: "NotNull",
- type: "Condition",
- desc: i18n.t("node.notNull.desc"),
- input: [i18n.t("node.notNull.input")],
- },
- {
- name: "Once",
- type: "Composite",
- desc: i18n.t("node.once.desc"),
- doc: i18n.t("node.once.doc"),
- },
- {
- name: "Parallel",
- type: "Composite",
- desc: i18n.t("node.parallel.desc"),
- doc: i18n.t("node.parallel.doc"),
- },
- {
- name: "Selector",
- type: "Composite",
- desc: i18n.t("node.selector.desc"),
- doc: i18n.t("node.selector.doc"),
- },
- {
- name: "Sequence",
- type: "Composite",
- desc: i18n.t("node.sequence.desc"),
- doc: i18n.t("node.sequence.doc"),
- },
- {
- name: "Wait",
- type: "Action",
- desc: i18n.t("node.wait.desc"),
- args: [
- {
- name: "time",
- type: "float",
- desc: i18n.t("node.wait.arg1.desc"),
- },
- ],
- },
- ],
- null,
- 2
- )
- );
+ fs.writeFileSync(Path.dirname(path) + "/node-config.b3-setting", zhNodeDef());
fs.writeFileSync(
Path.dirname(path) + "/example.json",
JSON.stringify(
diff --git a/src/misc/template.ts b/src/misc/template.ts
new file mode 100644
index 0000000..53e3423
--- /dev/null
+++ b/src/misc/template.ts
@@ -0,0 +1,323 @@
+type Status = "success" | "failure" | "running";
+
+type Constructor = new (...args: unknown[]) => T;
+
+interface TreeEnv {}
+interface Node {}
+
+interface NodeDef {
+ name: string;
+ type: "Action" | "Decorator" | "Condition" | "Composite";
+ desc: string;
+ icon?: string;
+ color?: string;
+ input?: string[];
+ args?: {
+ name: string;
+ type:
+ | "boolean"
+ | "boolean?"
+ | "int"
+ | "int?"
+ | "float"
+ | "float?"
+ | "enum"
+ | "enum?"
+ | "string"
+ | "string?"
+ | "code"
+ | "code?";
+ desc: string;
+ options?: { name: string; value: unknown }[];
+ }[];
+ output?: string[];
+ doc?: string;
+}
+
+abstract class Process {
+ init(node: Node): void {}
+
+ run(node: Node, env: TreeEnv): Status {
+ return "success";
+ }
+
+ abstract get descriptor(): NodeDef;
+}
+
+namespace zh {
+ export namespace action {
+ export class Clear extends Process {
+ override get descriptor(): NodeDef {
+ return {
+ name: "Clear",
+ type: "Action",
+ desc: "清除变量",
+ output: ["清除的变量名"],
+ };
+ }
+ }
+
+ export class Log extends Process {
+ override get descriptor(): NodeDef {
+ return {
+ name: "Log",
+ type: "Action",
+ desc: "打印日志",
+ args: [{ name: "message", type: "string", desc: "日志" }],
+ };
+ }
+ }
+
+ export class Now extends Process {
+ override get descriptor(): NodeDef {
+ return {
+ name: "Now",
+ type: "Action",
+ desc: "获取当前时间",
+ output: ["当前时间"],
+ };
+ }
+ }
+
+ export class Wait extends Process {
+ override get descriptor(): NodeDef {
+ return {
+ name: "Wait",
+ type: "Action",
+ desc: "等待",
+ input: ["等待时间?"],
+ args: [
+ { name: "time", type: "float", desc: "等待时间" },
+ { name: "random", type: "float?", desc: "随机范围" },
+ ],
+ };
+ }
+ }
+ }
+
+ export namespace composite {
+ export class Foreach extends Process {
+ override get descriptor(): NodeDef {
+ return {
+ name: "ForEach",
+ type: "Composite",
+ desc: "遍历数组",
+ input: ["数组"],
+ output: ["变量"],
+ doc: `
+ + 每次执行子节点前会设置当前遍历到的变量
+ + 会执行所有子节点
+ + 永远返回「成功」/「运行中」`,
+ };
+ }
+ }
+
+ export class Loop extends Process {
+ override get descriptor(): NodeDef {
+ return {
+ name: "Loop",
+ type: "Composite",
+ desc: "循环执行",
+ input: ["循环次数?"],
+ args: [{ name: "count", type: "int", desc: "循环次数" }],
+ };
+ }
+ }
+
+ export class Parallel extends Process {
+ override get descriptor(): NodeDef {
+ return {
+ name: "Parallel",
+ type: "Composite",
+ desc: "并行执行",
+ doc: `执行所有子节点并返回「成功」/「运行中」`,
+ };
+ }
+ }
+
+ export class Selector extends Process {
+ override get descriptor(): NodeDef {
+ return {
+ name: "Selector",
+ type: "Composite",
+ desc: "选择执行",
+ doc: `
+ + 一直往下执行,有子节点返回「成功」则返回「成功」
+ + 若全部节点返回「失败」则返回「失败」
+ + 子节点是或 (OR) 的关系`,
+ };
+ }
+ }
+
+ export class Sequence extends Process {
+ override get descriptor(): NodeDef {
+ return {
+ name: "Sequence",
+ type: "Composite",
+ desc: "顺序执行",
+ doc: `
+ + 一直往下执行,只有当所有子节点都返回「成功」, 才返回「成功」
+ + 子节点是与(AND)的关系`,
+ };
+ }
+ }
+ }
+
+ export namespace condition {
+ export class Check extends Process {
+ override get descriptor(): NodeDef {
+ return {
+ name: "Check",
+ type: "Condition",
+ desc: "检查True或False",
+ args: [{ name: "value", type: "code", desc: "值" }],
+ doc: `
+ + 做简单数值公式判定,返回「成功」或「失败」`,
+ };
+ }
+ }
+ export class IsNull extends Process {
+ override get descriptor(): NodeDef {
+ return {
+ name: "IsNull",
+ type: "Condition",
+ desc: "判断变量是否不存在",
+ input: ["判断的变量"],
+ };
+ }
+ }
+
+ export class NotNull extends Process {
+ override get descriptor(): NodeDef {
+ return {
+ name: "NotNull",
+ type: "Condition",
+ desc: "判断变量是否存在",
+ input: ["判断的变量"],
+ };
+ }
+ }
+ }
+
+ export namespace decorator {
+ export class AlwaysFail extends Process {
+ override get descriptor(): NodeDef {
+ return {
+ name: "AlwaysFail",
+ type: "Decorator",
+ desc: "始终返回失败",
+ doc: `
+ + 只能有一个子节点,多个仅执行第一个
+ + 不管子节点是否成功都返回「失败」`,
+ };
+ }
+ }
+ export class AlwaysSuccess extends Process {
+ override get descriptor(): NodeDef {
+ return {
+ name: "AlwaysSuccess",
+ type: "Decorator",
+ desc: "始终返回成功",
+ doc: `
+ + 只能有一个子节点,多个仅执行第一个
+ + 不管子节点是否成功都返回「成功」`,
+ };
+ }
+ }
+
+ export class Inverter extends Process {
+ override get descriptor(): NodeDef {
+ return {
+ name: "Inverter",
+ type: "Decorator",
+ desc: "反转子节点运行结果",
+ doc: `
+ + 只能有一个子节点,多个仅执行第一个
+ + 当子节点返回「成功」时返回「失败」
+ + 当子节点返回「失败」时返回「成功」
+ + 其余返回「运行中」`,
+ };
+ }
+ }
+
+ export class Once extends Process {
+ override get descriptor(): NodeDef {
+ return {
+ name: "Once",
+ type: "Decorator",
+ desc: "只执行一次",
+ doc: `
+ + 可以接多个子节点,子节点默认全部执行
+ + 被打断后该节点后的子节点依旧不会执行
+ + 该节点执行后永远返回「成功」`,
+ };
+ }
+ }
+ export class RepeatUntilFailure extends Process {
+ override get descriptor(): NodeDef {
+ return {
+ name: "RepeatUntilFailure",
+ type: "Decorator",
+ desc: "一直尝试直到子节点返回失败",
+ input: ["最大循环次数?"],
+ args: [{ name: "maxLoop", type: "int?", desc: "最大循环次数" }],
+ doc: `
+ + 只能有一个子节点,多个仅执行第一个
+ + 只有当子节点返回「失败」时,才返回「成功」,其它情况返回「运行中」状态
+ + 如果设定了尝试次数,超过指定次数则返回「失败」`,
+ };
+ }
+ }
+
+ export class RepeatUntilSuccess extends Process {
+ override get descriptor(): NodeDef {
+ return {
+ name: "RepeatUntilSuccess",
+ type: "Decorator",
+ desc: "一直尝试直到子节点返回成功",
+ input: ["最大循环次数?"],
+ args: [{ name: "maxLoop", type: "int?", desc: "最大循环次数" }],
+ doc: `
+ + 只能有一个子节点,多个仅执行第一个
+ + 只有当子节点返回「成功」时,才返回「成功」,其它情况返回「运行中」状态
+ + 如果设定了尝试次数,超过指定次数则返回「失败」`,
+ };
+ }
+ }
+ }
+}
+
+const toString = (nodes: Constructor[]) => {
+ const defs: NodeDef[] = [];
+ for (const cls of nodes) {
+ defs.push(new cls().descriptor);
+ }
+ defs.sort((a, b) => a.name.localeCompare(b.name));
+ let str = JSON.stringify(defs, null, 2);
+ str = str.replace(/"doc": "\\n +/g, '"doc": "');
+ str = str.replace(/\\n +/g, "\\n");
+ return str;
+};
+
+export const zhNodeDef = () => {
+ return toString([
+ zh.action.Clear,
+ zh.action.Log,
+ zh.action.Now,
+ zh.action.Wait,
+ zh.composite.Foreach,
+ zh.composite.Loop,
+ zh.composite.Parallel,
+ zh.composite.Selector,
+ zh.composite.Sequence,
+ zh.condition.Check,
+ zh.condition.IsNull,
+ zh.condition.NotNull,
+ zh.decorator.AlwaysFail,
+ zh.decorator.AlwaysSuccess,
+ zh.decorator.Inverter,
+ zh.decorator.Once,
+ zh.decorator.RepeatUntilFailure,
+ zh.decorator.RepeatUntilSuccess,
+ ]);
+};