Merge branch 'dev'

This commit is contained in:
codetypes committed 2024-04-26 10:10:22 +08:00
commit 78925c4ae5
14 files changed
+338 -188

No files matched your search

+8 -11
View File
@@ -1,14 +1,11 @@
{ {
"folders": [ "folders": [
{ {
"path": "." "path": "."
}
],
"settings": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"[json]": {
"editor.formatOnSave": false
}
} }
],
"settings": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
} }
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "behavior3editor", "name": "behavior3editor",
"version": "1.1.2", "version": "1.1.3",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "behavior3editor", "name": "behavior3editor",
"version": "1.1.2", "version": "1.1.3",
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "behavior3editor", "name": "behavior3editor",
"version": "1.1.2", "version": "1.1.3",
"main": "dist-electron/main/index.js", "main": "dist-electron/main/index.js",
"description": "行为树编辑器", "description": "行为树编辑器",
"author": "zhandouxiaojiji & codetypes", "author": "zhandouxiaojiji & codetypes",
+2 -2
View File
@@ -124,7 +124,7 @@
}, },
{ {
"name": "MoveToPos", "name": "MoveToPos",
"type": "Action", "type": "Action (Client)",
"args": [ "args": [
{ {
"name": "x", "name": "x",
@@ -141,7 +141,7 @@
}, },
{ {
"name": "MoveToTarget", "name": "MoveToTarget",
"type": "Action", "type": "Action (Server)",
"input": [ "input": [
"{目标}" "{目标}"
], ],
+165 -64
View File
@@ -10,17 +10,19 @@ import * as b3util from "@/misc/b3util";
import { message } from "@/misc/hooks"; import { message } from "@/misc/hooks";
import { Hotkey, isHotkeyPressed, isMacos, useHotkeys } from "@/misc/keys"; import { Hotkey, isHotkeyPressed, isMacos, useHotkeys } from "@/misc/keys";
import Path from "@/misc/path"; import Path from "@/misc/path";
import { ArrowDownOutlined, ArrowUpOutlined, CloseOutlined } from "@ant-design/icons";
import G6, { G6GraphEvent, Item, TreeGraph } from "@antv/g6"; import G6, { G6GraphEvent, Item, TreeGraph } from "@antv/g6";
import { dialog } from "@electron/remote"; import { dialog } from "@electron/remote";
import { useSize } from "ahooks"; import { useSize } from "ahooks";
import { Dropdown, Flex, FlexProps, MenuProps } from "antd"; import { Button, Dropdown, Flex, FlexProps, Input, MenuProps } from "antd";
import { clipboard } from "electron"; import { clipboard } from "electron";
import * as fs from "fs"; import * as fs from "fs";
import React, { FC, useCallback, useEffect, useMemo, useRef } from "react"; import React, { FC, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { FiDelete } from "react-icons/fi"; import { FiDelete } from "react-icons/fi";
import { IoMdReturnLeft } from "react-icons/io"; import { IoMdReturnLeft } from "react-icons/io";
import { mergeRefs } from "react-merge-refs"; import { mergeRefs } from "react-merge-refs";
import { useDebounceCallback } from "usehooks-ts";
import "./register-node"; import "./register-node";
export interface EditorProps extends React.HTMLAttributes<HTMLElement> { export interface EditorProps extends React.HTMLAttributes<HTMLElement> {
@@ -36,11 +38,27 @@ export const Editor: FC<EditorProps> = ({ onUpdate: updateState, data: editor, .
open: useWorkspace((state) => state.open), open: useWorkspace((state) => state.open),
workdir: useWorkspace((state) => state.workdir), workdir: useWorkspace((state) => state.workdir),
}; };
const [results, setResults] = useState<string[]>([]);
const [resultIndex, setResultIndex] = useState(0);
const [showingSearch, setShowingSearch] = useState(false);
const graphRef = useRef(null); const graphRef = useRef(null);
const sizeRef = useRef(null); const sizeRef = useRef(null);
const editorSize = useSize(sizeRef); const editorSize = useSize(sizeRef);
const { t } = useTranslation(); const { t } = useTranslation();
const onSearchChange = useDebounceCallback((filter: string) => {
const ids = filterNodes(filter, findDataById("1"));
setResults(ids);
setResultIndex(0);
if (ids.length > 0) {
editor.graph.focusItem(ids[0]);
selectNode(ids[0]);
}
if (filter.length == 0) {
restoreViewport();
}
}, 200);
const onChange = () => { const onChange = () => {
if (!editor.unsave) { if (!editor.unsave) {
editor.unsave = true; editor.unsave = true;
@@ -93,6 +111,17 @@ export const Editor: FC<EditorProps> = ({ onUpdate: updateState, data: editor, .
return editor.graph.findDataById(id) as TreeGraphData; return editor.graph.findDataById(id) as TreeGraphData;
}; };
const filterNodes = (str: string, node: TreeGraphData | null, ids?: string[]): string[] => {
ids ||= [];
if (node && str) {
if (node.name.toLocaleLowerCase().indexOf(str) >= 0) {
ids.push(node.id);
}
node.children?.forEach((child: TreeGraphData) => filterNodes(str, child, ids));
}
return ids;
};
const isAncestor = (ancestor: TreeGraphData, node: TreeGraphData): boolean => { const isAncestor = (ancestor: TreeGraphData, node: TreeGraphData): boolean => {
if (ancestor.id === node.parent) { if (ancestor.id === node.parent) {
return true; return true;
@@ -782,68 +811,70 @@ export const Editor: FC<EditorProps> = ({ onUpdate: updateState, data: editor, .
} }
); );
if (!editor.dispatch) { editor.dispatch = (event: EditEvent, data: unknown) => {
editor.dispatch = (event: EditEvent, data: unknown) => { switch (event) {
switch (event) { case "copy": {
case "copy": { copyNode();
copyNode(); break;
break;
}
case "paste": {
pasteNode();
break;
}
case "delete": {
deleteNode();
break;
}
case "insert": {
createNode();
break;
}
case "replace": {
replaceNode();
break;
}
case "save": {
save();
break;
}
case "undo": {
undo();
break;
}
case "redo": {
redo();
break;
}
case "reload": {
reload();
break;
}
case "rename": {
rename(data as string);
break;
}
case "updateTree": {
updateTree(data as EditTree);
break;
}
case "updateNode": {
updateNode(data as EditNode);
break;
}
case "editSubtree": {
editSubtree();
break;
}
case "saveAsSubtree": {
saveAsSubtree();
break;
}
} }
}; case "paste": {
} pasteNode();
break;
}
case "delete": {
deleteNode();
break;
}
case "insert": {
createNode();
break;
}
case "replace": {
replaceNode();
break;
}
case "save": {
save();
break;
}
case "undo": {
undo();
break;
}
case "redo": {
redo();
break;
}
case "reload": {
reload();
break;
}
case "rename": {
rename(data as string);
break;
}
case "updateTree": {
updateTree(data as EditTree);
break;
}
case "updateNode": {
updateNode(data as EditNode);
break;
}
case "searchNode": {
setShowingSearch(true);
break;
}
case "editSubtree": {
editSubtree();
break;
}
case "saveAsSubtree": {
saveAsSubtree();
break;
}
}
};
useEffect(() => { useEffect(() => {
if (!editorSize || (editorSize.width === 0 && editorSize.height === 0)) { if (!editorSize || (editorSize.width === 0 && editorSize.height === 0)) {
@@ -956,7 +987,7 @@ export const Editor: FC<EditorProps> = ({ onUpdate: updateState, data: editor, .
type MenuInfo = Parameters<Exclude<MenuProps["onClick"], undefined>>[0]; type MenuInfo = Parameters<Exclude<MenuProps["onClick"], undefined>>[0];
const onClick = useCallback((info: MenuInfo) => { const onClick = useCallback((info: MenuInfo) => {
editor.dispatch?.(info.key as EditEvent); editor.dispatch(info.key as EditEvent);
}, []); }, []);
return ( return (
@@ -967,6 +998,76 @@ export const Editor: FC<EditorProps> = ({ onUpdate: updateState, data: editor, .
tabIndex={-1} tabIndex={-1}
style={{ maxWidth: "inherit", maxHeight: "inherit" }} style={{ maxWidth: "inherit", maxHeight: "inherit" }}
> >
{showingSearch && (
<Flex
style={{
position: "absolute",
marginTop: "10px",
marginLeft: "10px",
backgroundColor: "#161b22",
padding: "4px 10px 4px 10px",
borderRadius: "4px",
borderLeft: "4px solid #f78166",
boxShadow: "0 0 8px 2px #0000005c",
alignItems: "center",
}}
>
<Input
autoFocus
allowClear
size="small"
style={{ borderRadius: "2px" }}
onChange={(e) => onSearchChange(e.currentTarget.value)}
/>
<div style={{ padding: "0 10px 0 5px" }}>
{results.length ? `${resultIndex + 1}/${results.length}` : ""}
</div>
<Button
icon={<ArrowDownOutlined />}
type="text"
size="small"
style={{ width: "30px" }}
disabled={results.length == 0}
onClick={() => {
if (results.length > 0) {
const idx = (resultIndex + 1) % results.length;
setResultIndex(idx);
editor.graph.focusItem(results[idx]);
selectNode(results[idx]);
}
}}
/>
<Button
icon={<ArrowUpOutlined />}
type="text"
size="small"
style={{ width: "30px" }}
disabled={results.length == 0}
onClick={() => {
if (results.length > 0) {
const idx = (resultIndex + results.length - 1) % results.length;
setResultIndex(idx);
console.log(idx, results[idx]);
editor.graph.focusItem(results[idx]);
selectNode(results[idx]);
}
}}
/>
<Button
icon={<CloseOutlined />}
type="text"
size="small"
style={{ width: "30px" }}
onClick={() => {
setShowingSearch(false);
setResults([]);
setResultIndex(0);
keysRef.current?.focus();
}}
/>
</Flex>
)}
<Dropdown menu={{ items: menuItems, onClick }} trigger={["contextMenu"]}> <Dropdown menu={{ items: menuItems, onClick }} trigger={["contextMenu"]}>
<div tabIndex={-1} style={{ width: "100%", height: "100%" }} ref={graphRef} /> <div tabIndex={-1} style={{ width: "100%", height: "100%" }} ref={graphRef} />
</Dropdown> </Dropdown>
+68 -39
View File
@@ -1,4 +1,5 @@
import { FileTreeType, useWorkspace } from "@/contexts/workspace-context"; import { FileTreeType, useWorkspace } from "@/contexts/workspace-context";
import { NodeDef, getNodeType } from "@/misc/b3type";
import * as b3util from "@/misc/b3util"; import * as b3util from "@/misc/b3util";
import { modal } from "@/misc/hooks"; import { modal } from "@/misc/hooks";
import { Hotkey, isHotkeyPressed, isMacos, useHotkeys } from "@/misc/keys"; import { Hotkey, isHotkeyPressed, isMacos, useHotkeys } from "@/misc/keys";
@@ -31,6 +32,15 @@ type MenuEvent =
| "move" | "move"
| "duplicate"; | "duplicate";
export type NodeTreeType = {
title: string;
def?: NodeDef;
icon?: React.ReactNode;
isLeaf?: boolean;
children?: NodeTreeType[];
style?: React.CSSProperties;
};
const findFile = (path: string | undefined, node: FileTreeType): FileTreeType | undefined => { const findFile = (path: string | undefined, node: FileTreeType): FileTreeType | undefined => {
if (!path) { if (!path) {
return; return;
@@ -89,11 +99,11 @@ const renameFile = (oldPath: string, newPath: string) => {
for (const editor of workspace.editors) { for (const editor of workspace.editors) {
if (isDirectory) { if (isDirectory) {
if (editor.path.startsWith(oldPath)) { if (editor.path.startsWith(oldPath)) {
editor.dispatch?.("rename", editor.path.replace(oldPath, newPath)); editor.dispatch("rename", editor.path.replace(oldPath, newPath));
} }
} else { } else {
if (editor.path === oldPath) { if (editor.path === oldPath) {
editor.dispatch?.("rename", newPath); editor.dispatch("rename", newPath);
} }
} }
} }
@@ -112,7 +122,6 @@ export const Explorer: FC = () => {
editors: useWorkspace((state) => state.editors), editors: useWorkspace((state) => state.editors),
fileTree: useWorkspace((state) => state.fileTree), fileTree: useWorkspace((state) => state.fileTree),
nodeDefs: useWorkspace((state) => state.nodeDefs), nodeDefs: useWorkspace((state) => state.nodeDefs),
nodeTree: useWorkspace((state) => state.nodeTree),
workdir: useWorkspace((state) => state.workdir), workdir: useWorkspace((state) => state.workdir),
onEditingNodeDef: useWorkspace((state) => state.onEditingNodeDef), onEditingNodeDef: useWorkspace((state) => state.onEditingNodeDef),
open: useWorkspace((state) => state.open), open: useWorkspace((state) => state.open),
@@ -134,49 +143,69 @@ export const Explorer: FC = () => {
); );
} }
if (workspace.nodeTree) { const nodeTree = useMemo(() => {
workspace.nodeTree.title = t("nodeDefinition"); const data: NodeTreeType = {
workspace.nodeTree.icon = ( title: t("nodeDefinition"),
<Flex justify="center" align="center" style={{ height: "100%" }}> icon: (
<PiTreeStructureFill size={19} />
</Flex>
);
workspace.nodeTree.children?.forEach((catalog) => {
catalog.icon = (
<Flex justify="center" align="center" style={{ height: "100%" }}> <Flex justify="center" align="center" style={{ height: "100%" }}>
<img <PiTreeStructureFill size={19} />
className="b3-node-icon"
style={{ width: "13px", height: "13px", color: "white" }}
src={`./icons/${catalog.title}.svg`}
/>
</Flex> </Flex>
); ),
catalog.children?.forEach((node) => { children: [],
if (node.def?.icon) { style: {
node.icon = ( fontWeight: "bold",
fontSize: "13px",
},
};
workspace.nodeDefs.forEach((nodeDef) => {
let catalog = data.children?.find((nt) => nt.title === nodeDef.type);
if (!catalog) {
const type = getNodeType(nodeDef);
catalog = {
title: nodeDef.type,
children: [],
icon: (
<Flex justify="center" align="center" style={{ height: "100%" }}> <Flex justify="center" align="center" style={{ height: "100%" }}>
<img <img
className="b3-node-icon" className="b3-node-icon"
key={catalog.title}
style={{ width: "13px", height: "13px", color: "white" }} style={{ width: "13px", height: "13px", color: "white" }}
src={ src={`./icons/${type}.svg`}
process.env.VITE_DEV_SERVER_URL && isMacos
? `${Path.basename(workspace.workdir)}/${node.def?.icon}`
: `${workspace.workdir}/${node.def?.icon}`
}
/> />
</Flex> </Flex>
); ),
} else { };
node.icon = ( data.children?.push(catalog);
<Flex justify="center" align="center" style={{ height: "100%" }}> }
<BsBoxFill style={{ width: "12px", height: "12px", color: "white" }} />{" "} catalog.children?.push({
</Flex> title: `${nodeDef.name}(${nodeDef.desc})`,
); isLeaf: true,
} def: nodeDef,
icon: nodeDef.icon ? (
<Flex justify="center" align="center" style={{ height: "100%" }}>
<img
className="b3-node-icon"
key={catalog.title}
style={{ width: "13px", height: "13px", color: "white" }}
src={
process.env.VITE_DEV_SERVER_URL && isMacos
? `${Path.basename(workspace.workdir)}/${nodeDef.icon}`
: `${workspace.workdir}/${nodeDef.icon}`
}
/>
</Flex>
) : (
<Flex justify="center" align="center" style={{ height: "100%" }}>
<BsBoxFill style={{ width: "12px", height: "12px", color: "white" }} />{" "}
</Flex>
),
}); });
}); });
} data.children?.sort((a, b) => a.title.localeCompare(b.title));
data.children?.forEach((child) =>
child.children?.sort((a, b) => a.title.localeCompare(b.title))
);
return data;
}, [t, workspace.nodeDefs]);
useEffect(() => { useEffect(() => {
if (workspace.editing) { if (workspace.editing) {
@@ -467,12 +496,12 @@ export const Explorer: FC = () => {
fs.renameSync(node.path, newPath); fs.renameSync(node.path, newPath);
for (const editor of workspace.editors) { for (const editor of workspace.editors) {
if (editor.path.startsWith(node.path)) { if (editor.path.startsWith(node.path)) {
editor.dispatch?.("rename", dest!.path + "/" + Path.basename(editor.path)); editor.dispatch("rename", dest!.path + "/" + Path.basename(editor.path));
} }
console.log("editor move", editor.path === newPath, editor.path, newPath); console.log("editor move", editor.path === newPath, editor.path, newPath);
if (editor.path.startsWith(newPath)) { if (editor.path.startsWith(newPath)) {
console.log("editor reload", editor.path === newPath, editor.path, newPath); console.log("editor reload", editor.path === newPath, editor.path, newPath);
editor.dispatch?.("reload"); editor.dispatch("reload");
} }
} }
}; };
@@ -795,7 +824,7 @@ export const Explorer: FC = () => {
<DirectoryTree <DirectoryTree
tabIndex={-1} tabIndex={-1}
fieldNames={{ key: "title" }} fieldNames={{ key: "title" }}
treeData={workspace.nodeTree ? [workspace.nodeTree] : []} treeData={nodeTree ? [nodeTree] : []}
draggable={{ icon: false, nodeDraggable: (node) => !!node.isLeaf }} draggable={{ icon: false, nodeDraggable: (node) => !!node.isLeaf }}
onSelect={(_, info) => { onSelect={(_, info) => {
const node = info.node; const node = info.node;
+11 -2
View File
@@ -30,7 +30,7 @@ const TreeInspector: FC = () => {
const data = {} as TreeModel; const data = {} as TreeModel;
data.name = values.name; data.name = values.name;
data.desc = values.desc || undefined; data.desc = values.desc || undefined;
workspace.editing?.dispatch?.("updateTree", { workspace.editing?.dispatch("updateTree", {
data: { data: {
name: values.name, name: values.name,
desc: values.desc || undefined, desc: values.desc || undefined,
@@ -80,6 +80,7 @@ const NodeInspector: FC = () => {
form.resetFields(); form.resetFields();
form.setFieldValue("id", data.id); form.setFieldValue("id", data.id);
form.setFieldValue("name", data.name); form.setFieldValue("name", data.name);
form.setFieldValue("type", def.type);
form.setFieldValue("desc", data.desc); form.setFieldValue("desc", data.desc);
form.setFieldValue("debug", data.debug); form.setFieldValue("debug", data.debug);
form.setFieldValue("path", data.path); form.setFieldValue("path", data.path);
@@ -173,7 +174,7 @@ const NodeInspector: FC = () => {
data.output ||= []; data.output ||= [];
data.output.push(v ?? ""); data.output.push(v ?? "");
}); });
workspace.editing?.dispatch?.("updateNode", { workspace.editing?.dispatch("updateNode", {
data: data, data: data,
} as EditNode); } as EditNode);
}; };
@@ -227,6 +228,14 @@ const NodeInspector: FC = () => {
> >
<Input disabled={true} /> <Input disabled={true} />
</Form.Item> </Form.Item>
<Form.Item
name="type"
label={
<div style={{ minWidth: "80px", justifyContent: "flex-end" }}>{t("node.type")}</div>
}
>
<Input disabled={true} />
</Form.Item>
<Form.Item label={t("node.name")} name="name"> <Form.Item label={t("node.name")} name="name">
<AutoComplete <AutoComplete
disabled={disabled} disabled={disabled}
+2 -2
View File
@@ -221,7 +221,7 @@ export const Menu: FC<LayoutProps> = () => {
label: t("insertNode"), label: t("insertNode"),
enabled: enabled, enabled: enabled,
click: () => { click: () => {
workspace.editing?.dispatch?.("insert"); workspace.editing?.dispatch("insert");
}, },
}, },
{ {
@@ -229,7 +229,7 @@ export const Menu: FC<LayoutProps> = () => {
label: t("deleteNode"), label: t("deleteNode"),
enabled: enabled, enabled: enabled,
click: () => { click: () => {
workspace.editing?.dispatch?.("delete"); workspace.editing?.dispatch("delete");
}, },
}, },
], ],
+18 -8
View File
@@ -1,7 +1,9 @@
import { useWorkspace } from "@/contexts/workspace-context"; import { useWorkspace } from "@/contexts/workspace-context";
import { TreeGraphData } from "@/misc/b3type"; import { TreeGraphData, getNodeType } from "@/misc/b3type";
import { checkTreeData, toBreakWord } from "@/misc/b3util"; import { checkTreeData, cutWordTo, toBreakWord } from "@/misc/b3util";
import i18n from "@/misc/i18n"; import i18n from "@/misc/i18n";
import { isMacos } from "@/misc/keys";
import Path from "@/misc/path";
import G6 from "@antv/g6"; import G6 from "@antv/g6";
const NODE_COLORS: any = { const NODE_COLORS: any = {
@@ -73,7 +75,7 @@ G6.registerNode(
draw(cfg, group) { draw(cfg, group) {
const workspace = useWorkspace.getState(); const workspace = useWorkspace.getState();
const nodeDef = workspace.getNodeDef(cfg.name as string); const nodeDef = workspace.getNodeDef(cfg.name as string);
let classify = nodeDef.type || "Other"; let classify = getNodeType(nodeDef);
let color = nodeDef.color || NODE_COLORS[classify] || NODE_COLORS["Other"]; let color = nodeDef.color || NODE_COLORS[classify] || NODE_COLORS["Other"];
if ( if (
!workspace.hasNodeDef(cfg.name as string) || !workspace.hasNodeDef(cfg.name as string) ||
@@ -176,7 +178,11 @@ G6.registerNode(
}); });
// icon // icon
const img = nodeDef.icon ? workspace.workdir + "/" + nodeDef.icon : `./icons/${classify}.svg`; const img = nodeDef.icon
? process.env.VITE_DEV_SERVER_URL && isMacos
? `${Path.basename(workspace.workdir)}/${nodeDef.icon}`
: `${workspace.workdir}/${nodeDef.icon}`
: `./icons/${classify}.svg`;
addShape("image", { addShape("image", {
attrs: { attrs: {
x: 5, x: 5,
@@ -216,11 +222,13 @@ G6.registerNode(
name: "name-text", name: "name-text",
}); });
const x = 5; const x = 6;
let y = 24; let y = 24;
// desc text // desc text
const desc = cfg.desc || nodeDef.desc; let desc = (cfg.desc || nodeDef.desc) as string;
if (desc) { if (desc) {
desc = i18n.t("regnode.mark") + desc;
desc = cutWordTo(desc, 31);
addShape("text", { addShape("text", {
attrs: { attrs: {
textBaseline: "top", textBaseline: "top",
@@ -228,7 +236,7 @@ G6.registerNode(
y, y,
lineHeight: 20, lineHeight: 20,
fontWeight: 800, fontWeight: 800,
text: `${i18n.t("regnode.mark")}${desc}`, text: `${desc}`,
fill: "black", fill: "black",
}, },
name: "desc-text", name: "desc-text",
@@ -327,13 +335,15 @@ G6.registerNode(
} }
if (cfg.path) { if (cfg.path) {
let path = (i18n.t("regnode.subtree") + cfg.path) as string;
path = cutWordTo(path, 33);
addShape("text", { addShape("text", {
attrs: { attrs: {
textBaseline: "top", textBaseline: "top",
x, x,
y: y + 20, y: y + 20,
lineHeight: 20, lineHeight: 20,
text: `${i18n.t("regnode.subtree")}${cfg.path}`, text: `${path}`,
fill: "black", fill: "black",
}, },
name: "subtree-text", name: "subtree-text",
+7 -4
View File
@@ -44,7 +44,7 @@ export const Workspace: FC = () => {
const { width = 0, height = 0 } = useWindowSize(); const { width = 0, height = 0 } = useWindowSize();
const keysRef = useHotkeys<HTMLDivElement>( const keysRef = useHotkeys<HTMLDivElement>(
[Hotkey.Save, Hotkey.CloseEditor, Hotkey.Search, Hotkey.Build], [Hotkey.Save, Hotkey.CloseEditor, Hotkey.SearchTree, Hotkey.Build, Hotkey.SearchNode],
(event) => { (event) => {
if (isHotkeyPressed(Hotkey.Save)) { if (isHotkeyPressed(Hotkey.Save)) {
event.preventDefault(); event.preventDefault();
@@ -58,12 +58,15 @@ export const Workspace: FC = () => {
workspace.close(workspace.editing.path); workspace.close(workspace.editing.path);
} }
} }
} else if (isHotkeyPressed(Hotkey.Search)) { } else if (isHotkeyPressed(Hotkey.SearchTree)) {
event.preventDefault(); event.preventDefault();
workspace.onShowingSearch(true); workspace.onShowingSearch(true);
} else if (isHotkeyPressed(Hotkey.Build)) { } else if (isHotkeyPressed(Hotkey.Build)) {
event.preventDefault(); event.preventDefault();
workspace.buildProject(); workspace.buildProject();
} else if (isHotkeyPressed(Hotkey.SearchNode)) {
event.preventDefault();
workspace.editing?.dispatch("searchNode");
} }
} }
); );
@@ -95,7 +98,7 @@ export const Workspace: FC = () => {
<Button <Button
type="primary" type="primary"
onClick={() => { onClick={() => {
editor.dispatch?.("save"); editor.dispatch("save");
workspace.close(editor.path); workspace.close(editor.path);
alert.destroy(); alert.destroy();
keysRef.current?.focus(); keysRef.current?.focus();
@@ -163,7 +166,7 @@ export const Workspace: FC = () => {
<Button <Button
type="primary" type="primary"
onClick={() => { onClick={() => {
unsaves.forEach((editor) => editor.dispatch?.("save")); unsaves.forEach((editor) => editor.dispatch("save"));
alert.destroy(); alert.destroy();
window.close(); window.close();
setShowingSave(false); setShowingSave(false);
+6 -40
View File
@@ -32,6 +32,7 @@ export type EditEvent =
| "rename" | "rename"
| "updateTree" | "updateTree"
| "updateNode" | "updateNode"
| "searchNode"
| "editSubtree" | "editSubtree"
| "saveAsSubtree"; | "saveAsSubtree";
@@ -53,7 +54,7 @@ export class EditorStore {
graphMatrix?: Matrix; graphMatrix?: Matrix;
graph!: TreeGraph; graph!: TreeGraph;
dispatch?: (event: EditEvent, data?: unknown) => void; dispatch!: (event: EditEvent, data?: unknown) => void;
constructor(path: string) { constructor(path: string) {
this.path = path; this.path = path;
@@ -79,15 +80,6 @@ export type FileTreeType = {
style?: React.CSSProperties; style?: React.CSSProperties;
}; };
export type NodeTreeType = {
title: string;
def?: NodeDef;
icon?: React.ReactNode;
isLeaf?: boolean;
children?: NodeTreeType[];
style?: React.CSSProperties;
};
export type EditNode = { export type EditNode = {
data: NodeModel; data: NodeModel;
editable: boolean; editable: boolean;
@@ -112,7 +104,6 @@ export type WorkspaceStore = {
path: string; path: string;
allFiles: string[]; allFiles: string[];
nodeTree?: NodeTreeType;
fileTree?: FileTreeType; fileTree?: FileTreeType;
editors: EditorStore[]; editors: EditorStore[];
editing?: EditorStore; editing?: EditorStore;
@@ -191,7 +182,7 @@ const loadFileTree = (workdir: string, filename: string) => {
const saveFile = (editor?: EditorStore) => { const saveFile = (editor?: EditorStore) => {
if (editor?.unsave) { if (editor?.unsave) {
editor.dispatch?.("save"); editor.dispatch("save");
} }
}; };
@@ -260,7 +251,7 @@ export const useWorkspace = create<WorkspaceStore>((set, get) => ({
} }
if (buildDir) { if (buildDir) {
for (const editor of workspace.editors) { for (const editor of workspace.editors) {
editor.dispatch?.("save"); editor.dispatch("save");
} }
try { try {
let hasError = false; let hasError = false;
@@ -452,37 +443,12 @@ export const useWorkspace = create<WorkspaceStore>((set, get) => ({
loadNodeDefs: () => { loadNodeDefs: () => {
const workspace = get(); const workspace = get();
const nodeDefData = readJson(`${workspace.workdir}/node-config.b3-setting`) as NodeDef[]; const nodeDefData = readJson(`${workspace.workdir}/node-config.b3-setting`) as NodeDef[];
const nodeTree: NodeTreeType = {
title: i18n.t("nodeDefinition"),
children: [],
style: {
fontWeight: "bold",
fontSize: "13px",
},
};
const nodeDefs: Map<string, NodeDef> = new Map(); const nodeDefs: Map<string, NodeDef> = new Map();
for (const v of nodeDefData) { for (const v of nodeDefData) {
nodeDefs.set(v.name, v); nodeDefs.set(v.name, v);
let catalog = nodeTree.children?.find((nt) => nt.title === v.type);
if (!catalog) {
catalog = {
title: v.type,
children: [],
};
nodeTree.children?.push(catalog);
}
catalog.children?.push({
title: `${v.name}(${v.desc})`,
isLeaf: true,
def: v,
});
} }
nodeTree.children?.sort((a, b) => a.title.localeCompare(b.title)); set({ nodeDefs });
nodeTree.children?.forEach((child) => workspace.editing?.dispatch("reload");
child.children?.sort((a, b) => a.title.localeCompare(b.title))
);
set({ nodeDefs, nodeTree });
workspace.editing?.dispatch?.("reload");
}, },
// node edit // node edit
+17 -1
View File
@@ -1,6 +1,7 @@
import { TreeGraphData as G6TreeGraphData } from "@antv/g6"; import { TreeGraphData as G6TreeGraphData } from "@antv/g6";
export type NodeArgType = "string" | "int" | "float" | "boolean" | "enum" | "code"; export type NodeArgType = "string" | "int" | "float" | "boolean" | "enum" | "code";
export type NodeType = "Action" | "Composite" | "Decorator" | "Condition" | "Other" | "Error";
export interface NodeArgOption { export interface NodeArgOption {
name: string; name: string;
@@ -16,7 +17,7 @@ export interface NodeArg {
} }
export interface NodeDef { export interface NodeDef {
name: string; name: string;
type: string; type: NodeType;
desc?: string; desc?: string;
args?: NodeArg[]; args?: NodeArg[];
input?: string[]; input?: string[];
@@ -67,3 +68,18 @@ export const unknownNodeDef: NodeDef = {
desc: "新建节点", desc: "新建节点",
type: "Action", type: "Action",
}; };
export const getNodeType = (def: NodeDef): NodeType => {
const type = def.type.toLocaleLowerCase().toString();
if (type.startsWith("action")) {
return "Action";
} else if (type.startsWith("composite")) {
return "Composite";
} else if (type.startsWith("decorator")) {
return "Decorator";
} else if (type.startsWith("condition")) {
return "Condition";
} else {
return "Other";
}
};
+29 -11
View File
@@ -368,24 +368,42 @@ export const createNewTree = (filename: string) => {
return tree; return tree;
}; };
export const toBreakWord = (str: string, len: number, char = "\n") => { const isAsciiChar = (c: number) => {
let strTemp = ""; return (c >= 0x0001 && c <= 0x007e) || (0xff60 <= c && c <= 0xff9f);
};
export const toBreakWord = (str: string, maxlen: number, char = "\n") => {
const chars: string[] = [];
let line = 1; let line = 1;
if (str.length <= len) { let len = 0;
return { str, line }; for (let i = 0; i < str.length; i++) {
const c = str.charCodeAt(i);
len += isAsciiChar(c) ? 1 : 2;
chars.push(String.fromCharCode(c));
if (len >= maxlen) {
len = 0;
line++;
chars.push("\n");
}
} }
while (str.length > len) {
strTemp += str.slice(0, len) + char;
str = str.slice(len, str.length);
line++;
}
strTemp += str;
return { return {
str: strTemp, str: chars.join(""),
line, line,
}; };
}; };
export const cutWordTo = (str: string, maxlen: number) => {
let i = 0;
for (; i < str.length; i++) {
const c = str.charCodeAt(i);
maxlen -= isAsciiChar(c) ? 1 : 2;
if (maxlen <= 0) {
break;
}
}
return str.slice(0, i) + (i < str.length - 1 ? "..." : "");
};
export const createProject = (path: string) => { export const createProject = (path: string) => {
fs.writeFileSync( fs.writeFileSync(
Path.dirname(path) + "/node-config.b3-setting", Path.dirname(path) + "/node-config.b3-setting",
+2 -1
View File
@@ -57,7 +57,8 @@ export const Hotkey = {
Redo: isMacos ? hotkey("shift+ctrl+z") : hotkey("ctrl+y"), Redo: isMacos ? hotkey("shift+ctrl+z") : hotkey("ctrl+y"),
Replace: hotkey("shift+ctrl+v"), Replace: hotkey("shift+ctrl+v"),
Save: hotkey("ctrl+s"), Save: hotkey("ctrl+s"),
Search: hotkey("ctrl+p"), SearchTree: hotkey("ctrl+p"),
SearchNode: hotkey("ctrl+f"),
Undo: hotkey("ctrl+z"), Undo: hotkey("ctrl+z"),
}; };