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 { G6GraphEvent } from "@antv/g6/lib/interface/behavior"; import * as Utils from "../../common/Utils"; import { BehaviorTreeModel, GraphNodeModel, BehaviorNodeModel, } from "../../common/BehaviorTreeModel"; import TreePanel from "./TreePanel"; import Settings from "../../main-process/Settings"; import "./Editor.css"; import { clipboard } from "electron"; export interface EditorProps { filepath: string; onChangeSaveState: (unsave: boolean) => void; } interface EditorState { curNodeId?: string; } export default class Editor extends React.Component { private ref: React.RefObject; state: EditorState = {}; private graph: TreeGraph; private dragSrcId: string; private dragDstId: string; private autoId: number; private undoStack: BehaviorNodeModel[] = []; private redoStack: BehaviorNodeModel[] = []; private treeModel: BehaviorTreeModel; private settings: Settings; private data: GraphNodeModel; private unsave: boolean = false; constructor(props: EditorProps) { super(props); this.ref = React.createRef(); this.settings = Utils.getRemoteSettings(); const str = fs.readFileSync(this.props.filepath, "utf8"); this.treeModel = JSON.parse(str); this.data = Utils.createTreeData(this.treeModel.root, this.settings); this.autoId = Utils.refreshNodeId(this.data); } shouldComponentUpdate(nextProps: EditorProps, nextState: EditorState) { return this.props.filepath != nextProps.filepath || this.state.curNodeId != nextState.curNodeId; } componentDidMount() { const graph = new TreeGraph({ container: this.ref.current, width: window.screen.width * 0.66, height: window.screen.height, animate: false, maxZoom: 2, // fitCenter: true, modes: { default: [ "drag-canvas", "zoom-canvas", "click-select", "hover", { type: "collapse-expand", trigger: "dblclick", onChange: (item, collapsed) => { this.onSelectNode(item.getID()); const data = item.getModel(); data.collapsed = collapsed; graph.setItemState(item, "collapsed", data.collapsed as boolean); const icon = data.collapsed ? G6.Marker.expand : G6.Marker.collapse; const marker = item .get("group") .find((ele: any) => ele.get("name") === "collapse-icon"); marker.attr("symbol", icon); return true; }, }, ], }, defaultEdge: { type: "cubic-horizontal", style: { stroke: "#A3B1BF", }, }, defaultNode: { type: "TreeNode", }, layout: { type: "compactBox", direction: "LR", getHGap: () => 50, getWidth: (d: GraphNodeModel) => { return 150; }, getHeight: (d: GraphNodeModel) => { if (d.size) { return d.size[1]; } else { return 50; } }, }, }); graph.on("node:mouseenter", (e: G6GraphEvent) => { const { item } = e; if (item.hasState("selected")) { return; } graph.setItemState(item, "hover", true); }); graph.on("node:mouseleave", (e: G6GraphEvent) => { const { item } = e; if (item.hasState("selected")) { return; } graph.setItemState(item, "hover", false); }); graph.on("nodeselectchange", (e: G6GraphEvent) => { if (e.target) { this.onSelectNode(e.target.getID()); } else { this.onSelectNode(null); } }); const clearDragDstState = () => { if (this.dragDstId) { graph.setItemState(this.dragDstId, "dragRight", false); graph.setItemState(this.dragDstId, "dragDown", false); graph.setItemState(this.dragDstId, "dragUp", false); this.dragDstId = null; } }; const clearDragSrcState = () => { if (this.dragSrcId) { graph.setItemState(this.dragSrcId, "dragSrc", false); this.dragSrcId = null; } }; graph.on("node:dragstart", (e: G6GraphEvent) => { this.dragSrcId = e.item.getID(); graph.setItemState(this.dragSrcId, "dragSrc", true); }); graph.on("node:dragend", (e: G6GraphEvent) => { if (this.dragSrcId) { graph.setItemState(this.dragSrcId, "dragSrc", false); this.dragSrcId = null; } }); graph.on("node:dragover", (e: G6GraphEvent) => { const dstNodeId = e.item.getID(); if (dstNodeId == this.dragSrcId) { return; } if (this.dragDstId) { graph.setItemState(this.dragDstId, "dragRight", false); graph.setItemState(this.dragDstId, "dragDown", false); graph.setItemState(this.dragDstId, "dragUp", false); } const box = e.item.getBBox(); if (e.x > box.minX + box.width * 0.6) { graph.setItemState(dstNodeId, "dragRight", true); } else if (e.y > box.minY + box.height * 0.5) { graph.setItemState(dstNodeId, "dragDown", true); } else { graph.setItemState(dstNodeId, "dragUp", true); } this.dragDstId = dstNodeId; }); graph.on("node:dragleave", (e: G6GraphEvent) => { clearDragDstState(); }); graph.on("node:drop", (e: G6GraphEvent) => { const srcNodeId = this.dragSrcId; const dstNode = e.item; var dragDir; if (dstNode.hasState("dragRight")) { dragDir = "dragRight"; } else if (dstNode.hasState("dragDown")) { dragDir = "dragDown"; } else if (dstNode.hasState("dragUp")) { dragDir = "dragUp"; } clearDragSrcState(); clearDragDstState(); if (!srcNodeId) { console.log("no drag src"); return; } if (srcNodeId == dstNode.getID()) { console.log("drop same node"); return; } const rootData = graph.findDataById("1"); const srcData = graph.findDataById(srcNodeId); const srcParent = Utils.findParent(rootData, srcNodeId); const dstData = graph.findDataById(dstNode.getID()); const dstParent = Utils.findParent(rootData, dstNode.getID()); if (!srcParent) { console.log("no parent!"); return; } if (Utils.findFromAllChildren(srcData, dstData.id)) { // 不能将父节点拖到自已的子孙节点 console.log("cannot move to child"); return; } const removeSrc = () => { this.pushUndoStack(); srcParent.children = srcParent.children.filter((e) => e.id != srcData.id); }; console.log("dstNode", dstNode); if (dragDir == "dragRight") { removeSrc(); if (!dstData.children) { dstData.children = []; } dstData.children.push(srcData); } else if (dragDir == "dragUp") { if (!dstParent) { return; } removeSrc(); const idx = dstParent.children.findIndex((e) => e.id == dstData.id); dstParent.children.splice(idx, 0, srcData); } else if (dragDir == "dragDown") { if (!dstParent) { return; } removeSrc(); const idx = dstParent.children.findIndex((e) => e.id == dstData.id); dstParent.children.splice(idx + 1, 0, srcData); } else { return; } // console.log("cur data", graph.findDataById('1')); this.changeWithoutAnim(); }); graph.data(this.data); graph.render(); graph.fitCenter(); graph.set("animate", true); this.graph = graph; this.forceUpdate(); } onSelectNode(curNodeId: string | null) { const graph = this.graph; if (this.state.curNodeId) { graph.setItemState(this.state.curNodeId, "selected", false); } this.setState({ curNodeId }); if (this.state.curNodeId) { graph.setItemState(this.state.curNodeId, "selected", true); } } createNode(name: string) { console.log("editor create node", name); const { curNodeId } = this.state; if (!curNodeId) { message.warn("未选中节点"); return; } this.pushUndoStack(); const curNodeData = this.graph.findDataById(curNodeId); const newNodeData: BehaviorNodeModel = { id: this.autoId++, name: name, }; if (!curNodeData.children) { curNodeData.children = []; } curNodeData.children.push(Utils.createTreeData(newNodeData, this.settings)); this.changeWithoutAnim(); } deleteNode() { console.log("editor delete node"); const { curNodeId } = this.state; if (!curNodeId) { return; } if (curNodeId == "1") { message.warn("根节点不能删除!"); return; } this.onSelectNode(null); this.pushUndoStack(); const rootData = this.graph.findDataById("1"); const parentData = Utils.findParent(rootData, curNodeId); parentData.children = parentData.children.filter((e) => e.id != curNodeId); this.changeWithoutAnim(); } changeWithoutAnim() { this.graph.set("animate", false); this.graph.changeData(); this.graph.layout(); this.graph.set("animate", true); this.props.onChangeSaveState(true); this.unsave = true; } save() { if (!this.unsave) { return; } const { filepath } = this.props; const data = this.graph.findDataById("1") as GraphNodeModel; this.autoId = Utils.refreshNodeId(data); const root = Utils.createFileData(data); const treeModel = { name: path.basename(filepath).slice(0, -5), root, desc: this.treeModel.desc, } as BehaviorTreeModel; fs.writeFileSync( filepath, JSON.stringify(treeModel, null, 2) ); this.props.onChangeSaveState(false); this.unsave = false; this.graph.set("animate", false); this.graph.changeData(Utils.createTreeData(root, this.settings)); this.graph.layout(); this.graph.fitCenter(); this.graph.set("animate", true); return treeModel; } copyNode() { console.log("editor copy node"); const { curNodeId } = this.state; if (!curNodeId) { return; } const data = this.graph.findDataById(curNodeId) as GraphNodeModel; clipboard.writeText(JSON.stringify(Utils.cloneNodeData(data), null, 2)); } pasteNode() { const { curNodeId } = this.state; if (!curNodeId) { message.warn("未选中节点"); return; } const curNodeData = this.graph.findDataById(curNodeId); try { const str = clipboard.readText(); if (!str || str == "") { return; } this.pushUndoStack(); const data = Utils.createTreeData(JSON.parse(str), this.settings); this.autoId = Utils.refreshNodeId(data, this.autoId); this.onSelectNode(null); if (!curNodeData.children) { curNodeData.children = []; } curNodeData.children.push(data); // this.autoId = Utils.refreshNodeId(this.graph.findDataById("1") as GraphNodeModel); this.changeWithoutAnim(); } catch (error) { // message.error("粘贴数据有误"); console.log("paste error"); } } useStackData(data: BehaviorNodeModel) { this.graph.set("animate", false); this.graph.changeData(Utils.createTreeData(data, this.settings)); this.graph.layout(); this.graph.fitCenter(); this.graph.set("animate", true); this.props.onChangeSaveState(true); this.unsave = true; } pushUndoStack(keepRedo?: boolean) { this.undoStack.push(Utils.cloneNodeData(this.graph.findDataById('1') as GraphNodeModel)); console.log("push undo", this.undoStack); if (!keepRedo) { this.redoStack = []; } } pushRedoStack() { this.redoStack.push(Utils.cloneNodeData(this.graph.findDataById('1') as GraphNodeModel)); console.log("push redo", this.redoStack); } undo() { if (this.undoStack.length == 0) { return; } const data = this.undoStack.pop(); this.pushRedoStack(); this.useStackData(data); } redo() { if (this.redoStack.length == 0) { return; } const data = this.redoStack.pop(); this.pushUndoStack(true); this.useStackData(data); } changeTreeDesc(desc: string) { this.treeModel.desc = desc; this.settings.setTreeDesc(this.props.filepath, desc); this.unsave = true; this.save(); } render() { const { curNodeId } = this.state; console.log("render tree", curNodeId); var curNode: any; if (curNodeId) { curNode = this.graph.findDataById(curNodeId); } return (
{curNode ? ( { if (forceUpdate) { const data: any = this.graph.findDataById(id); data.conf = this.settings.getNodeConf(data.name); data.size = Utils.calcTreeNodeSize(data); this.changeWithoutAnim(); } const item = this.graph.findById(id); item.draw(); this.props.onChangeSaveState(true); this.unsave = true; }} pushUndoStack={() => { this.pushUndoStack(); }} /> ) : ( { }} onRemoveTree={() => { }} onChangeTreeDesc={(desc) => { this.changeTreeDesc(desc); }} /> )}
); } }