fix node layout error and support subtree

This commit is contained in:
codetypes committed 2024-03-11 09:35:39 +08:00
1 parent d3c5bf53e7
commit 258a006383
21 files changed
+6019 -11457

No files matched your search

+3 -3
View File
@@ -1,3 +1,3 @@
registry=https://registry.npm.taobao.org/ # registry=https://registry.npm.taobao.org/
ELECTRON_MIRROR=https://registry.npmmirror.com/-/binary/electron/ # ELECTRON_MIRROR=https://registry.npmmirror.com/-/binary/electron/
ELECTRON_BUILDER_BINARIES_MIRROR=https://registry.npmmirror.com/-/binary/electron-builder-binaries/ # ELECTRON_BUILDER_BINARIES_MIRROR=https://registry.npmmirror.com/-/binary/electron-builder-binaries/
+14
View File
@@ -0,0 +1,14 @@
{
"folders": [
{
"path": "."
}
],
"settings": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"[json]": {
"editor.formatOnSave": false
}
}
}
+6 -3
View File
@@ -1,8 +1,8 @@
import { TreeGraphData } from "@antv/g6/lib/types"; import { TreeGraphData } from "@antv/g6/lib/types";
export interface ArgsOptional{ export interface ArgsOptional {
name:string; name: string;
value:string|number; value: string | number;
} }
export interface ArgsDefType { export interface ArgsDefType {
@@ -31,6 +31,7 @@ export interface BehaviorNodeModel {
output?: string[]; output?: string[];
children?: BehaviorNodeModel[]; children?: BehaviorNodeModel[];
debug?: boolean; debug?: boolean;
path?: string;
} }
export interface BehaviorTreeModel { export interface BehaviorTreeModel {
@@ -48,6 +49,8 @@ export interface GraphNodeModel extends TreeGraphData {
children?: GraphNodeModel[]; children?: GraphNodeModel[];
conf: BehaviorNodeTypeModel; conf: BehaviorNodeTypeModel;
debug?: boolean; debug?: boolean;
parent?: string;
path?: string;
size?: number[]; size?: number[];
} }
+1
View File
@@ -9,6 +9,7 @@ export default {
COPY_NODE: "COPY_NODE", COPY_NODE: "COPY_NODE",
PASTE_NODE: "PASTE_NODE", PASTE_NODE: "PASTE_NODE",
SAVE_ALL: "SAVE_ALL", SAVE_ALL: "SAVE_ALL",
BUILD: "BUILD",
SAVE: "SAVE", SAVE: "SAVE",
UNDO: "UNDO", UNDO: "UNDO",
REDO: "REDO", REDO: "REDO",
+93 -42
View File
@@ -1,13 +1,27 @@
import { TreeGraphData } from "@antv/g6/lib/types"; import { TreeGraph } from "@antv/g6";
import { BehaviorNodeModel, BehaviorTreeModel, GraphNodeModel } from "./BehaviorTreeModel";
import Settings from "../main-process/Settings";
import * as path from "path"; import * as path from "path";
import * as fs from "fs";
import Settings from "../main-process/Settings";
import { BehaviorNodeModel, BehaviorTreeModel, GraphNodeModel } from "./BehaviorTreeModel";
import { message } from "antd";
interface INodeDataLike {
path?: string;
id: string | number;
}
export const isSubtree = (nodeData: INodeDataLike) => {
return nodeData.path && nodeData.id.toString() !== "1";
};
const parsingStack: string[] = [];
export const cloneNodeData = (nodeData: GraphNodeModel) => { export const cloneNodeData = (nodeData: GraphNodeModel) => {
const newData: BehaviorNodeModel = { const newData: BehaviorNodeModel = {
id: Number(nodeData.id), id: Number(nodeData.id),
name: nodeData.name, name: nodeData.name,
desc: nodeData.desc, desc: nodeData.desc,
path: nodeData.path,
}; };
if (nodeData.input) { if (nodeData.input) {
newData.input = []; newData.input = [];
@@ -28,8 +42,8 @@ export const cloneNodeData = (nodeData: GraphNodeModel) => {
newData.args[k] = v; newData.args[k] = v;
} }
} }
if (nodeData.children) { if (nodeData.children && !isSubtree(nodeData)) {
newData.children = [] newData.children = [];
for (let child of nodeData.children) { for (let child of nodeData.children) {
newData.children.push(cloneNodeData(child)); newData.children.push(cloneNodeData(child));
} }
@@ -44,6 +58,7 @@ export const refreshNodeId = (nodeData: GraphNodeModel, id?: number) => {
nodeData.id = (id++).toString(); nodeData.id = (id++).toString();
if (nodeData.children) { if (nodeData.children) {
nodeData.children.forEach((child) => { nodeData.children.forEach((child) => {
child.parent = nodeData.id;
id = refreshNodeId(child, id); id = refreshNodeId(child, id);
}); });
} }
@@ -51,21 +66,24 @@ export const refreshNodeId = (nodeData: GraphNodeModel, id?: number) => {
}; };
export const calcTreeNodeSize = (treeNode: GraphNodeModel) => { export const calcTreeNodeSize = (treeNode: GraphNodeModel) => {
var height = 40; var height = 40 + 2;
const updateHeight = (obj: any) => { const updateHeight = (obj: any) => {
if (Array.isArray(obj) || (obj && Object.keys(obj).length > 0)) { if (Array.isArray(obj) || (obj && Object.keys(obj).length > 0)) {
const { str, line } = toBreakWord(`参数:${JSON.stringify(obj)}`, 35); const { str, line } = toBreakWord(`参数:${JSON.stringify(obj)}`, 35);
height += 20 * line; height += 20 * line;
} }
}; };
if (treeNode.path) {
height += 20;
}
updateHeight(treeNode.args); updateHeight(treeNode.args);
updateHeight(treeNode.input); updateHeight(treeNode.input);
updateHeight(treeNode.output); updateHeight(treeNode.output);
return [200, height]; return [200, height];
}; };
export const createTreeData = (bNode: BehaviorNodeModel, settings: Settings) => { export const createTreeData = (bNode: BehaviorNodeModel, settings: Settings, parent?: string) => {
const treeData: GraphNodeModel = { let treeData: GraphNodeModel = {
id: bNode.id.toString(), id: bNode.id.toString(),
name: bNode.name, name: bNode.name,
desc: bNode.desc, desc: bNode.desc,
@@ -74,19 +92,60 @@ export const createTreeData = (bNode: BehaviorNodeModel, settings: Settings) =>
output: bNode.output, output: bNode.output,
debug: bNode.debug, debug: bNode.debug,
conf: settings.getNodeConf(bNode.name), conf: settings.getNodeConf(bNode.name),
parent: parent,
}; };
treeData.size = calcTreeNodeSize(treeData); treeData.size = calcTreeNodeSize(treeData);
if (!parent) {
parsingStack.length = 0;
}
if (bNode.children) { if (bNode.children) {
treeData.children = []; treeData.children = [];
bNode.children.forEach((child) => { bNode.children.forEach((child) => {
treeData.children.push(createTreeData(child, settings)); treeData.children.push(createTreeData(child, settings, treeData.id));
}); });
} else if (bNode.path) {
if (parsingStack.indexOf(bNode.path) >= 0) {
treeData.path = bNode.path;
treeData.size = calcTreeNodeSize(treeData);
message.error(`循环引用节点:${bNode.path}`, 4);
return treeData;
}
parsingStack.push(bNode.path);
try {
const subtreePath = settings.workdir + "/" + bNode.path;
const str = fs.readFileSync(subtreePath, "utf8");
console.log("read subtree:", subtreePath);
treeData = createTreeData(JSON.parse(str).root, settings, treeData.id);
treeData.path = bNode.path;
treeData.size = calcTreeNodeSize(treeData);
} catch (error) {
console.log("parse subtree:", error);
}
parsingStack.pop();
} }
calcTreeNodeSize(treeData); calcTreeNodeSize(treeData);
return treeData; return treeData;
}; };
export const createFileData = (gNode: GraphNodeModel) => { export const createBuildData = (path: string, settings: Settings): BehaviorNodeModel | null => {
try {
path = settings.workdir + "/" + path;
const str = fs.readFileSync(path, "utf8");
let treeModel = JSON.parse(str);
const data = createTreeData(treeModel.root, settings);
refreshNodeId(data);
treeModel.root = createFileData(data, true);
return treeModel as BehaviorNodeModel;
} catch (error) {
console.log("build error:", path, error);
}
return null;
};
export const createFileData = (gNode: GraphNodeModel, includeSubtree?: boolean) => {
const nodeData: BehaviorNodeModel = { const nodeData: BehaviorNodeModel = {
id: Number(gNode.id), id: Number(gNode.id),
name: gNode.name, name: gNode.name,
@@ -95,45 +154,37 @@ export const createFileData = (gNode: GraphNodeModel) => {
input: gNode.input || undefined, input: gNode.input || undefined,
output: gNode.output || undefined, output: gNode.output || undefined,
debug: gNode.debug, debug: gNode.debug,
path: gNode.path,
}; };
if (gNode.children) { if (gNode.children && (includeSubtree || !isSubtree(gNode))) {
nodeData.children = []; nodeData.children = [];
gNode.children.forEach((child) => { gNode.children.forEach((child) => {
nodeData.children.push(createFileData(child)); nodeData.children.push(createFileData(child, includeSubtree));
}); });
} }
return nodeData; return nodeData;
}; };
export const findParent = (node: TreeGraphData, id: string): TreeGraphData | null => { export const findParent = (graph: TreeGraph, node: GraphNodeModel) => {
if (node.children) { if (node.parent) {
for (let child of node.children) { return graph.findDataById(node.parent);
if (child.id == id) { } else {
return node; return null;
} else {
let parent = findParent(child, id);
if (parent) {
return parent;
}
}
}
} }
return null;
}; };
export const findFromAllChildren = (node: TreeGraphData, id: string): TreeGraphData | null => { export const isAncestor = (
if (node.id == id) { graph: TreeGraph,
return node; ancestor: GraphNodeModel,
node: GraphNodeModel
): boolean => {
if (ancestor.id === node.parent) {
return true;
} else if (node.parent) {
return isAncestor(graph, ancestor, graph.findDataById(node.parent) as GraphNodeModel);
} else {
return false;
} }
if (node.children) {
for (let child of node.children) {
let found = findFromAllChildren(child, id);
if (found) {
return found;
}
}
}
return null;
}; };
export const getRemoteSettings = () => { export const getRemoteSettings = () => {
@@ -156,20 +207,20 @@ export const createNewTree = (filename: string) => {
return tree; return tree;
}; };
export const toBreakWord = (str: string, len: number, char='\n') => { export const toBreakWord = (str: string, len: number, char = "\n") => {
var strTemp = ""; var strTemp = "";
var line = 1; var line = 1;
if(str.length <= len) { if (str.length <= len) {
return {str, line}; return { str, line };
} }
while (str.length > len) { while (str.length > len) {
strTemp += str.substr(0, len) + char; strTemp += str.substr(0, len) + char;
str = str.substr(len, str.length); str = str.substr(len, str.length);
line ++; line++;
} }
strTemp += str; strTemp += str;
return { return {
str: strTemp, str: strTemp,
line, line,
}; };
} };
+32 -32
View File
@@ -54,7 +54,7 @@ export default class AppMenu {
submenu: [ submenu: [
{ {
label: "新建", label: "新建",
accelerator: "ctrl+n", accelerator: "CommandOrControl+n",
click: () => { click: () => {
(async () => { (async () => {
const res = await dialog.showSaveDialog({ const res = await dialog.showSaveDialog({
@@ -74,7 +74,7 @@ export default class AppMenu {
}, },
{ {
label: "打开文件", label: "打开文件",
accelerator: "Ctrl+O", accelerator: "CommandOrControl+O",
click: () => { click: () => {
(async () => { (async () => {
const res = await dialog.showOpenDialog({ const res = await dialog.showOpenDialog({
@@ -102,14 +102,14 @@ export default class AppMenu {
{ type: "separator" }, { type: "separator" },
{ {
label: "保存", label: "保存",
accelerator: "ctrl+s", accelerator: "CommandOrControl+s",
click: () => { click: () => {
this.webContents.send(MainEventType.SAVE); this.webContents.send(MainEventType.SAVE);
}, },
}, },
{ {
label: "全部保存", label: "全部保存",
accelerator: "ctrl+shift+s", accelerator: "CommandOrControl+shift+s",
click: () => { click: () => {
this.webContents.send(MainEventType.SAVE_ALL); this.webContents.send(MainEventType.SAVE_ALL);
}, },
@@ -136,6 +136,13 @@ export default class AppMenu {
})(); })();
}, },
}, },
{
label: "构建",
accelerator: "CommandOrControl+b",
click: () => {
this.webContents.send(MainEventType.BUILD);
},
},
], ],
}); });
} }
@@ -146,29 +153,29 @@ export default class AppMenu {
submenu: [ submenu: [
{ {
label: "撤销", label: "撤销",
accelerator: "ctrl+z", accelerator: "CommandOrControl+z",
click: () => { click: () => {
this.webContents.send(MainEventType.UNDO); this.webContents.send(MainEventType.UNDO);
} },
}, },
{ {
label: "恢复", label: "恢复",
accelerator: "ctrl+y", accelerator: "CommandOrControl+y",
click: () => { click: () => {
this.webContents.send(MainEventType.REDO); this.webContents.send(MainEventType.REDO);
} },
}, },
{ type: "separator" }, { type: "separator" },
{ {
label: "新建节点", label: "新建节点",
accelerator: "insert", accelerator: process.platform === "darwin" ? "enter" : "insert",
click: () => { click: () => {
this.webContents.send(MainEventType.CREATE_NODE, "unknow"); this.webContents.send(MainEventType.CREATE_NODE, "unknow");
}, },
}, },
{ {
label: "删除节点", label: "删除节点",
accelerator: "delete", accelerator: process.platform === "darwin" ? "backspace" : "delete",
click: () => { click: () => {
this.webContents.send(MainEventType.DELETE_NODE); this.webContents.send(MainEventType.DELETE_NODE);
}, },
@@ -176,21 +183,14 @@ export default class AppMenu {
{ type: "separator" }, { type: "separator" },
{ {
label: "复制节点", label: "复制节点",
accelerator: "ctrl+c",
role: "copy", role: "copy",
click: () => {
this.webContents.send(MainEventType.COPY_NODE);
},
}, },
{ {
label: "粘贴节点", label: "粘贴节点",
accelerator: "ctrl+v", role: "paste",
click: () => {
this.webContents.send(MainEventType.PASTE_NODE);
},
}, },
] ],
}) });
} }
private createWorkspaceMenu() { private createWorkspaceMenu() {
@@ -277,7 +277,7 @@ export default class AppMenu {
{ type: "separator" }, { type: "separator" },
{ {
label: "打开目录", label: "打开目录",
accelerator: "Ctrl+Shift+O", accelerator: "CommandOrControl+Shift+O",
click: () => { click: () => {
if (!this.settings.curWorkspace.getNodeConfPath()) { if (!this.settings.curWorkspace.getNodeConfPath()) {
dialog.showErrorBox("警告", "请先指定节点定义配置!"); dialog.showErrorBox("警告", "请先指定节点定义配置!");
@@ -341,23 +341,23 @@ export default class AppMenu {
for (let model of this.settings.curWorkspace.getServers()) { for (let model of this.settings.curWorkspace.getServers()) {
serverItems.push({ serverItems.push({
label: `${model.name} ${model.host}`, label: `${model.name} ${model.host}`,
type: 'checkbox', type: "checkbox",
checked: curServerName == model.name, checked: curServerName == model.name,
click: () => { click: () => {
this.settings.serverName = model.name; this.settings.serverName = model.name;
this.mainProcess.rebuildMenu(); this.mainProcess.rebuildMenu();
} },
}) });
} }
return new MenuItem({ return new MenuItem({
label: "开发工具", label: "开发工具",
submenu: [ submenu: [
{ {
label: "热更", label: "热更",
accelerator: "Ctrl+R", accelerator: "CommandOrControl+R",
click: () => { click: () => {
this.webContents.send(MainEventType.RELOAD_SERVER); this.webContents.send(MainEventType.RELOAD_SERVER);
} },
}, },
{ {
label: "联调服务器", label: "联调服务器",
@@ -375,14 +375,14 @@ export default class AppMenu {
this.webContents.send(MainEventType.BATCH_EXEC, res.filePaths[0]); this.webContents.send(MainEventType.BATCH_EXEC, res.filePaths[0]);
} }
})(); })();
} },
}, },
{ {
type: 'separator', type: "separator",
}, },
{ {
label: "打开控制台", label: "打开控制台",
accelerator: "Ctrl+Shift+I", accelerator: "CommandOrControl+Shift+I",
click: (_, browserWindow) => { click: (_, browserWindow) => {
browserWindow.webContents.toggleDevTools(); browserWindow.webContents.toggleDevTools();
}, },
@@ -394,11 +394,11 @@ export default class AppMenu {
}, },
}, },
{ {
type: 'separator', type: "separator",
}, },
{ {
label: `当前版本:${packageConf.version}` label: `当前版本:${packageConf.version}`,
} },
], ],
}); });
} }
+4 -4
View File
@@ -18,10 +18,10 @@ export class MainProcess {
nativeTheme.themeSource = "dark"; nativeTheme.themeSource = "dark";
app.on("ready", () => { app.on("ready", () => {
this.createWindow(); this.createWindow();
electronLocalshortcut.register(this.mainWindow, "Ctrl+C", () => { electronLocalshortcut.register(this.mainWindow, "CommandOrControl+C", () => {
this.mainWindow.webContents.send(MainEventType.COPY_NODE); this.mainWindow.webContents.send(MainEventType.COPY_NODE);
}); });
electronLocalshortcut.register(this.mainWindow, "Ctrl+V", () => { electronLocalshortcut.register(this.mainWindow, "CommandOrControl+V", () => {
this.mainWindow.webContents.send(MainEventType.PASTE_NODE); this.mainWindow.webContents.send(MainEventType.PASTE_NODE);
}); });
}); });
@@ -46,12 +46,12 @@ export class MainProcess {
height: 800, height: 800,
webPreferences: { webPreferences: {
nodeIntegration: true, nodeIntegration: true,
contextIsolation:false, contextIsolation: false,
}, },
// fullscreenable:false, // fullscreenable:false,
// maximizable:false // maximizable:false
}); });
require("@electron/remote/main").initialize(); require("@electron/remote/main").initialize();
require("@electron/remote/main").enable(this.mainWindow.webContents); require("@electron/remote/main").enable(this.mainWindow.webContents);
this.mainWindow.maximize(); this.mainWindow.maximize();
+20 -8
View File
@@ -1,5 +1,6 @@
import * as fs from "fs"; import * as fs from "fs";
import Workspace from "./Workspace"; import Workspace from "./Workspace";
import { app } from "electron";
export interface BehaviorNodeClassify { export interface BehaviorNodeClassify {
classify: string; classify: string;
@@ -14,7 +15,10 @@ export interface SettingsModel {
treesDesc?: { [name: string]: string }; treesDesc?: { [name: string]: string };
} }
const settingPath = "settings.json"; const settingPath = app.getPath("userData") + "/settings.json";
console.log("setting path:", settingPath);
const sampleNodeClassify: BehaviorNodeClassify[] = [ const sampleNodeClassify: BehaviorNodeClassify[] = [
{ classify: "Composite", desc: "复合节点" }, { classify: "Composite", desc: "复合节点" },
{ classify: "Decorator", desc: "修饰节点" }, { classify: "Decorator", desc: "修饰节点" },
@@ -35,7 +39,7 @@ export default class Settings {
fs.writeFileSync(settingPath, JSON.stringify(this.model, null, 2)); fs.writeFileSync(settingPath, JSON.stringify(this.model, null, 2));
this.dirty = false; this.dirty = false;
} }
}, 1000) }, 1000);
} }
get nodeConfPath() { get nodeConfPath() {
@@ -54,7 +58,7 @@ export default class Settings {
return this.model.nodeClassify; return this.model.nodeClassify;
} }
get serverModel() { get serverModel() {
const servers = this.curWorkspace.getServers() const servers = this.curWorkspace.getServers();
if (!servers) { if (!servers) {
return; return;
} }
@@ -66,7 +70,7 @@ export default class Settings {
return servers[0]; return servers[0];
} }
get serverName() { get serverName() {
const servers = this.curWorkspace.getServers() const servers = this.curWorkspace.getServers();
if (!servers) { if (!servers) {
return ""; return "";
} }
@@ -78,7 +82,7 @@ export default class Settings {
if (servers[0]) { if (servers[0]) {
return servers[0].name; return servers[0].name;
} else { } else {
return '' return "";
} }
} }
set serverName(name: string) { set serverName(name: string) {
@@ -119,10 +123,18 @@ export default class Settings {
this.dirty = true; this.dirty = true;
} }
get workdir() {
return this.curWorkspace.getWorkdir();
}
getNodeConf(name: string) { getNodeConf(name: string) {
return this.curWorkspace.getNodeConf(name); return this.curWorkspace.getNodeConf(name);
} }
hasNodeConf(name: string) {
return this.curWorkspace.hasNodeConf(name);
}
pushRecentWorkspace(path: string) { pushRecentWorkspace(path: string) {
var list = this.model.recentWorkspaces; var list = this.model.recentWorkspaces;
if (list.indexOf(path) >= 0) { if (list.indexOf(path) >= 0) {
@@ -137,12 +149,12 @@ export default class Settings {
} }
getTreeDesc(name: string) { getTreeDesc(name: string) {
const key = this.curWorkspace.getFilepath() + ' ' + name; const key = this.curWorkspace.getFilepath() + " " + name;
var desc = this.model.treesDesc[key]; var desc = this.model.treesDesc[key];
if (!desc) { if (!desc) {
const str = fs.readFileSync(name, "utf8"); const str = fs.readFileSync(name, "utf8");
const tree = JSON.parse(str); const tree = JSON.parse(str);
desc = tree.desc ? tree.desc : ''; desc = tree.desc ? tree.desc : "";
this.model.treesDesc[key] = desc; this.model.treesDesc[key] = desc;
this.save(); this.save();
} }
@@ -150,7 +162,7 @@ export default class Settings {
} }
setTreeDesc(name: string, desc?: string) { setTreeDesc(name: string, desc?: string) {
const key = this.curWorkspace.getFilepath() + ' ' + name; const key = this.curWorkspace.getFilepath() + " " + name;
this.model.treesDesc[key] = desc; this.model.treesDesc[key] = desc;
} }
} }
+7 -3
View File
@@ -43,8 +43,8 @@ export default class Workspace {
const model = JSON.parse(str) as WorkspaceModel; const model = JSON.parse(str) as WorkspaceModel;
if (model.isRelative) { if (model.isRelative) {
const root = path.dirname(this.filepath); const root = path.dirname(this.filepath);
this.nodeConfPath = path.join(root,model.nodeConfPath); this.nodeConfPath = path.join(root, model.nodeConfPath);
this.workdir = path.join(root,model.workdir); this.workdir = path.join(root, model.workdir);
} else { } else {
this.nodeConfPath = model.nodeConfPath; this.nodeConfPath = model.nodeConfPath;
this.workdir = model.workdir; this.workdir = model.workdir;
@@ -86,7 +86,7 @@ export default class Workspace {
this.workdir = workdir; this.workdir = workdir;
} }
getServers() { getServers() {
if(this.model && this.model.servers) { if (this.model && this.model.servers) {
return this.model.servers; return this.model.servers;
} else { } else {
return []; return [];
@@ -154,4 +154,8 @@ export default class Workspace {
getNodeConf(name: string) { getNodeConf(name: string) {
return this.name2conf[name] || unknowNodeType; return this.name2conf[name] || unknowNodeType;
} }
hasNodeConf(name: string) {
return !!this.name2conf[name];
}
} }
+5410 -11217
View File
File diff suppressed because it is too large. Load diff
+22 -20
View File
@@ -1,12 +1,13 @@
{ {
"name": "behavior3editor", "name": "behavior3editor",
"version": "0.1.11", "version": "0.2.0",
"description": "行为树编辑器", "description": "行为树编辑器",
"main": "./dist/main.js", "main": "./dist/main.js",
"scripts": { "scripts": {
"prestart": "webpack --config webpack.config.js", "prestart": "webpack --config webpack.config.js",
"start": "electron .", "start": "electron .",
"build": "electron-packager ./ behavior3editor --platform=win32 --arch=x64 --out=./build --overwrite", "build": "electron-packager ./ behavior3editor --platform=win32 --arch=x64 --out=./build --overwrite",
"build-mac": "electron-packager ./ behavior3editor --platform=darwin --arch=universal --out=./build --overwrite",
"dist": "npm run prestart && electron-builder --dir && electron-builder --win --x64" "dist": "npm run prestart && electron-builder --dir && electron-builder --win --x64"
}, },
"repository": { "repository": {
@@ -26,30 +27,31 @@
}, },
"homepage": "https://github.com/zhandouxiaojiji/behavior3editor#readme", "homepage": "https://github.com/zhandouxiaojiji/behavior3editor#readme",
"devDependencies": { "devDependencies": {
"@types/electron-localshortcut": "^3.1.0", "@types/electron-localshortcut": "^3.1.3",
"@types/node": "^14.14.8", "@types/glob": "^8.1.0",
"@types/rimraf": "^3.0.0", "@types/node": "^20.11.25",
"copy-webpack-plugin": "^6.3.1", "@types/rimraf": "^3.0.2",
"css-loader": "^4.3.0", "copy-webpack-plugin": "^12.0.2",
"electron": "^20.0.3", "css-loader": "^6.10.0",
"electron": "^29.1.1",
"electron-builder": "^24.13.3",
"electron-localshortcut": "^3.2.1", "electron-localshortcut": "^3.2.1",
"electron-packager": "^15.5.2", "electron-packager": "^17.1.2",
"electron-builder": "^23.3.3", "file-loader": "^6.2.0",
"file-loader": "^6.1.1", "less": "^4.2.0",
"less": "^3.12.2", "less-loader": "^12.2.0",
"less-loader": "^7.1.0", "node-fetch": "^3.3.2",
"node-fetch": "^2.6.1", "source-map-loader": "^5.0.0",
"source-map-loader": "^1.1.0", "style-loader": "^3.3.4",
"style-loader": "^2.0.0", "ts-loader": "^9.5.1",
"ts-loader": "^8.0.4", "typescript": "^5.3.3",
"typescript": "^4.7.4", "webpack": "^5.90.3",
"webpack": "^4.44.2", "webpack-cli": "^5.1.4"
"webpack-cli": "^3.3.12"
}, },
"dependencies": { "dependencies": {
"@ant-design/icons": "^4.3.0", "@ant-design/icons": "^4.3.0",
"@antv/g6": "^3.8.5", "@antv/g6": "^3.8.5",
"@electron/remote": "^2.0.8", "@electron/remote": "^2.1.2",
"@types/react": "^16.9.51", "@types/react": "^16.9.51",
"@types/react-dom": "^16.9.8", "@types/react-dom": "^16.9.8",
"antd": "^4.6.6", "antd": "^4.6.6",
+21 -9
View File
@@ -207,20 +207,32 @@ export default class NodePanel extends React.Component<NodePanelProps> {
const required = e.type.indexOf("?") == -1; const required = e.type.indexOf("?") == -1;
if (e.type.indexOf("string") >= 0) { if (e.type.indexOf("string") >= 0) {
return <Input onBlur={this.handleSubmit} />; return <Input onBlur={this.handleSubmit} />;
} else if (e.type.indexOf("int") >= 0 || e.type.indexOf("float") >= 0) { } else if (e.type.indexOf("int") >= 0) {
return (
<InputNumber
precision={0}
style={{ width: "100%" }}
onBlur={this.handleSubmit}
/>
);
} else if (e.type.indexOf("float") >= 0) {
return <InputNumber style={{ width: "100%" }} onBlur={this.handleSubmit} />; return <InputNumber style={{ width: "100%" }} onBlur={this.handleSubmit} />;
} else if (e.type.indexOf("boolean") >= 0) { } else if (e.type.indexOf("boolean") >= 0) {
return <Switch onChange={this.handleSubmit} />; return <Switch onChange={this.handleSubmit} />;
} else if (e.type.indexOf("code") >= 0) { } else if (e.type.indexOf("code") >= 0) {
return <Input onBlur={this.handleSubmit} placeholder={"表达式"} />; return <Input onBlur={this.handleSubmit} placeholder={"表达式"} />;
} else if (e.type.indexOf("enum") >= 0 ) { } else if (e.type.indexOf("enum") >= 0) {
return <Select style={{ width: 120 }} onChange={this.handleSubmit} > return (
{ <Select style={{ width: 120 }} onChange={this.handleSubmit}>
e.options.map((e)=>{ {e.options.map((e) => {
return (<Option key={e.name} value={e.value}>{e.name}</Option>) return (
}) <Option key={e.name} value={e.value}>
} {e.name}
</Select>; </Option>
);
})}
</Select>
);
} }
}; };
+107 -48
View File
@@ -1,22 +1,22 @@
import * as React from "react";
import * as fs from "fs";
import * as path from "path";
import { Row, Col, message, Card } from "antd";
import NodePanel from "./NodePanel";
import G6, { TreeGraph } from "@antv/g6"; import G6, { TreeGraph } from "@antv/g6";
import { G6GraphEvent } from "@antv/g6/lib/interface/behavior"; import { G6GraphEvent } from "@antv/g6/lib/interface/behavior";
import * as Utils from "../../common/Utils"; import { Col, Row, message } from "antd";
import * as fs from "fs";
import * as path from "path";
import * as React from "react";
import { import {
BehaviorNodeModel,
BehaviorTreeModel, BehaviorTreeModel,
GraphNodeModel, GraphNodeModel,
BehaviorNodeModel,
} from "../../common/BehaviorTreeModel"; } from "../../common/BehaviorTreeModel";
import TreePanel from "./TreePanel"; import * as Utils from "../../common/Utils";
import Settings from "../../main-process/Settings"; import Settings from "../../main-process/Settings";
import NodePanel from "./NodePanel";
import TreePanel from "./TreePanel";
import "./Editor.css"; import { Matrix } from "@antv/g6/lib/types";
import { clipboard } from "electron"; import { clipboard } from "electron";
import { Matrix } from '@antv/g6/lib/types'; import "./Editor.css";
export interface EditorProps { export interface EditorProps {
filepath: string; filepath: string;
@@ -26,7 +26,7 @@ export interface EditorProps {
interface EditorState { interface EditorState {
curNodeId?: string; curNodeId?: string;
blockNodeSelectChange?: boolean; blockNodeSelectChange?: boolean;
viewportMatrix?: Matrix viewportMatrix?: Matrix;
} }
export default class Editor extends React.Component<EditorProps, EditorState> { export default class Editor extends React.Component<EditorProps, EditorState> {
@@ -34,6 +34,7 @@ export default class Editor extends React.Component<EditorProps, EditorState> {
state: EditorState = {}; state: EditorState = {};
private graph: TreeGraph; private graph: TreeGraph;
private isInEditor: boolean = false;
private dragSrcId: string; private dragSrcId: string;
private dragDstId: string; private dragDstId: string;
private autoId: number; private autoId: number;
@@ -56,7 +57,9 @@ export default class Editor extends React.Component<EditorProps, EditorState> {
} }
shouldComponentUpdate(nextProps: EditorProps, nextState: EditorState) { shouldComponentUpdate(nextProps: EditorProps, nextState: EditorState) {
return this.props.filepath != nextProps.filepath || this.state.curNodeId != nextState.curNodeId; return (
this.props.filepath != nextProps.filepath || this.state.curNodeId != nextState.curNodeId
);
} }
componentDidMount() { componentDidMount() {
@@ -118,15 +121,23 @@ export default class Editor extends React.Component<EditorProps, EditorState> {
}); });
graph.on("viewportchange", (data: any) => { graph.on("viewportchange", (data: any) => {
if (data.action == 'translate' || data.action == 'zoom') { if (data.action == "translate" || data.action == "zoom") {
this.state.viewportMatrix = data.matrix this.state.viewportMatrix = data.matrix;
} }
}) });
graph.on("contextmenu", (e: G6GraphEvent) => { graph.on("contextmenu", (e: G6GraphEvent) => {
require("@electron/remote").Menu.getApplicationMenu().popup(); require("@electron/remote").Menu.getApplicationMenu().popup();
}); });
graph.on("canvas:mouseover", () => {
this.isInEditor = true;
});
graph.on("canvas:mouseleave", () => {
this.isInEditor = false;
});
graph.on("node:mouseenter", (e: G6GraphEvent) => { graph.on("node:mouseenter", (e: G6GraphEvent) => {
const { item } = e; const { item } = e;
if (item.hasState("selected")) { if (item.hasState("selected")) {
@@ -146,7 +157,7 @@ export default class Editor extends React.Component<EditorProps, EditorState> {
graph.on("nodeselectchange", (e: G6GraphEvent) => { graph.on("nodeselectchange", (e: G6GraphEvent) => {
if (this.state.blockNodeSelectChange) { if (this.state.blockNodeSelectChange) {
// ** 重置选中效果 // ** 重置选中效果
this.onSelectNode(this.state.curNodeId) this.onSelectNode(this.state.curNodeId);
return; return;
} }
if (e.target) { if (e.target) {
@@ -237,16 +248,16 @@ export default class Editor extends React.Component<EditorProps, EditorState> {
} }
const rootData = graph.findDataById("1"); const rootData = graph.findDataById("1");
const srcData = graph.findDataById(srcNodeId); const srcData = graph.findDataById(srcNodeId) as GraphNodeModel;
const srcParent = Utils.findParent(rootData, srcNodeId); const srcParent = Utils.findParent(this.graph, srcData);
const dstData = graph.findDataById(dstNode.getID()); const dstData = graph.findDataById(dstNode.getID()) as GraphNodeModel;
const dstParent = Utils.findParent(rootData, dstNode.getID()); const dstParent = Utils.findParent(this.graph, dstData);
if (!srcParent) { if (!srcParent) {
console.log("no parent!"); console.log("no parent!");
return; return;
} }
if (Utils.findFromAllChildren(srcData, dstData.id)) { if (Utils.isAncestor(this.graph, srcData, dstData)) {
// 不能将父节点拖到自已的子孙节点 // 不能将父节点拖到自已的子孙节点
console.log("cannot move to child"); console.log("cannot move to child");
return; return;
@@ -262,6 +273,7 @@ export default class Editor extends React.Component<EditorProps, EditorState> {
if (!dstData.children) { if (!dstData.children) {
dstData.children = []; dstData.children = [];
} }
srcData.parent = dstData.id;
dstData.children.push(srcData); dstData.children.push(srcData);
} else if (dragDir == "dragUp") { } else if (dragDir == "dragUp") {
if (!dstParent) { if (!dstParent) {
@@ -269,6 +281,7 @@ export default class Editor extends React.Component<EditorProps, EditorState> {
} }
removeSrc(); removeSrc();
const idx = dstParent.children.findIndex((e) => e.id == dstData.id); const idx = dstParent.children.findIndex((e) => e.id == dstData.id);
srcData.parent = dstParent.id;
dstParent.children.splice(idx, 0, srcData); dstParent.children.splice(idx, 0, srcData);
} else if (dragDir == "dragDown") { } else if (dragDir == "dragDown") {
if (!dstParent) { if (!dstParent) {
@@ -276,6 +289,7 @@ export default class Editor extends React.Component<EditorProps, EditorState> {
} }
removeSrc(); removeSrc();
const idx = dstParent.children.findIndex((e) => e.id == dstData.id); const idx = dstParent.children.findIndex((e) => e.id == dstData.id);
srcData.parent = dstParent.id;
dstParent.children.splice(idx + 1, 0, srcData); dstParent.children.splice(idx + 1, 0, srcData);
} else { } else {
return; return;
@@ -309,6 +323,7 @@ export default class Editor extends React.Component<EditorProps, EditorState> {
if (this.state.curNodeId) { if (this.state.curNodeId) {
graph.setItemState(this.state.curNodeId, "selected", false); graph.setItemState(this.state.curNodeId, "selected", false);
graph.setItemState(this.state.curNodeId, "hover", false);
} }
this.setState({ curNodeId }); this.setState({ curNodeId });
@@ -318,6 +333,10 @@ export default class Editor extends React.Component<EditorProps, EditorState> {
} }
createNode(name: string) { createNode(name: string) {
if (!this.isInEditor) {
return;
}
console.log("editor create node", name); console.log("editor create node", name);
const { curNodeId } = this.state; const { curNodeId } = this.state;
if (!curNodeId) { if (!curNodeId) {
@@ -333,7 +352,7 @@ export default class Editor extends React.Component<EditorProps, EditorState> {
if (!curNodeData.children) { if (!curNodeData.children) {
curNodeData.children = []; curNodeData.children = [];
} }
curNodeData.children.push(Utils.createTreeData(newNodeData, this.settings)); curNodeData.children.push(Utils.createTreeData(newNodeData, this.settings, curNodeId));
this.changeWithoutAnim(); this.changeWithoutAnim();
} }
@@ -344,15 +363,17 @@ export default class Editor extends React.Component<EditorProps, EditorState> {
return; return;
} }
if (curNodeId == "1") { if (curNodeId === "1") {
message.warn("根节点不能删除!"); message.warn("根节点不能删除!");
return; return;
} }
this.onSelectNode(null); this.onSelectNode(null);
this.pushUndoStack(); this.pushUndoStack();
const rootData = this.graph.findDataById("1"); const parentData = Utils.findParent(
const parentData = Utils.findParent(rootData, curNodeId); this.graph,
this.graph.findDataById(curNodeId) as GraphNodeModel
);
parentData.children = parentData.children.filter((e) => e.id != curNodeId); parentData.children = parentData.children.filter((e) => e.id != curNodeId);
this.changeWithoutAnim(); this.changeWithoutAnim();
} }
@@ -380,20 +401,35 @@ export default class Editor extends React.Component<EditorProps, EditorState> {
root, root,
desc: this.treeModel.desc, desc: this.treeModel.desc,
} as BehaviorTreeModel; } as BehaviorTreeModel;
fs.writeFileSync( fs.writeFileSync(filepath, JSON.stringify(treeModel, null, 2));
filepath,
JSON.stringify(treeModel, null, 2)
);
this.props.onChangeSaveState(false); this.props.onChangeSaveState(false);
this.unsave = false; this.unsave = false;
const treeData = Utils.createTreeData(root, this.settings);
this.autoId = Utils.refreshNodeId(treeData);
this.graph.set("animate", false); this.graph.set("animate", false);
this.graph.changeData(Utils.createTreeData(root, this.settings)); this.graph.changeData(treeData);
this.graph.layout(); this.graph.layout();
this.restoreViewport(); this.restoreViewport();
this.graph.set("animate", true); this.graph.set("animate", true);
} }
reload() {
if (!this.unsave) {
const data = this.graph.findDataById("1") as GraphNodeModel;
this.autoId = Utils.refreshNodeId(data);
const root = Utils.createFileData(data);
const treeData = Utils.createTreeData(root, this.settings);
this.autoId = Utils.refreshNodeId(treeData);
this.graph.set("animate", false);
this.graph.changeData(treeData);
this.graph.layout();
this.restoreViewport();
this.graph.set("animate", true);
}
}
getTreeModel() { getTreeModel() {
const { filepath } = this.props; const { filepath } = this.props;
const data = this.graph.findDataById("1") as GraphNodeModel; const data = this.graph.findDataById("1") as GraphNodeModel;
@@ -407,28 +443,40 @@ export default class Editor extends React.Component<EditorProps, EditorState> {
} }
copyNode() { copyNode() {
if (!this.isInEditor) {
return;
}
console.log("editor copy node"); console.log("editor copy node");
const { curNodeId } = this.state; const { curNodeId } = this.state;
if (!curNodeId) { if (!curNodeId) {
return; return;
} }
const data = this.graph.findDataById(curNodeId) as GraphNodeModel; const data = this.graph.findDataById(curNodeId) as GraphNodeModel;
clipboard.writeText(JSON.stringify(Utils.cloneNodeData(data), null, 2)); const str = JSON.stringify(Utils.cloneNodeData(data), null, 2);
console.log("copy:", str);
clipboard.writeText(str);
} }
pasteNode() { pasteNode() {
if (!this.isInEditor) {
return;
}
const { curNodeId } = this.state; const { curNodeId } = this.state;
if (!curNodeId) { if (!curNodeId) {
message.warn("未选中节点"); message.warn("未选中节点");
return; return;
} }
const curNodeData = this.graph.findDataById(curNodeId); const curNodeData = this.graph.findDataById(curNodeId);
try { try {
const str = clipboard.readText(); const str = clipboard.readText();
if (!str || str == "") { if (!str || str == "") {
return; return;
} }
const data = Utils.createTreeData(JSON.parse(str), this.settings); console.log("paste:", str);
const data = Utils.createTreeData(JSON.parse(str), this.settings, curNodeId);
this.autoId = Utils.refreshNodeId(data, this.autoId); this.autoId = Utils.refreshNodeId(data, this.autoId);
this.onSelectNode(null); this.onSelectNode(null);
if (!curNodeData.children) { if (!curNodeData.children) {
@@ -446,7 +494,9 @@ export default class Editor extends React.Component<EditorProps, EditorState> {
useStackData(data: BehaviorNodeModel) { useStackData(data: BehaviorNodeModel) {
this.graph.set("animate", false); this.graph.set("animate", false);
this.graph.changeData(Utils.createTreeData(data, this.settings)); this.data = Utils.createTreeData(data, this.settings);
this.autoId = Utils.refreshNodeId(this.data);
this.graph.changeData(this.data);
this.graph.layout(); this.graph.layout();
this.restoreViewport(); this.restoreViewport();
this.graph.set("animate", true); this.graph.set("animate", true);
@@ -456,7 +506,7 @@ export default class Editor extends React.Component<EditorProps, EditorState> {
} }
pushUndoStack(keepRedo?: boolean) { pushUndoStack(keepRedo?: boolean) {
this.undoStack.push(Utils.cloneNodeData(this.graph.findDataById('1') as GraphNodeModel)); this.undoStack.push(Utils.cloneNodeData(this.graph.findDataById("1") as GraphNodeModel));
console.log("push undo", this.undoStack); console.log("push undo", this.undoStack);
if (!keepRedo) { if (!keepRedo) {
this.redoStack = []; this.redoStack = [];
@@ -464,7 +514,7 @@ export default class Editor extends React.Component<EditorProps, EditorState> {
} }
pushRedoStack() { pushRedoStack() {
this.redoStack.push(Utils.cloneNodeData(this.graph.findDataById('1') as GraphNodeModel)); this.redoStack.push(Utils.cloneNodeData(this.graph.findDataById("1") as GraphNodeModel));
console.log("push redo", this.redoStack); console.log("push redo", this.redoStack);
} }
@@ -504,22 +554,35 @@ export default class Editor extends React.Component<EditorProps, EditorState> {
return ( return (
<div className="editor"> <div className="editor">
<Row className="editorBd"> <Row className="editorBd">
<Col span={18} className="editorContent" ref={this.ref} onMouseDownCapture={(event) => { <Col
this.state.blockNodeSelectChange = false; span={18}
}} /> className="editorContent"
<Col span={6} className="editorSidebar" onMouseDownCapture={(event) => { ref={this.ref}
this.state.blockNodeSelectChange = true; onMouseDownCapture={(event) => {
}}> this.state.blockNodeSelectChange = false;
}}
/>
<Col
span={6}
className="editorSidebar"
onMouseDownCapture={(event) => {
this.state.blockNodeSelectChange = true;
}}
>
{curNode ? ( {curNode ? (
<NodePanel <NodePanel
model={curNode} model={curNode}
settings={this.settings} settings={this.settings}
updateNode={(id, forceUpdate) => { updateNode={(id, forceUpdate) => {
if (forceUpdate) { if (forceUpdate) {
const data: any = this.graph.findDataById(id); const data: GraphNodeModel = this.graph.findDataById(
id
) as GraphNodeModel;
data.conf = this.settings.getNodeConf(data.name); data.conf = this.settings.getNodeConf(data.name);
data.size = Utils.calcTreeNodeSize(data); data.size = Utils.calcTreeNodeSize(data);
this.changeWithoutAnim(); this.changeWithoutAnim();
this.save();
return;
} }
const item = this.graph.findById(id); const item = this.graph.findById(id);
item.draw(); item.draw();
@@ -533,12 +596,8 @@ export default class Editor extends React.Component<EditorProps, EditorState> {
) : ( ) : (
<TreePanel <TreePanel
model={this.treeModel} model={this.treeModel}
onRenameTree={(name: string) => { onRenameTree={(name: string) => {}}
onRemoveTree={() => {}}
}}
onRemoveTree={() => {
}}
onChangeTreeDesc={(desc) => { onChangeTreeDesc={(desc) => {
this.changeTreeDesc(desc); this.changeTreeDesc(desc);
}} }}
+111 -51
View File
@@ -1,6 +1,7 @@
import G6 from "@antv/g6"; import G6 from "@antv/g6";
import { toBreakWord } from "../common/Utils"; import { toBreakWord } from "../common/Utils";
import Settings from "../main-process/Settings"; import Settings from "../main-process/Settings";
import { BehaviorNodeTypeModel } from "../common/BehaviorTreeModel";
const NODE_COLORS: any = { const NODE_COLORS: any = {
["Composite"]: "rgb(91,237,32)", ["Composite"]: "rgb(91,237,32)",
@@ -8,8 +9,11 @@ const NODE_COLORS: any = {
["Condition"]: "rgb(228,20,139)", ["Condition"]: "rgb(228,20,139)",
["Action"]: "rgb(91,143,249)", ["Action"]: "rgb(91,143,249)",
["Other"]: "rgb(112,112,112)", ["Other"]: "rgb(112,112,112)",
["Error"]: "rgb(255,0,0)",
}; };
const SELECTED_LINE_WIDTH = 4;
export default function (settings: Settings) { export default function (settings: Settings) {
const collapseStyle: any = { const collapseStyle: any = {
"collapse-icon": { "collapse-icon": {
@@ -22,6 +26,10 @@ export default function (settings: Settings) {
{ {
options: { options: {
type: "rect", type: "rect",
anchorPoints: [
[0, 0.5],
[1, 0.5],
],
labelCfg: { labelCfg: {
style: { style: {
fill: "blue", fill: "blue",
@@ -35,7 +43,10 @@ export default function (settings: Settings) {
selected: { selected: {
"main-box": { "main-box": {
stroke: "yellow", stroke: "yellow",
lineWidth: 5, lineWidth: SELECTED_LINE_WIDTH,
},
"name-bg-selected": {
fill: "yellow",
}, },
}, },
hover: { hover: {
@@ -47,35 +58,39 @@ export default function (settings: Settings) {
}, },
dragRight: { dragRight: {
"drag-right": { "drag-right": {
fillOpacity: 0.6, fillOpacity: 0.7,
}, },
}, },
dragUp: { dragUp: {
"drag-up": { "drag-up": {
fillOpacity: 0.6, fillOpacity: 0.7,
}, },
}, },
dragDown: { dragDown: {
"drag-down": { "drag-down": {
fillOpacity: 0.6, fillOpacity: 0.7,
}, },
}, },
}, },
}, },
draw(cfg, group) { draw(cfg, group) {
const nodeConf = settings.getNodeConf(cfg.name as string); const nodeConf = settings.getNodeConf(cfg.name as string);
const classify = nodeConf.type || "Other"; let classify = nodeConf.type || "Other";
if (
!settings.hasNodeConf(cfg.name as string) ||
(cfg.path && (!cfg.children || (cfg.children as []).length == 0))
) {
classify = "Error";
}
const color = NODE_COLORS[classify] || "Other"; const color = NODE_COLORS[classify] || "Other";
var size = cfg.size ? (cfg.size as number[]) : [150, 40]; var size = cfg.size ? (cfg.size as number[]) : [150, 40];
const w = size[0]; const w = size[0];
const h = size[1]; const h = size[1];
const x0 = -w / 2; const r = 4;
const y0 = -h / 2;
const r = 2;
const shape = group.addShape("rect", { const shape = group.addShape("rect", {
attrs: { attrs: {
x: x0, x: 0,
y: y0, y: 0,
width: w, width: w,
height: h, height: h,
stroke: color, stroke: color,
@@ -87,15 +102,45 @@ export default function (settings: Settings) {
draggable: true, draggable: true,
}); });
// is subtree
if (cfg.path && cfg.id !== "1") {
group.addShape("rect", {
attrs: {
x: -10,
y: -10,
width: w + 20,
height: h + 20,
stroke: "white",
lineWidth: 2.5,
lineDash: [5, 5],
radius: [r, r, r, r],
},
name: "subtree",
draggable: true,
});
}
// name bg // name bg
group.addShape("rect", { group.addShape("rect", {
attrs: { attrs: {
x: x0 + 1.5, x: 0,
y: y0 + 1.5, y: 0,
width: w - 3, width: w,
height: 16, height: 20,
fill: color, fill: color,
// radius: r, radius: [r, r, 0, 0],
},
name: "name-bg-selected",
draggable: true,
});
group.addShape("rect", {
attrs: {
x: SELECTED_LINE_WIDTH / 2,
y: SELECTED_LINE_WIDTH / 2,
width: w - 5,
height: 20 - SELECTED_LINE_WIDTH / 2,
fill: color,
radius: [r, r, 0, 0],
}, },
name: "name-bg", name: "name-bg",
draggable: true, draggable: true,
@@ -105,8 +150,8 @@ export default function (settings: Settings) {
group.addShape("text", { group.addShape("text", {
attrs: { attrs: {
textBaseline: "top", textBaseline: "top",
x: x0 - 3, x: -3,
y: -8, y: h / 2 - 8,
fontSize: 20, fontSize: 20,
lineHeight: 20, lineHeight: 20,
text: cfg.id, text: cfg.id,
@@ -122,8 +167,8 @@ export default function (settings: Settings) {
var img = `./static/icons/${classify}.svg`; var img = `./static/icons/${classify}.svg`;
group.addShape("image", { group.addShape("image", {
attrs: { attrs: {
x: x0 + 3, x: 5,
y: y0 + 2, y: 2,
height: 14, height: 14,
width: 14, width: 14,
img, img,
@@ -135,8 +180,8 @@ export default function (settings: Settings) {
if (cfg.debug) { if (cfg.debug) {
group.addShape("image", { group.addShape("image", {
attrs: { attrs: {
x: x0 + 182, x: 182,
y: y0 + 2, y: 2,
height: 14, height: 14,
width: 14, width: 14,
img: `./static/icons/Debug.svg`, img: `./static/icons/Debug.svg`,
@@ -149,8 +194,8 @@ export default function (settings: Settings) {
group.addShape("text", { group.addShape("text", {
attrs: { attrs: {
textBaseline: "top", textBaseline: "top",
x: x0 + 18, x: 22,
y: y0 + 4, y: 3,
fontWeight: 800, fontWeight: 800,
lineHeight: 20, lineHeight: 20,
text: cfg.name, text: cfg.name,
@@ -159,8 +204,8 @@ export default function (settings: Settings) {
name: "name-text", name: "name-text",
}); });
var x = x0 + 2; var x = 5;
var y = y0 + 23; var y = 24;
// desc text // desc text
const desc = cfg.desc || nodeConf.desc; const desc = cfg.desc || nodeConf.desc;
if (desc) { if (desc) {
@@ -171,7 +216,7 @@ export default function (settings: Settings) {
y, y,
lineHeight: 20, lineHeight: 20,
fontWeight: 800, fontWeight: 800,
text: `备注:${desc}`, text: `备注:${desc}`,
fill: "black", fill: "black",
}, },
name: "desc-text", name: "desc-text",
@@ -180,7 +225,7 @@ export default function (settings: Settings) {
const args: any = cfg.args; const args: any = cfg.args;
if (nodeConf.args && args && Object.keys(args).length > 0) { if (nodeConf.args && args && Object.keys(args).length > 0) {
const { str, line } = toBreakWord(`参数:${JSON.stringify(args)}`, 35); const { str, line } = toBreakWord(`参数:${JSON.stringify(args)}`, 34);
group.addShape("text", { group.addShape("text", {
attrs: { attrs: {
textBaseline: "top", textBaseline: "top",
@@ -198,7 +243,7 @@ export default function (settings: Settings) {
const input: [] = cfg.input ? (cfg.input as []) : []; const input: [] = cfg.input ? (cfg.input as []) : [];
if (nodeConf.input && input.length > 0) { if (nodeConf.input && input.length > 0) {
const { str, line } = toBreakWord(`输入:${JSON.stringify(input)}`, 35); const { str, line } = toBreakWord(`输入:${JSON.stringify(input)}`, 35);
group.addShape("text", { group.addShape("text", {
attrs: { attrs: {
textBaseline: "top", textBaseline: "top",
@@ -215,7 +260,7 @@ export default function (settings: Settings) {
const output: [] = cfg.output ? (cfg.output as []) : []; const output: [] = cfg.output ? (cfg.output as []) : [];
if (nodeConf.output && output.length > 0) { if (nodeConf.output && output.length > 0) {
const { str, line } = toBreakWord(`输出:${JSON.stringify(output)}`, 35); const { str, line } = toBreakWord(`输出:${JSON.stringify(output)}`, 35);
group.addShape("text", { group.addShape("text", {
attrs: { attrs: {
textBaseline: "top", textBaseline: "top",
@@ -230,11 +275,26 @@ export default function (settings: Settings) {
y += 20 * line; y += 20 * line;
} }
if (cfg.path) {
group.addShape("text", {
attrs: {
textBaseline: "top",
x,
y: y + 20,
lineHeight: 20,
text: `子树:${cfg.path}`,
fill: "black",
},
name: "subtree-text",
});
y += 20;
}
if (Array.isArray(cfg.children) && cfg.children.length > 0) { if (Array.isArray(cfg.children) && cfg.children.length > 0) {
group.addShape("marker", { group.addShape("marker", {
attrs: { attrs: {
x: w / 2, x: w,
y: 0, y: h / 2,
r: 6, r: 6,
symbol: G6.Marker.collapse, symbol: G6.Marker.collapse,
stroke: "#666", stroke: "#666",
@@ -248,43 +308,43 @@ export default function (settings: Settings) {
group.addShape("rect", { group.addShape("rect", {
name: "drag-up", name: "drag-up",
attrs: { attrs: {
x: -w / 2, x: 0,
y: -h / 2,
width: w,
height: h / 2,
fill: "blue",
fillOpacity: 0,
},
draggable: true,
// visible: false,
});
group.addShape("rect", {
name: "drag-down",
attrs: {
x: -w / 2,
y: 0, y: 0,
width: w, width: w,
height: h / 2, height: h / 2,
fill: "blue", fill: "blue",
fillOpacity: 0, fillOpacity: 0,
radius: [r, r, 0, 0],
},
draggable: true,
});
group.addShape("rect", {
name: "drag-down",
attrs: {
x: 0,
y: h / 2,
width: w,
height: h / 2,
fill: "blue",
fillOpacity: 0,
radius: [0, 0, r, r],
}, },
draggable: true, draggable: true,
// visible: false,
}); });
group.addShape("rect", { group.addShape("rect", {
name: "drag-right", name: "drag-right",
attrs: { attrs: {
x: w * 0.1, x: w / 2,
y: -h / 2, y: 0,
width: w * 0.4, width: w / 2,
height: h, height: h,
fill: "blue", fill: "blue",
fillOpacity: 0, fillOpacity: 0,
radius: [0, r, r, 0],
}, },
draggable: true, draggable: true,
// visible: false,
}); });
return shape; return shape;
}, },
+42 -10
View File
@@ -1,12 +1,14 @@
import Editor from "./Editor"; import { Tabs, message } from "antd";
import React, { Component } from "react";
import { Layout, Tabs, message } from "antd";
import { ipcRenderer } from "electron"; import { ipcRenderer } from "electron";
import * as fs from "fs";
import * as path from "path"; import * as path from "path";
import * as Utils from "../common/Utils";
import React, { Component } from "react";
import Editor from "./Editor";
import "antd/dist/antd.dark.css"; import "antd/dist/antd.dark.css";
import MainEventType from "../common/MainEventType";
import { BehaviorTreeModel } from "../common/BehaviorTreeModel"; import { BehaviorTreeModel } from "../common/BehaviorTreeModel";
import MainEventType from "../common/MainEventType";
const { TabPane } = Tabs; const { TabPane } = Tabs;
@@ -70,6 +72,31 @@ export default class TreeTabs extends Component<TreeTabsProps, TreeTabsState> {
message.success("已保存所有行为树"); message.success("已保存所有行为树");
}); });
ipcRenderer.on(MainEventType.BUILD, () => {
for (let k in this.editors) {
let editor = this.editors[k];
editor.save();
}
const settings = Utils.getRemoteSettings();
const workdir = settings.workdir;
if (!fs.existsSync(workdir)) {
console.log("workdir not found:", workdir);
return;
}
const files = fs.readdirSync(workdir).filter((f) => {
return f.endsWith(".json");
});
for (const path of files) {
const builddir = fs.realpathSync(workdir + "/../build", "utf-8");
fs.mkdirSync(builddir, { recursive: true });
const buildpath = builddir + "/" + path;
console.log("build:", buildpath);
const treeModel = Utils.createBuildData(path, settings);
fs.writeFileSync(buildpath, JSON.stringify(treeModel, null, 2));
}
});
ipcRenderer.on(MainEventType.UNDO, () => { ipcRenderer.on(MainEventType.UNDO, () => {
const editor = this.getCurEditor(); const editor = this.getCurEditor();
editor?.undo(); editor?.undo();
@@ -88,7 +115,7 @@ export default class TreeTabs extends Component<TreeTabsProps, TreeTabsState> {
} }
getOpenTreesModel() { getOpenTreesModel() {
const trees: BehaviorTreeModel[] = [] const trees: BehaviorTreeModel[] = [];
for (let k in this.editors) { for (let k in this.editors) {
let editor = this.editors[k]; let editor = this.editors[k];
if (editor) { if (editor) {
@@ -108,7 +135,7 @@ export default class TreeTabs extends Component<TreeTabsProps, TreeTabsState> {
} }
openFile(path: string) { openFile(path: string) {
if (!this.state.trees.find(e => e.filepath == path)) { if (!this.state.trees.find((e) => e.filepath == path)) {
const trees = this.state.trees; const trees = this.state.trees;
trees.push({ filepath: path }); trees.push({ filepath: path });
this.setState({ trees, curTree: path }); this.setState({ trees, curTree: path });
@@ -119,7 +146,7 @@ export default class TreeTabs extends Component<TreeTabsProps, TreeTabsState> {
closeFile(path: string) { closeFile(path: string) {
let trees = this.state.trees; let trees = this.state.trees;
trees = trees.filter(tree => tree.filepath != path); trees = trees.filter((tree) => tree.filepath != path);
const length = trees.length; const length = trees.length;
this.setState({ trees: trees, curTree: length > 0 ? trees[0].filepath : null }); this.setState({ trees: trees, curTree: length > 0 ? trees[0].filepath : null });
} }
@@ -139,6 +166,8 @@ export default class TreeTabs extends Component<TreeTabsProps, TreeTabsState> {
defaultActiveKey={curTree} defaultActiveKey={curTree}
activeKey={curTree} activeKey={curTree}
onChange={(activeKey) => { onChange={(activeKey) => {
const editor = this.editors[activeKey];
editor?.reload();
this.setState({ curTree: activeKey }); this.setState({ curTree: activeKey });
}} }}
onEdit={(targetKey, action) => { onEdit={(targetKey, action) => {
@@ -149,14 +178,17 @@ export default class TreeTabs extends Component<TreeTabsProps, TreeTabsState> {
> >
{trees.map((tree) => { {trees.map((tree) => {
return ( return (
<TabPane tab={`${path.basename(tree.filepath)}${tree.unsave?"*":''}`} key={tree.filepath}> <TabPane
tab={`${path.basename(tree.filepath)}${tree.unsave ? "*" : ""}`}
key={tree.filepath}
>
<Editor <Editor
filepath={tree.filepath} filepath={tree.filepath}
onChangeSaveState={(unsave) => { onChangeSaveState={(unsave) => {
if(tree.unsave != unsave) { if (tree.unsave != unsave) {
tree.unsave = unsave; tree.unsave = unsave;
this.forceUpdate(); this.forceUpdate();
} }
}} }}
ref={(ref) => { ref={(ref) => {
this.editors[tree.filepath] = ref; this.editors[tree.filepath] = ref;
+55 -1
View File
@@ -1 +1,55 @@
{"name":"hero","root":{"id":1,"name":"Selector","desc":"英雄测试AI","args":{},"children":[{"id":2,"name":"Sequence","desc":"攻击","args":{},"children":[{"id":3,"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}}]},{"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"} {
"name": "hero",
"root": {
"id": 1,
"name": "Selector",
"desc": "英雄测试AI",
"args": {},
"children": [
{
"id": 2,
"name": "Sequence",
"desc": "攻击",
"args": {},
"children": [
{
"id": 3,
"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 } }
]
},
{
"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"
}
+19 -4
View File
@@ -18,10 +18,18 @@
"args": {}, "args": {},
"output": [ "output": [
"hp" "hp"
],
"children": [
{
"id": 4,
"name": "Sequence",
"desc": "新建行为树",
"path": "subtree2.json"
}
] ]
}, },
{ {
"id": 4, "id": 6,
"name": "Cmp", "name": "Cmp",
"args": { "args": {
"gt": 50, "gt": 50,
@@ -32,17 +40,24 @@
] ]
}, },
{ {
"id": 5, "id": 7,
"name": "Log", "name": "Log",
"desc": "攻击", "desc": "攻击",
"args": { "args": {
"str": "Attack!" "str": "Attack!"
} },
"children": []
},
{
"id": 8,
"name": "Sequence",
"desc": "新建行为树",
"path": "subtree1.json"
} }
] ]
}, },
{ {
"id": 6, "id": 11,
"name": "Log", "name": "Log",
"desc": "逃跑", "desc": "逃跑",
"args": { "args": {
+25
View File
@@ -0,0 +1,25 @@
{
"name": "subtree",
"root": {
"id": 1,
"name": "Sequence",
"desc": "新建行为树",
"children": [
{
"id": 2,
"name": "Log",
"args": {
"str": "hello"
}
},
{
"id": 3,
"name": "Wait",
"args": {
"time": 1
}
}
]
},
"desc": "子树1"
}
+15
View File
@@ -0,0 +1,15 @@
{
"name": "subtree2",
"root": {
"id": 1,
"name": "Sequence",
"desc": "新建行为树",
"children": [
{
"id": 2,
"name": "unknow"
}
]
},
"desc": "子树2"
}
+10
View File
@@ -0,0 +1,10 @@
<?xml version="1.0" ?>
<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>

After

Width:  |  Height:  |  Size: 663 B

+2 -2
View File
@@ -25,11 +25,11 @@ module.exports = {
{ {
test: /\.less$/, test: /\.less$/,
include: /node_modules/, include: /node_modules/,
loaders: ['style-loader','css-loader','less-loader'] use: ['style-loader',{ loader: 'css-loader' },{ loader: 'less-loader' }]
}, },
{ {
test: /\.css$/, test: /\.css$/,
loaders: ['style-loader', 'css-loader'] use: ['style-loader', { loader: 'css-loader' }]
}, },
{//https://github.com/ashtuchkin/iconv-lite/issues/205 {//https://github.com/ashtuchkin/iconv-lite/issues/205
test: /node_modules[\/\\](iconv-lite)[\/\\].+/, test: /node_modules[\/\\](iconv-lite)[\/\\].+/,