From 543c5b09c35fa528e175a5a2c9542bc63307e542 Mon Sep 17 00:00:00 2001 From: codetypes Date: Wed, 24 Apr 2024 22:33:57 +0800 Subject: [PATCH 1/4] support group by type for node def --- behavior3editor.code-workspace | 19 +++--- sample/node-config.b3-setting | 4 +- src/components/explorer.tsx | 99 ++++++++++++++++++++----------- src/components/inspector.tsx | 9 +++ src/components/register-node.ts | 12 +++- src/contexts/workspace-context.ts | 37 +----------- src/misc/b3type.ts | 18 +++++- 7 files changed, 110 insertions(+), 88 deletions(-) diff --git a/behavior3editor.code-workspace b/behavior3editor.code-workspace index 5c6e990..38d027f 100644 --- a/behavior3editor.code-workspace +++ b/behavior3editor.code-workspace @@ -1,14 +1,11 @@ { - "folders": [ - { - "path": "." - } - ], - "settings": { - "editor.formatOnSave": true, - "editor.defaultFormatter": "esbenp.prettier-vscode", - "[json]": { - "editor.formatOnSave": false - } + "folders": [ + { + "path": "." } + ], + "settings": { + "editor.formatOnSave": true, + "editor.defaultFormatter": "esbenp.prettier-vscode" + } } diff --git a/sample/node-config.b3-setting b/sample/node-config.b3-setting index ce58d1f..cab684b 100644 --- a/sample/node-config.b3-setting +++ b/sample/node-config.b3-setting @@ -124,7 +124,7 @@ }, { "name": "MoveToPos", - "type": "Action", + "type": "Action (Client)", "args": [ { "name": "x", @@ -141,7 +141,7 @@ }, { "name": "MoveToTarget", - "type": "Action", + "type": "Action (Server)", "input": [ "{目标}" ], diff --git a/src/components/explorer.tsx b/src/components/explorer.tsx index e73d3c8..1c0ea54 100644 --- a/src/components/explorer.tsx +++ b/src/components/explorer.tsx @@ -1,4 +1,5 @@ import { FileTreeType, useWorkspace } from "@/contexts/workspace-context"; +import { NodeDef, getNodeType } from "@/misc/b3type"; import * as b3util from "@/misc/b3util"; import { modal } from "@/misc/hooks"; import { Hotkey, isHotkeyPressed, isMacos, useHotkeys } from "@/misc/keys"; @@ -31,6 +32,15 @@ type MenuEvent = | "move" | "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 => { if (!path) { return; @@ -112,7 +122,6 @@ export const Explorer: FC = () => { editors: useWorkspace((state) => state.editors), fileTree: useWorkspace((state) => state.fileTree), nodeDefs: useWorkspace((state) => state.nodeDefs), - nodeTree: useWorkspace((state) => state.nodeTree), workdir: useWorkspace((state) => state.workdir), onEditingNodeDef: useWorkspace((state) => state.onEditingNodeDef), open: useWorkspace((state) => state.open), @@ -134,49 +143,69 @@ export const Explorer: FC = () => { ); } - if (workspace.nodeTree) { - workspace.nodeTree.title = t("nodeDefinition"); - workspace.nodeTree.icon = ( - - - - ); - workspace.nodeTree.children?.forEach((catalog) => { - catalog.icon = ( + const nodeTree = useMemo(() => { + const data: NodeTreeType = { + title: t("nodeDefinition"), + icon: ( - + - ); - catalog.children?.forEach((node) => { - if (node.def?.icon) { - node.icon = ( + ), + children: [], + style: { + 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: ( - ); - } else { - node.icon = ( - - {" "} - - ); - } + ), + }; + data.children?.push(catalog); + } + catalog.children?.push({ + title: `${nodeDef.name}(${nodeDef.desc})`, + isLeaf: true, + def: nodeDef, + icon: nodeDef.icon ? ( + + + + ) : ( + + {" "} + + ), }); }); - } + 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(() => { if (workspace.editing) { @@ -795,7 +824,7 @@ export const Explorer: FC = () => { !!node.isLeaf }} onSelect={(_, info) => { const node = info.node; diff --git a/src/components/inspector.tsx b/src/components/inspector.tsx index d8d5818..d3c0257 100644 --- a/src/components/inspector.tsx +++ b/src/components/inspector.tsx @@ -80,6 +80,7 @@ const NodeInspector: FC = () => { form.resetFields(); form.setFieldValue("id", data.id); form.setFieldValue("name", data.name); + form.setFieldValue("type", def.type); form.setFieldValue("desc", data.desc); form.setFieldValue("debug", data.debug); form.setFieldValue("path", data.path); @@ -227,6 +228,14 @@ const NodeInspector: FC = () => { > + {t("node.type")} + } + > + + ((set, get) => ({ loadNodeDefs: () => { const workspace = get(); 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 = new Map(); for (const v of nodeDefData) { 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)); - nodeTree.children?.forEach((child) => - child.children?.sort((a, b) => a.title.localeCompare(b.title)) - ); - set({ nodeDefs, nodeTree }); + set({ nodeDefs }); workspace.editing?.dispatch?.("reload"); }, diff --git a/src/misc/b3type.ts b/src/misc/b3type.ts index 3b0cc82..749724b 100644 --- a/src/misc/b3type.ts +++ b/src/misc/b3type.ts @@ -1,6 +1,7 @@ import { TreeGraphData as G6TreeGraphData } from "@antv/g6"; export type NodeArgType = "string" | "int" | "float" | "boolean" | "enum" | "code"; +export type NodeType = "Action" | "Composite" | "Decorator" | "Condition" | "Other" | "Error"; export interface NodeArgOption { name: string; @@ -16,7 +17,7 @@ export interface NodeArg { } export interface NodeDef { name: string; - type: string; + type: NodeType; desc?: string; args?: NodeArg[]; input?: string[]; @@ -67,3 +68,18 @@ export const unknownNodeDef: NodeDef = { desc: "新建节点", 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"; + } +}; From 7dbea171bb538f910eafe0eb870176236a18b9c0 Mon Sep 17 00:00:00 2001 From: codetypes Date: Thu, 25 Apr 2024 23:59:49 +0800 Subject: [PATCH 2/4] optimize break word --- src/components/register-node.ts | 14 +++++++----- src/misc/b3util.ts | 40 ++++++++++++++++++++++++--------- 2 files changed, 38 insertions(+), 16 deletions(-) diff --git a/src/components/register-node.ts b/src/components/register-node.ts index cdccbba..00fb589 100644 --- a/src/components/register-node.ts +++ b/src/components/register-node.ts @@ -1,6 +1,6 @@ import { useWorkspace } from "@/contexts/workspace-context"; 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 { isMacos } from "@/misc/keys"; import Path from "@/misc/path"; @@ -222,11 +222,13 @@ G6.registerNode( name: "name-text", }); - const x = 5; + const x = 6; let y = 24; // desc text - const desc = cfg.desc || nodeDef.desc; + let desc = (cfg.desc || nodeDef.desc) as string; if (desc) { + desc = i18n.t("regnode.mark") + desc; + desc = cutWordTo(desc, 31); addShape("text", { attrs: { textBaseline: "top", @@ -234,7 +236,7 @@ G6.registerNode( y, lineHeight: 20, fontWeight: 800, - text: `${i18n.t("regnode.mark")}${desc}`, + text: `${desc}`, fill: "black", }, name: "desc-text", @@ -333,13 +335,15 @@ G6.registerNode( } if (cfg.path) { + let path = (i18n.t("regnode.subtree") + cfg.path) as string; + path = cutWordTo(path, 33); addShape("text", { attrs: { textBaseline: "top", x, y: y + 20, lineHeight: 20, - text: `${i18n.t("regnode.subtree")}${cfg.path}`, + text: `${path}`, fill: "black", }, name: "subtree-text", diff --git a/src/misc/b3util.ts b/src/misc/b3util.ts index 2f8ea43..db956ac 100644 --- a/src/misc/b3util.ts +++ b/src/misc/b3util.ts @@ -368,24 +368,42 @@ export const createNewTree = (filename: string) => { return tree; }; -export const toBreakWord = (str: string, len: number, char = "\n") => { - let strTemp = ""; +const isAsciiChar = (c: number) => { + return (c >= 0x0001 && c <= 0x007e) || (0xff60 <= c && c <= 0xff9f); +}; + +export const toBreakWord = (str: string, maxlen: number, char = "\n") => { + const chars: string[] = []; let line = 1; - if (str.length <= len) { - return { str, line }; + let len = 0; + 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 { - str: strTemp, + str: chars.join(""), 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) => { fs.writeFileSync( Path.dirname(path) + "/node-config.b3-setting", From 82d1d8931183c49c979cc355b2811955a536db9e Mon Sep 17 00:00:00 2001 From: codetypes Date: Fri, 26 Apr 2024 09:37:49 +0800 Subject: [PATCH 3/4] support search node --- package-lock.json | 4 +- package.json | 2 +- src/components/editor.tsx | 106 +++++++++++++++++++++++++++++- src/components/workspace.tsx | 4 +- src/contexts/workspace-context.ts | 1 + src/misc/keys.ts | 3 +- 6 files changed, 112 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7590aa9..9bf5649 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "behavior3editor", - "version": "1.1.2", + "version": "1.1.3", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "behavior3editor", - "version": "1.1.2", + "version": "1.1.3", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 411038a..f0b5159 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "behavior3editor", - "version": "1.1.2", + "version": "1.1.3", "main": "dist-electron/main/index.js", "description": "行为树编辑器", "author": "zhandouxiaojiji & codetypes", diff --git a/src/components/editor.tsx b/src/components/editor.tsx index 93dfbd7..c79d6b4 100644 --- a/src/components/editor.tsx +++ b/src/components/editor.tsx @@ -10,17 +10,19 @@ import * as b3util from "@/misc/b3util"; import { message } from "@/misc/hooks"; import { Hotkey, isHotkeyPressed, isMacos, useHotkeys } from "@/misc/keys"; import Path from "@/misc/path"; +import { ArrowDownOutlined, ArrowUpOutlined, CloseOutlined } from "@ant-design/icons"; import G6, { G6GraphEvent, Item, TreeGraph } from "@antv/g6"; import { dialog } from "@electron/remote"; 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 * 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 { FiDelete } from "react-icons/fi"; import { IoMdReturnLeft } from "react-icons/io"; import { mergeRefs } from "react-merge-refs"; +import { useDebounceCallback } from "usehooks-ts"; import "./register-node"; export interface EditorProps extends React.HTMLAttributes { @@ -36,11 +38,27 @@ export const Editor: FC = ({ onUpdate: updateState, data: editor, . open: useWorkspace((state) => state.open), workdir: useWorkspace((state) => state.workdir), }; + const [results, setResults] = useState([]); + const [resultIndex, setResultIndex] = useState(0); + const [showingSearch, setShowingSearch] = useState(false); const graphRef = useRef(null); const sizeRef = useRef(null); const editorSize = useSize(sizeRef); 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 = () => { if (!editor.unsave) { editor.unsave = true; @@ -93,6 +111,17 @@ export const Editor: FC = ({ onUpdate: updateState, data: editor, . 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 => { if (ancestor.id === node.parent) { return true; @@ -761,6 +790,7 @@ export const Editor: FC = ({ onUpdate: updateState, data: editor, . Hotkey.Delete, Hotkey.Enter, Hotkey.Backspace, + Hotkey.SearchNode, ], (event) => { event.preventDefault(); @@ -778,6 +808,8 @@ export const Editor: FC = ({ onUpdate: updateState, data: editor, . createNode(); } else if (isHotkeyPressed(Hotkey.Delete) || isHotkeyPressed(Hotkey.Backspace)) { deleteNode(); + } else if (isHotkeyPressed(Hotkey.SearchNode)) { + setShowingSearch(true); } } ); @@ -967,6 +999,76 @@ export const Editor: FC = ({ onUpdate: updateState, data: editor, . tabIndex={-1} style={{ maxWidth: "inherit", maxHeight: "inherit" }} > + {showingSearch && ( + + onSearchChange(e.currentTarget.value)} + /> +
+ {results.length ? `${resultIndex + 1}/${results.length}` : ""} +
+