From e03b106652b3c19398f62125e00ccc038111b8df Mon Sep 17 00:00:00 2001 From: YHH <359807859@qq.com> Date: Tue, 4 Nov 2025 23:53:26 +0800 Subject: [PATCH] =?UTF-8?q?refactor(editor):=20=E4=BC=98=E5=8C=96=E5=B8=83?= =?UTF-8?q?=E5=B1=80=E7=AE=A1=E7=90=86=E5=92=8C=E8=A1=8C=E4=B8=BA=E6=A0=91?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/editor-app/src/App.tsx | 117 ++++--- .../src/components/AssetBrowser.tsx | 81 +++-- .../editor-app/src/components/FileTree.tsx | 136 +++++-- .../components/FlexLayoutDockContainer.tsx | 331 +++++++++++++++++- .../src/plugins/BehaviorTreePlugin.ts | 89 +++-- .../editor-app/src/plugins/ProfilerPlugin.tsx | 2 +- .../panels/BehaviorTreeEditorPanel.css | 63 ++-- .../panels/BehaviorTreeEditorPanel.tsx | 132 ++++--- .../src/services/BehaviorTreeFileService.ts | 60 ++++ .../src/stores/behaviorTreeStore.ts | 19 +- .../editor-app/src/styles/AssetBrowser.css | 65 +++- .../editor-app/src/styles/EntityInspector.css | 8 +- packages/editor-app/src/styles/FileTree.css | 15 + .../editor-app/src/styles/FlexLayoutDock.css | 56 ++- .../editor-app/src/styles/SceneHierarchy.css | 27 +- 15 files changed, 958 insertions(+), 243 deletions(-) create mode 100644 packages/editor-app/src/services/BehaviorTreeFileService.ts diff --git a/packages/editor-app/src/App.tsx b/packages/editor-app/src/App.tsx index 12fb2eec..f5f8619c 100644 --- a/packages/editor-app/src/App.tsx +++ b/packages/editor-app/src/App.tsx @@ -1,5 +1,5 @@ import { useState, useEffect, useRef, useCallback } from 'react'; -import { Core, Scene } from '@esengine/ecs-framework'; +import { Core, Scene, createLogger } from '@esengine/ecs-framework'; import * as ECSFramework from '@esengine/ecs-framework'; import { EditorPluginManager, UIRegistry, MessageHub, SerializerRegistry, EntityStoreService, ComponentRegistry, LocaleService, ProjectService, ComponentDiscoveryService, PropertyMetadataService, LogService, SettingsRegistry, SceneManagerService, FileActionRegistry, PanelDescriptor } from '@esengine/editor-core'; import { GlobalBlackboardService } from '@esengine/behavior-tree'; @@ -40,9 +40,10 @@ localeService.registerTranslations('en', en); localeService.registerTranslations('zh', zh); Core.services.registerInstance(LocaleService, localeService); -// 注册全局黑板服务 Core.services.registerSingleton(GlobalBlackboardService); +const logger = createLogger('App'); + function App() { const initRef = useRef(false); const pluginLoaderRef = useRef(new PluginLoader()); @@ -77,8 +78,11 @@ function App() { confirmText: string; cancelText: string; onConfirm: () => void; - } | null>(null); + } | null>(null); const [activeDynamicPanels, setActiveDynamicPanels] = useState([]); + const [activePanelId, setActivePanelId] = useState(undefined); + const [dynamicPanelTitles, setDynamicPanelTitles] = useState>(new Map()); + const [isEditorFullscreen, setIsEditorFullscreen] = useState(false); useEffect(() => { // 禁用默认右键菜单 @@ -234,14 +238,34 @@ function App() { if (!messageHub) return; const unsubscribe = messageHub.subscribe('dynamic-panel:open', (data: any) => { - const { panelId } = data; - console.log('[App] Opening dynamic panel:', panelId); - setActiveDynamicPanels(prev => { - if (prev.includes(panelId)) { - return prev; - } - return [...prev, panelId]; + const { panelId, title } = data; + logger.info('Opening dynamic panel:', panelId, 'with title:', title); + setActiveDynamicPanels((prev) => { + const newPanels = prev.includes(panelId) ? prev : [...prev, panelId]; + return newPanels; }); + setActivePanelId(panelId); + + // 更新动态面板标题 + if (title) { + setDynamicPanelTitles((prev) => { + const newTitles = new Map(prev); + newTitles.set(panelId, title); + return newTitles; + }); + } + }); + + return () => unsubscribe?.(); + }, [messageHub]); + + useEffect(() => { + if (!messageHub) return; + + const unsubscribe = messageHub.subscribe('editor:fullscreen', (data: any) => { + const { fullscreen } = data; + logger.info('Editor fullscreen state changed:', fullscreen); + setIsEditorFullscreen(fullscreen); }); return () => unsubscribe?.(); @@ -632,16 +656,19 @@ function App() { // 添加激活的动态面板 const dynamicPanels: FlexDockPanel[] = activeDynamicPanels - .filter(panelId => { + .filter((panelId) => { const panelDesc = uiRegistry.getPanel(panelId); return panelDesc && panelDesc.component; }) - .map(panelId => { + .map((panelId) => { const panelDesc = uiRegistry.getPanel(panelId)!; const Component = panelDesc.component; + // 优先使用动态标题,否则使用默认标题 + const customTitle = dynamicPanelTitles.get(panelId); + const defaultTitle = (panelDesc as any).titleZh && locale === 'zh' ? (panelDesc as any).titleZh : panelDesc.title; return { id: panelDesc.id, - title: (panelDesc as any).titleZh && locale === 'zh' ? (panelDesc as any).titleZh : panelDesc.title, + title: customTitle || defaultTitle, content: , closable: panelDesc.closable ?? true }; @@ -651,7 +678,7 @@ function App() { console.log('[App] Loading dynamic panels:', dynamicPanels); setPanels([...corePanels, ...pluginPanels, ...dynamicPanels]); } - }, [projectLoaded, entityStore, messageHub, logService, uiRegistry, pluginManager, locale, currentProjectPath, t, pluginUpdateTrigger, isProfilerMode, handleOpenSceneByPath, activeDynamicPanels]); + }, [projectLoaded, entityStore, messageHub, logService, uiRegistry, pluginManager, locale, currentProjectPath, t, pluginUpdateTrigger, isProfilerMode, handleOpenSceneByPath, activeDynamicPanels, dynamicPanelTitles]); if (!initialized) { @@ -708,42 +735,44 @@ function App() { return (
-
- setShowPluginManager(true)} - onOpenProfiler={() => setShowProfiler(true)} - onOpenPortManager={() => setShowPortManager(true)} - onOpenSettings={() => setShowSettings(true)} - onToggleDevtools={handleToggleDevtools} - onOpenAbout={handleOpenAbout} - onCreatePlugin={handleCreatePlugin} - /> -
- - {status} + {!isEditorFullscreen && ( +
+ setShowPluginManager(true)} + onOpenProfiler={() => setShowProfiler(true)} + onOpenPortManager={() => setShowPortManager(true)} + onOpenSettings={() => setShowSettings(true)} + onToggleDevtools={handleToggleDevtools} + onOpenAbout={handleOpenAbout} + onCreatePlugin={handleCreatePlugin} + /> +
+ + {status} +
-
+ )}
{ - console.log('[App] Panel closed:', panelId); - // 从激活的动态面板列表中移除 - setActiveDynamicPanels(prev => prev.filter(id => id !== panelId)); + logger.info('Panel closed:', panelId); + setActiveDynamicPanels((prev) => prev.filter((id) => id !== panelId)); }} />
diff --git a/packages/editor-app/src/components/AssetBrowser.tsx b/packages/editor-app/src/components/AssetBrowser.tsx index aef645f8..5f497d42 100644 --- a/packages/editor-app/src/components/AssetBrowser.tsx +++ b/packages/editor-app/src/components/AssetBrowser.tsx @@ -1,9 +1,9 @@ -import { useState, useEffect } from 'react'; -import { Folder, File, FileCode, FileJson, FileImage, FileText, FolderOpen, Copy, Trash2, Edit3, LayoutGrid, List } from 'lucide-react'; +import { useState, useEffect, useRef } from 'react'; +import { Folder, File, FileCode, FileJson, FileImage, FileText, FolderOpen, Copy, Trash2, Edit3, LayoutGrid, List, ChevronsUp } from 'lucide-react'; import { Core } from '@esengine/ecs-framework'; import { MessageHub, FileActionRegistry } from '@esengine/editor-core'; import { TauriAPI, DirectoryEntry } from '../api/tauri'; -import { FileTree } from './FileTree'; +import { FileTree, FileTreeHandle } from './FileTree'; import { ResizablePanel } from './ResizablePanel'; import { ContextMenu, ContextMenuItem } from './ContextMenu'; import '../styles/AssetBrowser.css'; @@ -26,6 +26,8 @@ interface AssetBrowserProps { export function AssetBrowser({ projectPath, locale, onOpenScene }: AssetBrowserProps) { const messageHub = Core.services.resolve(MessageHub); const fileActionRegistry = Core.services.resolve(FileActionRegistry); + const detailViewFileTreeRef = useRef(null); + const treeOnlyViewFileTreeRef = useRef(null); const [currentPath, setCurrentPath] = useState(null); const [selectedPath, setSelectedPath] = useState(null); const [assets, setAssets] = useState([]); @@ -369,6 +371,11 @@ export function AssetBrowser({ projectPath, locale, onOpenScene }: AssetBrowserP const getFileIcon = (asset: AssetItem) => { if (asset.type === 'folder') { + // 检查是否为框架专用文件夹 + const folderName = asset.name.toLowerCase(); + if (folderName === 'plugins' || folderName === '.ecs') { + return ; + } return ; } @@ -421,56 +428,60 @@ export function AssetBrowser({ projectPath, locale, onOpenScene }: AssetBrowserP background: '#252526', alignItems: 'center' }}> -
+
+ void; +} + +export const FileTree = forwardRef(({ rootPath, onSelectFile, selectedPath, messageHub, searchQuery, showFiles = true }, ref) => { const [tree, setTree] = useState([]); const [loading, setLoading] = useState(false); const [internalSelectedPath, setInternalSelectedPath] = useState(null); @@ -48,6 +52,26 @@ export function FileTree({ rootPath, onSelectFile, selectedPath, messageHub, sea const [filteredTree, setFilteredTree] = useState([]); const fileActionRegistry = Core.services.resolve(FileActionRegistry); + const collapseAll = () => { + const collapseNode = (node: TreeNode): TreeNode => { + if (node.type === 'folder') { + return { + ...node, + expanded: false, + children: node.children ? node.children.map(collapseNode) : node.children + }; + } + return node; + }; + + const collapsedTree = tree.map(node => collapseNode(node)); + setTree(collapsedTree); + }; + + useImperativeHandle(ref, () => ({ + collapseAll + })); + useEffect(() => { if (rootPath) { loadRootDirectory(rootPath); @@ -214,8 +238,72 @@ export function FileTree({ rootPath, onSelectFile, selectedPath, messageHub, sea }; const refreshTree = async () => { - if (rootPath) { - await loadRootDirectory(rootPath); + if (!rootPath) return; + + // 保存当前展开状态 + const expandedPaths = new Set(); + const collectExpandedPaths = (nodes: TreeNode[]) => { + for (const node of nodes) { + if (node.type === 'folder' && node.expanded) { + expandedPaths.add(node.path); + if (node.children) { + collectExpandedPaths(node.children); + } + } + } + }; + collectExpandedPaths(tree); + + // 重新加载根目录,获取最新的文件结构 + try { + const entries = await TauriAPI.listDirectory(rootPath); + const children = entriesToNodes(entries); + + const rootName = rootPath.split(/[/\\]/).filter((p) => p).pop() || 'Project'; + let rootNode: TreeNode = { + name: rootName, + path: rootPath, + type: 'folder', + children: children, + expanded: true, + loaded: true + }; + + // 恢复展开状态 + if (expandedPaths.size > 0) { + const restoreExpandedState = async (node: TreeNode): Promise => { + if (node.type === 'folder' && expandedPaths.has(node.path)) { + let children = node.children || []; + if (!node.loaded && node.children) { + children = await loadChildren(node); + } + const restoredChildren = await Promise.all( + children.map(child => restoreExpandedState(child)) + ); + return { + ...node, + expanded: true, + loaded: true, + children: restoredChildren + }; + } else if (node.type === 'folder' && node.children) { + const restoredChildren = await Promise.all( + node.children.map(child => restoreExpandedState(child)) + ); + return { + ...node, + children: restoredChildren + }; + } + return node; + }; + + rootNode = await restoreExpandedState(rootNode); + } + + setTree([rootNode]); + } catch (error) { + console.error('Failed to refresh directory:', error); } }; @@ -252,22 +340,6 @@ export function FileTree({ rootPath, onSelectFile, selectedPath, messageHub, sea setTree(expandedTree); }; - const collapseAll = () => { - const collapseNode = (node: TreeNode): TreeNode => { - if (node.type === 'folder') { - return { - ...node, - expanded: false, - children: node.children ? node.children.map(collapseNode) : node.children - }; - } - return node; - }; - - const collapsedTree = tree.map(node => collapseNode(node)); - setTree(collapsedTree); - }; - const handleRename = async (node: TreeNode) => { if (!newName || newName === node.name) { setRenamingNode(null); @@ -570,7 +642,11 @@ export function FileTree({ rootPath, onSelectFile, selectedPath, messageHub, sea {node.type === 'folder' ? ( - + node.name.toLowerCase() === 'plugins' || node.name.toLowerCase() === '.ecs' ? ( + + ) : ( + + ) ) : ( )} @@ -615,22 +691,6 @@ export function FileTree({ rootPath, onSelectFile, selectedPath, messageHub, sea return ( <> -
- - -
{ @@ -688,4 +748,4 @@ export function FileTree({ rootPath, onSelectFile, selectedPath, messageHub, sea )} ); -} +}); diff --git a/packages/editor-app/src/components/FlexLayoutDockContainer.tsx b/packages/editor-app/src/components/FlexLayoutDockContainer.tsx index a217fb4f..5b1663fe 100644 --- a/packages/editor-app/src/components/FlexLayoutDockContainer.tsx +++ b/packages/editor-app/src/components/FlexLayoutDockContainer.tsx @@ -1,8 +1,143 @@ -import { useCallback, ReactNode, useMemo } from 'react'; -import { Layout, Model, TabNode, IJsonModel, Actions, IJsonTabSetNode, IJsonRowNode } from 'flexlayout-react'; +import { useCallback, ReactNode, useRef, useEffect, useState } from 'react'; +import { Layout, Model, TabNode, IJsonModel, Actions, IJsonTabSetNode, IJsonRowNode, Action, IJsonTabNode, DockLocation } from 'flexlayout-react'; import 'flexlayout-react/style/light.css'; import '../styles/FlexLayoutDock.css'; +/** + * 合并保存的布局和新的默认布局 + * 保留用户的布局调整(大小、位置等),同时添加新面板并移除已关闭的面板 + */ +function mergeLayouts(savedLayout: IJsonModel, defaultLayout: IJsonModel, currentPanels: FlexDockPanel[]): IJsonModel { + // 获取当前所有面板ID + const currentPanelIds = new Set(currentPanels.map(p => p.id)); + + // 收集保存布局中存在的面板ID + const savedPanelIds = new Set(); + const collectPanelIds = (node: any) => { + if (node.type === 'tab' && node.id) { + savedPanelIds.add(node.id); + } + if (node.children) { + node.children.forEach((child: any) => collectPanelIds(child)); + } + }; + collectPanelIds(savedLayout.layout); + + // 同时收集borders中的面板ID + if (savedLayout.borders) { + savedLayout.borders.forEach((border: any) => { + if (border.children) { + collectPanelIds({ children: border.children }); + } + }); + } + + // 找出新增的面板和已移除的面板 + const newPanelIds = Array.from(currentPanelIds).filter(id => !savedPanelIds.has(id)); + const removedPanelIds = Array.from(savedPanelIds).filter(id => !currentPanelIds.has(id)); + + // 克隆保存的布局 + const mergedLayout = JSON.parse(JSON.stringify(savedLayout)); + + // 确保borders为空(不保留最小化状态) + if (mergedLayout.borders) { + mergedLayout.borders = mergedLayout.borders.map((border: any) => ({ + ...border, + children: [] + })); + } + + // 第一步:移除已关闭的面板 + if (removedPanelIds.length > 0) { + const removePanels = (node: any): boolean => { + if (!node.children) return false; + + // 过滤掉已移除的tab + if (node.type === 'tabset' || node.type === 'row') { + const originalLength = node.children.length; + node.children = node.children.filter((child: any) => { + if (child.type === 'tab') { + return !removedPanelIds.includes(child.id); + } + return true; + }); + + // 如果有tab被移除,调整selected索引 + if (node.type === 'tabset' && node.children.length < originalLength) { + if (node.selected >= node.children.length) { + node.selected = Math.max(0, node.children.length - 1); + } + } + + // 递归处理子节点 + node.children.forEach((child: any) => removePanels(child)); + + return node.children.length < originalLength; + } + + return false; + }; + removePanels(mergedLayout.layout); + } + + // 第二步:如果没有新面板,直接返回清理后的布局 + if (newPanelIds.length === 0) { + return mergedLayout; + } + + // 第三步:在默认布局中找到新面板的配置 + const newPanelTabs: IJsonTabNode[] = []; + const findNewPanels = (node: any) => { + if (node.type === 'tab' && node.id && newPanelIds.includes(node.id)) { + newPanelTabs.push(node); + } + if (node.children) { + node.children.forEach((child: any) => findNewPanels(child)); + } + }; + findNewPanels(defaultLayout.layout); + + // 第四步:将新面板添加到中心区域的第一个tabset + const addNewPanelsToCenter = (node: any): boolean => { + if (node.type === 'tabset') { + // 检查是否是中心区域的tabset(通过检查是否包含非hierarchy/asset/inspector/console面板) + const hasNonSidePanel = node.children?.some((child: any) => { + const id = child.id || ''; + return !id.includes('hierarchy') && + !id.includes('asset') && + !id.includes('inspector') && + !id.includes('console'); + }); + + if (hasNonSidePanel && node.children) { + // 添加新面板到这个tabset + node.children.push(...newPanelTabs); + // 选中最后添加的面板 + node.selected = node.children.length - 1; + return true; + } + } + + if (node.children) { + for (const child of node.children) { + if (addNewPanelsToCenter(child)) { + return true; + } + } + } + + return false; + }; + + // 尝试添加新面板到中心区域 + if (!addNewPanelsToCenter(mergedLayout.layout)) { + // 如果没有找到合适的tabset,使用默认布局 + return defaultLayout; + } + + return mergedLayout; +} + export interface FlexDockPanel { id: string; title: string; @@ -13,9 +148,15 @@ export interface FlexDockPanel { interface FlexLayoutDockContainerProps { panels: FlexDockPanel[]; onPanelClose?: (panelId: string) => void; + activePanelId?: string; } -export function FlexLayoutDockContainer({ panels, onPanelClose }: FlexLayoutDockContainerProps) { +export function FlexLayoutDockContainer({ panels, onPanelClose, activePanelId }: FlexLayoutDockContainerProps) { + const layoutRef = useRef(null); + const previousLayoutJsonRef = useRef(null); + const previousPanelIdsRef = useRef(''); + const previousPanelTitlesRef = useRef>(new Map()); + const createDefaultLayout = useCallback((): IJsonModel => { const hierarchyPanels = panels.filter((p) => p.id.includes('hierarchy')); const assetPanels = panels.filter((p) => p.id.includes('asset')); @@ -28,9 +169,20 @@ export function FlexLayoutDockContainer({ panels, onPanelClose }: FlexLayoutDock // Build center column children const centerColumnChildren: (IJsonTabSetNode | IJsonRowNode)[] = []; if (centerPanels.length > 0) { + // 找到要激活的tab的索引 + let activeTabIndex = 0; + if (activePanelId) { + const index = centerPanels.findIndex((p) => p.id === activePanelId); + if (index !== -1) { + activeTabIndex = index; + } + } + centerColumnChildren.push({ type: 'tabset', weight: 70, + selected: activeTabIndex, + enableMaximize: true, children: centerPanels.map((p) => ({ type: 'tab', name: p.title, @@ -44,6 +196,7 @@ export function FlexLayoutDockContainer({ panels, onPanelClose }: FlexLayoutDock centerColumnChildren.push({ type: 'tabset', weight: 30, + enableMaximize: true, children: bottomPanels.map((p) => ({ type: 'tab', name: p.title, @@ -65,6 +218,7 @@ export function FlexLayoutDockContainer({ panels, onPanelClose }: FlexLayoutDock leftColumnChildren.push({ type: 'tabset', weight: 50, + enableMaximize: true, children: hierarchyPanels.map((p) => ({ type: 'tab', name: p.title, @@ -79,6 +233,7 @@ export function FlexLayoutDockContainer({ panels, onPanelClose }: FlexLayoutDock leftColumnChildren.push({ type: 'tabset', weight: 50, + enableMaximize: true, children: assetPanels.map((p) => ({ type: 'tab', name: p.title, @@ -102,6 +257,7 @@ export function FlexLayoutDockContainer({ panels, onPanelClose }: FlexLayoutDock mainRowChildren.push({ type: 'tabset', weight: 60, + enableMaximize: true, children: centerChild.children } as IJsonTabSetNode); } else if (centerChild) { @@ -123,6 +279,7 @@ export function FlexLayoutDockContainer({ panels, onPanelClose }: FlexLayoutDock mainRowChildren.push({ type: 'tabset', weight: 20, + enableMaximize: true, children: rightPanels.map((p) => ({ type: 'tab', name: p.title, @@ -137,22 +294,168 @@ export function FlexLayoutDockContainer({ panels, onPanelClose }: FlexLayoutDock global: { tabEnableClose: true, tabEnableRename: false, - tabSetEnableMaximize: false, + tabSetEnableMaximize: true, tabSetEnableDrop: true, tabSetEnableDrag: true, tabSetEnableDivide: true, - borderEnableDrop: true + borderEnableDrop: true, + borderAutoSelectTabWhenOpen: true, + borderAutoSelectTabWhenClosed: true }, - borders: [], + borders: [ + { + type: 'border', + location: 'bottom', + size: 200, + children: [] + }, + { + type: 'border', + location: 'right', + size: 300, + children: [] + } + ], layout: { type: 'row', weight: 100, children: mainRowChildren } }; - }, [panels]); + }, [panels, activePanelId]); - const model = useMemo(() => Model.fromJson(createDefaultLayout()), [createDefaultLayout]); + const [model, setModel] = useState(() => { + try { + return Model.fromJson(createDefaultLayout()); + } catch (error) { + throw new Error(`Failed to create layout model: ${error instanceof Error ? error.message : String(error)}`); + } + }); + + useEffect(() => { + try { + // 检查面板ID列表是否真的变化了(而不只是标题等属性变化) + const currentPanelIds = panels.map(p => p.id).sort().join(','); + const previousIds = previousPanelIdsRef.current; + + // 检查标题是否变化 + const currentTitles = new Map(panels.map(p => [p.id, p.title])); + const titleChanges: Array<{ id: string; newTitle: string }> = []; + + for (const panel of panels) { + const previousTitle = previousPanelTitlesRef.current.get(panel.id); + if (previousTitle && previousTitle !== panel.title) { + titleChanges.push({ id: panel.id, newTitle: panel.title }); + } + } + + // 更新标题引用 + previousPanelTitlesRef.current = currentTitles; + + // 如果只是标题变化,更新tab名称 + if (titleChanges.length > 0 && currentPanelIds === previousIds && model) { + titleChanges.forEach(({ id, newTitle }) => { + const node = model.getNodeById(id); + if (node && node.getType() === 'tab') { + model.doAction(Actions.renameTab(id, newTitle)); + } + }); + return; + } + + if (currentPanelIds === previousIds) { + return; + } + + // 计算新增和移除的面板 + const prevSet = new Set(previousIds.split(',').filter(id => id)); + const currSet = new Set(currentPanelIds.split(',').filter(id => id)); + const newPanelIds = Array.from(currSet).filter(id => !prevSet.has(id)); + const removedPanelIds = Array.from(prevSet).filter(id => !currSet.has(id)); + + previousPanelIdsRef.current = currentPanelIds; + + // 如果已经有布局且只是添加新面板,使用Action动态添加 + if (model && newPanelIds.length > 0 && removedPanelIds.length === 0 && previousIds) { + // 找到要添加的面板 + const newPanels = panels.filter(p => newPanelIds.includes(p.id)); + + // 找到中心区域的tabset ID + let centerTabsetId: string | null = null; + + model.visitNodes((node: any) => { + if (node.getType() === 'tabset') { + const tabset = node as any; + // 检查是否是中心tabset + const children = tabset.getChildren(); + const hasNonSidePanel = children.some((child: any) => { + const id = child.getId(); + return !id.includes('hierarchy') && + !id.includes('asset') && + !id.includes('inspector') && + !id.includes('console'); + }); + if (hasNonSidePanel && !centerTabsetId) { + centerTabsetId = tabset.getId(); + } + } + }); + + if (centerTabsetId) { + // 动态添加tab到中心tabset + newPanels.forEach(panel => { + model.doAction(Actions.addNode( + { + type: 'tab', + name: panel.title, + id: panel.id, + component: panel.id, + enableClose: panel.closable !== false + }, + centerTabsetId!, + DockLocation.CENTER, + -1 // 添加到末尾 + )); + }); + + // 选中最后添加的面板 + const lastPanel = newPanels[newPanels.length - 1]; + if (lastPanel) { + setTimeout(() => { + const node = model.getNodeById(lastPanel.id); + if (node) { + model.doAction(Actions.selectTab(lastPanel.id)); + } + }, 0); + } + + return; + } + } + + // 否则完全重建布局 + const defaultLayout = createDefaultLayout(); + + // 如果有保存的布局,尝试合并 + if (previousLayoutJsonRef.current && previousIds) { + try { + const savedLayout = JSON.parse(previousLayoutJsonRef.current); + const mergedLayout = mergeLayouts(savedLayout, defaultLayout, panels); + const newModel = Model.fromJson(mergedLayout); + setModel(newModel); + return; + } catch (error) { + // 合并失败,使用默认布局 + } + } + + // 使用默认布局 + const newModel = Model.fromJson(defaultLayout); + setModel(newModel); + } catch (error) { + throw new Error(`Failed to update layout model: ${error instanceof Error ? error.message : String(error)}`); + } + }, [createDefaultLayout, panels]); const factory = useCallback((node: TabNode) => { const component = node.getComponent(); @@ -160,9 +463,9 @@ export function FlexLayoutDockContainer({ panels, onPanelClose }: FlexLayoutDock return panel?.content ||
Panel not found
; }, [panels]); - const onAction = useCallback((action: any) => { + const onAction = useCallback((action: Action) => { if (action.type === Actions.DELETE_TAB) { - const tabId = action.data.node; + const tabId = (action.data as { node: string }).node; if (onPanelClose) { onPanelClose(tabId); } @@ -170,12 +473,20 @@ export function FlexLayoutDockContainer({ panels, onPanelClose }: FlexLayoutDock return action; }, [onPanelClose]); + const onModelChange = useCallback((newModel: Model) => { + // 保存布局状态以便在panels变化时恢复 + const layoutJson = newModel.toJson(); + previousLayoutJsonRef.current = JSON.stringify(layoutJson); + }, []); + return (
); diff --git a/packages/editor-app/src/plugins/BehaviorTreePlugin.ts b/packages/editor-app/src/plugins/BehaviorTreePlugin.ts index f24b4e54..21d09563 100644 --- a/packages/editor-app/src/plugins/BehaviorTreePlugin.ts +++ b/packages/editor-app/src/plugins/BehaviorTreePlugin.ts @@ -1,12 +1,22 @@ import type { Core, ServiceContainer } from '@esengine/ecs-framework'; import { IEditorPlugin, EditorPluginCategory, PanelPosition, MessageHub } from '@esengine/editor-core'; -import type { MenuItem, ToolbarItem, PanelDescriptor, ISerializer, FileActionHandler, FileCreationTemplate, FileContextMenuItem } from '@esengine/editor-core'; +import type { + MenuItem, + ToolbarItem, + PanelDescriptor, + ISerializer, + FileActionHandler, + FileCreationTemplate, + FileContextMenuItem +} from '@esengine/editor-core'; import { BehaviorTreeData } from '@esengine/behavior-tree'; import { BehaviorTreeEditorPanel } from '../presentation/components/behavior-tree/panels'; import { FileText } from 'lucide-react'; import { TauriAPI } from '../api/tauri'; import { createElement } from 'react'; import { useBehaviorTreeStore } from '../stores/behaviorTreeStore'; +import { createRootNode } from '../domain/constants/RootNode'; +import { behaviorTreeFileService } from '../services/BehaviorTreeFileService'; /** * 行为树编辑器插件 @@ -113,30 +123,43 @@ export class BehaviorTreePlugin implements IEditorPlugin { console.log('[BehaviorTreePlugin] onDoubleClick called for:', filePath); if (this.messageHub) { - useBehaviorTreeStore.getState().setIsOpen(true); + const store = useBehaviorTreeStore.getState(); + store.setIsOpen(true); + store.setPendingFilePath(filePath); // 状态通道(同步,时序安全) + + // 提取文件名 + const fileName = filePath.split(/[\\/]/).pop()?.replace('.btree', '') || '行为树'; await this.messageHub.publish('dynamic-panel:open', { - panelId: 'behavior-tree-editor' + panelId: 'behavior-tree-editor', + title: fileName }); - await this.messageHub.publish('behavior-tree:open-file', { - filePath: filePath + // 消息通道(异步,用于其他监听者) + await this.messageHub.publish('behavior-tree:load-file', { + filePath }); - console.log('[BehaviorTreePlugin] Panel opened and file loaded'); } else { console.error('[BehaviorTreePlugin] MessageHub is not available!'); } }, onOpen: async (filePath: string) => { if (this.messageHub) { - useBehaviorTreeStore.getState().setIsOpen(true); + const store = useBehaviorTreeStore.getState(); + store.setIsOpen(true); + store.setPendingFilePath(filePath); // 状态通道(同步,时序安全) + + // 提取文件名 + const fileName = filePath.split(/[\\/]/).pop()?.replace('.btree', '') || '行为树'; await this.messageHub.publish('dynamic-panel:open', { - panelId: 'behavior-tree-editor' + panelId: 'behavior-tree-editor', + title: fileName }); - await this.messageHub.publish('behavior-tree:open-file', { - filePath: filePath + // 消息通道(异步,用于其他监听者) + await this.messageHub.publish('behavior-tree:load-file', { + filePath }); } }, @@ -147,14 +170,21 @@ export class BehaviorTreePlugin implements IEditorPlugin { icon: createElement(FileText, { size: 16 }), onClick: async (filePath: string) => { if (this.messageHub) { - useBehaviorTreeStore.getState().setIsOpen(true); + const store = useBehaviorTreeStore.getState(); + store.setIsOpen(true); + store.setPendingFilePath(filePath); // 状态通道(同步,时序安全) + + // 提取文件名 + const fileName = filePath.split(/[\\/]/).pop()?.replace('.btree', '') || '行为树'; await this.messageHub.publish('dynamic-panel:open', { - panelId: 'behavior-tree-editor' + panelId: 'behavior-tree-editor', + title: fileName }); - await this.messageHub.publish('behavior-tree:open-file', { - filePath: filePath + // 消息通道(异步,用于其他监听者) + await this.messageHub.publish('behavior-tree:load-file', { + filePath }); } } @@ -173,14 +203,25 @@ export class BehaviorTreePlugin implements IEditorPlugin { defaultFileName: 'NewBehaviorTree', icon: createElement(FileText, { size: 16 }), createContent: async (fileName: string) => { - const emptyTree: BehaviorTreeData = { - id: `tree_${Date.now()}`, - name: fileName, - rootNodeId: '', - nodes: new Map(), - blackboardVariables: new Map() + const rootNode = createRootNode(); + const now = new Date().toISOString(); + const editorFormat = { + version: '1.0.0', + metadata: { + name: fileName, + description: '', + createdAt: now, + modifiedAt: now + }, + nodes: [rootNode.toObject()], + connections: [], + blackboard: {}, + canvasState: { + offset: { x: 0, y: 0 }, + scale: 1 + } }; - return this.serializeBehaviorTreeData(emptyTree); + return JSON.stringify(editorFormat, null, 2); } } ]; @@ -196,9 +237,9 @@ export class BehaviorTreePlugin implements IEditorPlugin { })), blackboardVariables: treeData.blackboardVariables ? Array.from(treeData.blackboardVariables.entries()).map(([key, value]) => ({ - key, - value - })) + key, + value + })) : [] }; return JSON.stringify(serializable, null, 2); diff --git a/packages/editor-app/src/plugins/ProfilerPlugin.tsx b/packages/editor-app/src/plugins/ProfilerPlugin.tsx index 003cf76b..f50d29a6 100644 --- a/packages/editor-app/src/plugins/ProfilerPlugin.tsx +++ b/packages/editor-app/src/plugins/ProfilerPlugin.tsx @@ -134,7 +134,7 @@ export class ProfilerPlugin implements IEditorPlugin { id: 'profiler-monitor', title: 'Performance Monitor', position: 'center' as any, - closable: true, + closable: false, component: ProfilerDockPanel, order: 200 } diff --git a/packages/editor-app/src/presentation/components/behavior-tree/panels/BehaviorTreeEditorPanel.css b/packages/editor-app/src/presentation/components/behavior-tree/panels/BehaviorTreeEditorPanel.css index b7db8136..250cc6bb 100644 --- a/packages/editor-app/src/presentation/components/behavior-tree/panels/BehaviorTreeEditorPanel.css +++ b/packages/editor-app/src/presentation/components/behavior-tree/panels/BehaviorTreeEditorPanel.css @@ -3,7 +3,26 @@ flex-direction: column; height: 100%; width: 100%; - background: var(--bg-secondary); + background: #1e1e1e; +} + +/* 全屏模式 */ +.behavior-tree-editor-panel.fullscreen { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 9999; + background: #1e1e1e; +} + +/* 全屏模式下的工具栏 - 确保不透明 */ +.behavior-tree-editor-panel.fullscreen .behavior-tree-editor-toolbar { + background: #252526; + opacity: 1; + position: relative; + z-index: 10000; } /* 工具栏 */ @@ -11,8 +30,8 @@ display: flex; align-items: center; padding: 8px 12px; - background: var(--bg-primary); - border-bottom: 1px solid var(--border-color); + background: #252526; + border-bottom: 1px solid #3e3e42; gap: 8px; flex-wrap: wrap; } @@ -26,7 +45,7 @@ .behavior-tree-editor-toolbar .toolbar-divider { width: 1px; height: 24px; - background: var(--border-color); + background: #3e3e42; margin: 0 4px; } @@ -39,17 +58,17 @@ border: 1px solid transparent; border-radius: 4px; cursor: pointer; - color: var(--text-primary); + color: #cccccc; transition: all 0.2s; } .behavior-tree-editor-toolbar .toolbar-btn:hover:not(:disabled) { - background: var(--bg-hover); - border-color: var(--border-color); + background: #2a2d2e; + border-color: #3e3e42; } .behavior-tree-editor-toolbar .toolbar-btn:active:not(:disabled) { - background: var(--bg-active); + background: #37373d; } .behavior-tree-editor-toolbar .toolbar-btn:disabled { @@ -165,7 +184,7 @@ .behavior-tree-editor-toolbar .file-name { font-size: 13px; - color: var(--text-secondary); + color: #9d9d9d; font-weight: 500; } @@ -192,8 +211,8 @@ width: 350px; display: flex; flex-direction: column; - background: var(--bg-primary); - border: 1px solid var(--border-color); + background: #252526; + border: 1px solid #3e3e42; border-radius: 8px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3), 0 0 0 1px rgba(255, 255, 255, 0.05); z-index: 100; @@ -205,11 +224,11 @@ justify-content: space-between; padding: 12px; background: #2d2d2d; - border-bottom: 1px solid var(--border-color); + border-bottom: 1px solid #3e3e42; border-radius: 8px 8px 0 0; font-weight: 500; font-size: 13px; - color: var(--text-primary); + color: #cccccc; } .blackboard-header .close-btn { @@ -221,14 +240,14 @@ border: 1px solid transparent; border-radius: 4px; cursor: pointer; - color: var(--text-secondary); + color: #9d9d9d; transition: all 0.2s; } .blackboard-header .close-btn:hover { - background: var(--bg-hover); - border-color: var(--border-color); - color: var(--text-primary); + background: #2a2d2e; + border-color: #3e3e42; + color: #cccccc; } .blackboard-content { @@ -247,18 +266,18 @@ justify-content: center; width: 32px; height: 48px; - background: var(--bg-primary); - border: 1px solid var(--border-color); + background: #252526; + border: 1px solid #3e3e42; border-radius: 8px 0 0 8px; cursor: pointer; - color: var(--text-secondary); + color: #9d9d9d; transition: all 0.2s; z-index: 99; box-shadow: -2px 0 8px rgba(0, 0, 0, 0.2); } .blackboard-toggle-btn:hover { - background: var(--bg-hover); - color: var(--text-primary); + background: #2a2d2e; + color: #cccccc; width: 36px; } diff --git a/packages/editor-app/src/presentation/components/behavior-tree/panels/BehaviorTreeEditorPanel.tsx b/packages/editor-app/src/presentation/components/behavior-tree/panels/BehaviorTreeEditorPanel.tsx index 013d42f4..f6c665c3 100644 --- a/packages/editor-app/src/presentation/components/behavior-tree/panels/BehaviorTreeEditorPanel.tsx +++ b/packages/editor-app/src/presentation/components/behavior-tree/panels/BehaviorTreeEditorPanel.tsx @@ -1,6 +1,6 @@ -import { useState, useEffect, useRef } from 'react'; +import { useState, useEffect, useRef, useCallback, useLayoutEffect } from 'react'; import { useTranslation } from 'react-i18next'; -import { Save, FolderOpen, Download, Play, Pause, Square, SkipForward, Clipboard, ChevronRight, ChevronLeft, Copy } from 'lucide-react'; +import { Save, FolderOpen, Download, Play, Pause, Square, SkipForward, Clipboard, ChevronRight, ChevronLeft, Copy, Home, Maximize2, Minimize2 } from 'lucide-react'; import { invoke } from '@tauri-apps/api/core'; import { open, message } from '@tauri-apps/plugin-dialog'; import { Core } from '@esengine/ecs-framework'; @@ -16,6 +16,7 @@ import { createLogger } from '@esengine/ecs-framework'; import { LocalBlackboardTypeGenerator } from '../../../../generators/LocalBlackboardTypeGenerator'; import { GlobalBlackboardTypeGenerator } from '../../../../generators/GlobalBlackboardTypeGenerator'; import { useExecutionController } from '../../../hooks/useExecutionController'; +import { behaviorTreeFileService } from '../../../../services/BehaviorTreeFileService'; import './BehaviorTreeEditorPanel.css'; const logger = createLogger('BehaviorTreeEditorPanel'); @@ -31,11 +32,12 @@ export const BehaviorTreeEditorPanel: React.FC = ( const { isOpen, + pendingFilePath, + setPendingFilePath, nodes, connections, exportToJSON, exportToRuntimeAsset, - importFromJSON, blackboardVariables, setBlackboardVariables, updateBlackboardVariable, @@ -45,8 +47,7 @@ export const BehaviorTreeEditorPanel: React.FC = ( setIsExecuting, saveNodesDataSnapshot, restoreNodesData, - setIsOpen, - reset + resetView } = useBehaviorTreeStore(); const [currentFilePath, setCurrentFilePath] = useState(null); @@ -58,8 +59,10 @@ export const BehaviorTreeEditorPanel: React.FC = ( const [isBlackboardOpen, setIsBlackboardOpen] = useState(true); const [globalVariables, setGlobalVariables] = useState>({}); const [hasUnsavedGlobalChanges, setHasUnsavedGlobalChanges] = useState(false); + const [isFullscreen, setIsFullscreen] = useState(false); const isInitialMount = useRef(true); const initialStateSnapshot = useRef<{ nodes: number; variables: number }>({ nodes: 0, variables: 0 }); + const processingFileRef = useRef(null); const { executionMode, @@ -148,31 +151,37 @@ export const BehaviorTreeEditorPanel: React.FC = ( } }, [nodes, blackboardVariables]); - useEffect(() => { - const handleOpenFile = async (data: any) => { - if (data.filePath && data.filePath.endsWith('.btree')) { - try { - const json = await invoke('read_behavior_tree_file', { filePath: data.filePath }); - importFromJSON(json); - setCurrentFilePath(data.filePath); - setHasUnsavedChanges(false); - isInitialMount.current = true; - initialStateSnapshot.current = { - nodes: nodes.length, - variables: Object.keys(blackboardVariables).length - }; - logger.info('行为树已加载', data.filePath); - showToast(`已打开 ${data.filePath.split(/[\\/]/).pop()?.replace('.btree', '')}`, 'success'); - } catch (error) { - logger.error('加载行为树失败', error); - showToast(`加载失败: ${error}`, 'error'); - } - } - }; + const loadFile = useCallback(async (filePath: string) => { + const result = await behaviorTreeFileService.loadFile(filePath); - const unsubscribe = messageHub?.subscribe('behavior-tree:open-file', handleOpenFile); - return () => unsubscribe?.(); - }, [messageHub, importFromJSON, nodes.length, blackboardVariables]); + if (result.success && result.fileName) { + setCurrentFilePath(filePath); + setHasUnsavedChanges(false); + isInitialMount.current = true; + initialStateSnapshot.current = { nodes: 0, variables: 0 }; + showToast(`已打开 ${result.fileName}`, 'success'); + } else if (result.error) { + showToast(`加载失败: ${result.error}`, 'error'); + } + }, [showToast]); + + // 使用 useLayoutEffect 处理 pendingFilePath(同步执行,DOM 更新前) + // 这是文件加载的唯一入口,避免重复 + useLayoutEffect(() => { + if (!pendingFilePath) return; + + // 防止 React StrictMode 导致的重复执行 + if (processingFileRef.current === pendingFilePath) { + return; + } + + processingFileRef.current = pendingFilePath; + + loadFile(pendingFilePath).then(() => { + setPendingFilePath(null); + processingFileRef.current = null; + }); + }, [pendingFilePath, loadFile, setPendingFilePath]); const loadGlobalBlackboard = async (path: string) => { try { @@ -223,15 +232,20 @@ export const BehaviorTreeEditorPanel: React.FC = ( }); if (selected) { - const json = await invoke('read_behavior_tree_file', { filePath: selected as string }); - importFromJSON(json); - setCurrentFilePath(selected as string); - setHasUnsavedChanges(false); - isInitialMount.current = true; - logger.info('行为树已加载', selected); + const result = await behaviorTreeFileService.loadFile(selected as string); + if (result.success && result.fileName) { + setCurrentFilePath(selected as string); + setHasUnsavedChanges(false); + isInitialMount.current = true; + initialStateSnapshot.current = { nodes: 0, variables: 0 }; + showToast(`已打开 ${result.fileName}`, 'success'); + } else if (result.error) { + showToast(`加载失败: ${result.error}`, 'error'); + } } } catch (error) { logger.error('加载失败', error); + showToast(`加载失败: ${error}`, 'error'); } }; @@ -373,12 +387,12 @@ export const BehaviorTreeEditorPanel: React.FC = ( const handleCopyBehaviorTree = () => { const buildNodeTree = (nodeId: string, depth: number = 0): string => { - const node = nodes.find(n => n.id === nodeId); + const node = nodes.find((n) => n.id === nodeId); if (!node) return ''; const indent = ' '.repeat(depth); const childrenText = node.children.length > 0 - ? `\n${node.children.map(childId => buildNodeTree(childId, depth + 1)).join('\n')}` + ? `\n${node.children.map((childId) => buildNodeTree(childId, depth + 1)).join('\n')}` : ''; const propertiesText = Object.keys(node.data).length > 0 @@ -388,7 +402,7 @@ export const BehaviorTreeEditorPanel: React.FC = ( return `${indent}- ${node.template.displayName} (${node.template.type})${propertiesText}${childrenText}`; }; - const rootNode = nodes.find(n => n.id === ROOT_NODE_ID); + const rootNode = nodes.find((n) => n.id === ROOT_NODE_ID); if (!rootNode) { showToast('未找到根节点', 'error'); return; @@ -408,26 +422,26 @@ ${buildNodeTree(ROOT_NODE_ID)} ${Object.entries(blackboardVariables).map(([key, value]) => ` - ${key}: ${JSON.stringify(value)}`).join('\n') || ' 无'} 全部节点详情: -${nodes.filter(n => n.id !== ROOT_NODE_ID).map(node => { - const incoming = connections.filter(c => c.to === node.id); - const outgoing = connections.filter(c => c.from === node.id); - return ` +${nodes.filter((n) => n.id !== ROOT_NODE_ID).map((node) => { + const incoming = connections.filter((c) => c.to === node.id); + const outgoing = connections.filter((c) => c.from === node.id); + return ` [${node.template.displayName}] 类型: ${node.template.type} 分类: ${node.template.category} 类名: ${node.template.className || '无'} ID: ${node.id} 子节点: ${node.children.length}个 - 输入连接: ${incoming.length}个${incoming.length > 0 ? '\n ' + incoming.map(c => { - const fromNode = nodes.find(n => n.id === c.from); - return `← ${fromNode?.template.displayName || '未知'}`; - }).join('\n ') : ''} - 输出连接: ${outgoing.length}个${outgoing.length > 0 ? '\n ' + outgoing.map(c => { - const toNode = nodes.find(n => n.id === c.to); - return `→ ${toNode?.template.displayName || '未知'}`; - }).join('\n ') : ''} + 输入连接: ${incoming.length}个${incoming.length > 0 ? '\n ' + incoming.map((c) => { + const fromNode = nodes.find((n) => n.id === c.from); + return `← ${fromNode?.template.displayName || '未知'}`; +}).join('\n ') : ''} + 输出连接: ${outgoing.length}个${outgoing.length > 0 ? '\n ' + outgoing.map((c) => { + const toNode = nodes.find((n) => n.id === c.to); + return `→ ${toNode?.template.displayName || '未知'}`; +}).join('\n ') : ''} 属性: ${JSON.stringify(node.data, null, 4)}`; -}).join('\n')} + }).join('\n')} `.trim(); navigator.clipboard.writeText(treeStructure).then(() => { @@ -590,12 +604,20 @@ ${nodes.filter(n => n.id !== ROOT_NODE_ID).map(node => { setHasUnsavedGlobalChanges(true); }; + const toggleFullscreen = () => { + const newFullscreenState = !isFullscreen; + setIsFullscreen(newFullscreenState); + + // 通知主界面切换全屏状态 + messageHub?.publish('editor:fullscreen', { fullscreen: newFullscreenState }); + }; + if (!isOpen) { return null; } return ( -
+
{/* 文件操作 */}
@@ -654,6 +676,12 @@ ${nodes.filter(n => n.id !== ROOT_NODE_ID).map(node => { {/* 视图控制 */}
+ + diff --git a/packages/editor-app/src/services/BehaviorTreeFileService.ts b/packages/editor-app/src/services/BehaviorTreeFileService.ts new file mode 100644 index 00000000..1af2c476 --- /dev/null +++ b/packages/editor-app/src/services/BehaviorTreeFileService.ts @@ -0,0 +1,60 @@ +import { invoke } from '@tauri-apps/api/core'; +import { createLogger } from '@esengine/ecs-framework'; +import { useBehaviorTreeStore } from '../stores/behaviorTreeStore'; + +const logger = createLogger('BehaviorTreeFileService'); + +export interface FileLoadResult { + success: boolean; + fileName?: string; + error?: string; +} + +export class BehaviorTreeFileService { + private loadingFiles = new Set(); + + async loadFile(filePath: string): Promise { + try { + if (!filePath.endsWith('.btree')) { + return { + success: false, + error: '无效的文件类型' + }; + } + + // 防止重复加载同一个文件(保底机制,通常不应该被触发) + if (this.loadingFiles.has(filePath)) { + logger.debug('文件正在加载中,跳过重复请求:', filePath); + return { success: false, error: '文件正在加载中' }; + } + + this.loadingFiles.add(filePath); + + try { + logger.info('加载行为树文件:', filePath); + const json = await invoke('read_behavior_tree_file', { filePath }); + + const store = useBehaviorTreeStore.getState(); + store.importFromJSON(json); + + const fileName = filePath.split(/[\\/]/).pop()?.replace('.btree', '') || '未命名'; + logger.info('行为树已加载:', fileName); + + return { + success: true, + fileName + }; + } finally { + this.loadingFiles.delete(filePath); + } + } catch (error) { + logger.error('加载行为树失败', error); + return { + success: false, + error: error instanceof Error ? error.message : String(error) + }; + } + } +} + +export const behaviorTreeFileService = new BehaviorTreeFileService(); diff --git a/packages/editor-app/src/stores/behaviorTreeStore.ts b/packages/editor-app/src/stores/behaviorTreeStore.ts index 2933c424..7c87383e 100644 --- a/packages/editor-app/src/stores/behaviorTreeStore.ts +++ b/packages/editor-app/src/stores/behaviorTreeStore.ts @@ -5,6 +5,8 @@ import { Connection } from '../domain/models/Connection'; import { Blackboard, BlackboardValue } from '../domain/models/Blackboard'; import { Position } from '../domain/value-objects/Position'; import { createRootNode, ROOT_NODE_ID } from '../domain/constants/RootNode'; +import { BehaviorTree } from '../domain/models/BehaviorTree'; +import { useBehaviorTreeDataStore } from '../application/state/BehaviorTreeDataStore'; /** * 行为树 Store 状态接口 @@ -18,6 +20,7 @@ export interface NodeExecutionInfo { interface BehaviorTreeState { isOpen: boolean; + pendingFilePath: string | null; nodes: Node[]; connections: Connection[]; blackboard: Blackboard; @@ -112,6 +115,7 @@ interface BehaviorTreeState { ) => string | Uint8Array; setIsOpen: (isOpen: boolean) => void; + setPendingFilePath: (filePath: string | null) => void; reset: () => void; } @@ -121,6 +125,7 @@ interface BehaviorTreeState { */ export const useBehaviorTreeStore = create((set, get) => ({ isOpen: false, + pendingFilePath: null, nodes: [], connections: [], blackboard: new Blackboard(), @@ -412,6 +417,10 @@ export const useBehaviorTreeStore = create((set, get) => ({ const loadedBlackboard = Blackboard.fromObject(blackboardData); + // 同步更新领域层数据存储,确保 Command 系统使用正确的数据 + const newTree = new BehaviorTree(loadedNodes, loadedConnections, loadedBlackboard, ROOT_NODE_ID); + useBehaviorTreeDataStore.getState().setTree(newTree); + set({ isOpen: true, nodes: loadedNodes, @@ -420,7 +429,13 @@ export const useBehaviorTreeStore = create((set, get) => ({ blackboardVariables: blackboardData, initialBlackboardVariables: blackboardData, canvasOffset: data.canvasState?.offset || { x: 0, y: 0 }, - canvasScale: data.canvasState?.scale || 1 + canvasScale: data.canvasState?.scale || 1, + // 只清理连线相关状态,避免切换文件时残留上一个文件的连线状态 + connectingFrom: null, + connectingFromProperty: null, + connectingToPos: null, + // 清理选中状态 + selectedNodeIds: [] }); }, @@ -454,6 +469,8 @@ export const useBehaviorTreeStore = create((set, get) => ({ setIsOpen: (isOpen: boolean) => set({ isOpen }), + setPendingFilePath: (filePath: string | null) => set({ pendingFilePath: filePath }), + reset: () => set({ isOpen: false, nodes: [], diff --git a/packages/editor-app/src/styles/AssetBrowser.css b/packages/editor-app/src/styles/AssetBrowser.css index a2d12a7e..2ebf0139 100644 --- a/packages/editor-app/src/styles/AssetBrowser.css +++ b/packages/editor-app/src/styles/AssetBrowser.css @@ -2,8 +2,10 @@ display: flex; flex-direction: column; height: 100%; - background: #1e1e1e; - border-top: 1px solid #333; + background: var(--color-bg-base); + border-top: 1px solid var(--color-border-default); + container-type: inline-size; + container-name: asset-browser; } .asset-browser-header { @@ -21,31 +23,57 @@ .view-mode-buttons { display: flex; - gap: 4px; + gap: 0; + margin-left: auto; } .view-mode-btn { - padding: 4px 8px; + padding: 6px 12px; background: transparent; border: 1px solid #3e3e3e; - border-radius: 3px; color: #cccccc; cursor: pointer; transition: all 0.2s ease; display: flex; align-items: center; justify-content: center; + gap: 6px; + font-size: 12px; + font-weight: normal; + border-radius: 0; } -.view-mode-btn:hover { +.view-mode-btn:first-child { + border-top-left-radius: 3px; + border-bottom-left-radius: 3px; + border-right: none; +} + +.view-mode-btn:last-child { + border-top-right-radius: 3px; + border-bottom-right-radius: 3px; +} + +.view-mode-btn:hover:not(.active) { background: #2a2d2e; - border-color: #007acc; } .view-mode-btn.active { background: #0e639c; border-color: #0e639c; color: #ffffff; + font-weight: 500; +} + +/* 响应式:小宽度时只显示图标 - 使用容器查询 */ +@container asset-browser (max-width: 400px) { + .view-mode-text { + display: none; + } + + .view-mode-btn { + padding: 6px 8px; + } } .asset-browser-content { @@ -154,14 +182,14 @@ } /* 容器查询:根据容器宽度调整布局 */ -@container (max-width: 400px) { +@container asset-list-container (max-width: 400px) { .asset-list { grid-template-columns: repeat(auto-fill, minmax(70px, 1fr)); gap: 6px; } } -@container (max-width: 250px) { +@container asset-list-container (max-width: 250px) { .asset-list { grid-template-columns: repeat(auto-fill, minmax(60px, 1fr)); gap: 4px; @@ -169,7 +197,7 @@ } } -@container (max-width: 150px) { +@container asset-list-container (max-width: 150px) { .asset-list { grid-template-columns: 1fr; gap: 2px; @@ -199,7 +227,7 @@ } /* 中等窄度优化 */ -@container (max-width: 250px) { +@container asset-list-container (max-width: 250px) { .asset-item { padding: 8px 6px; } @@ -253,6 +281,21 @@ color: #dcb67a; } +/* 系统文件夹特殊标记 */ +.asset-icon.system-folder { + filter: drop-shadow(0 0 3px rgba(66, 165, 245, 0.5)); + animation: subtle-pulse 3s ease-in-out infinite; +} + +@keyframes subtle-pulse { + 0%, 100% { + filter: drop-shadow(0 0 3px rgba(66, 165, 245, 0.5)); + } + 50% { + filter: drop-shadow(0 0 6px rgba(66, 165, 245, 0.7)); + } +} + .asset-info { flex: 1; width: 100%; diff --git a/packages/editor-app/src/styles/EntityInspector.css b/packages/editor-app/src/styles/EntityInspector.css index bfe3f637..412d117b 100644 --- a/packages/editor-app/src/styles/EntityInspector.css +++ b/packages/editor-app/src/styles/EntityInspector.css @@ -34,14 +34,14 @@ .inspector-content { flex: 1; - overflow-y: auto; - overflow-x: hidden; + overflow: auto; padding: var(--spacing-md); min-height: 0; } .inspector-content::-webkit-scrollbar { width: 14px; + height: 14px; } .inspector-content::-webkit-scrollbar-track { @@ -60,6 +60,10 @@ background-clip: padding-box; } +.inspector-content::-webkit-scrollbar-corner { + background: transparent; +} + .inspector-section { margin-bottom: 20px; padding: 12px; diff --git a/packages/editor-app/src/styles/FileTree.css b/packages/editor-app/src/styles/FileTree.css index 206f60cd..c718dd95 100644 --- a/packages/editor-app/src/styles/FileTree.css +++ b/packages/editor-app/src/styles/FileTree.css @@ -97,6 +97,21 @@ font-size: var(--font-size-md); } +/* 系统文件夹特殊标记 */ +.tree-icon .system-folder-icon { + filter: drop-shadow(0 0 2px rgba(66, 165, 245, 0.5)); + animation: subtle-pulse-tree 3s ease-in-out infinite; +} + +@keyframes subtle-pulse-tree { + 0%, 100% { + filter: drop-shadow(0 0 2px rgba(66, 165, 245, 0.5)); + } + 50% { + filter: drop-shadow(0 0 4px rgba(66, 165, 245, 0.7)); + } +} + .tree-label { flex: 1; overflow: hidden; diff --git a/packages/editor-app/src/styles/FlexLayoutDock.css b/packages/editor-app/src/styles/FlexLayoutDock.css index 3a17c14f..bf57177b 100644 --- a/packages/editor-app/src/styles/FlexLayoutDock.css +++ b/packages/editor-app/src/styles/FlexLayoutDock.css @@ -111,14 +111,24 @@ .flexlayout__tab_button_trailing { margin-left: 8px; - opacity: 0; + opacity: 0.6; transition: opacity 0.15s ease; + display: flex; + align-items: center; + justify-content: center; + padding: 2px; + border-radius: 3px; } +.flexlayout__tab_button:hover .flexlayout__tab_button_trailing, .flexlayout__tab:hover .flexlayout__tab_button_trailing { opacity: 1; } +.flexlayout__tab_button_trailing:hover { + background: rgba(255, 255, 255, 0.1); +} + .flexlayout__tab_button_trailing svg { width: 12px; height: 12px; @@ -133,6 +143,32 @@ background: #2d2d30; } +/* 标签栏滚动条样式 */ +.flexlayout__tab_moveable::-webkit-scrollbar { + width: 14px; + height: 14px; +} + +.flexlayout__tab_moveable::-webkit-scrollbar-track { + background: transparent; +} + +.flexlayout__tab_moveable::-webkit-scrollbar-thumb { + background: rgba(121, 121, 121, 0.4); + border-radius: 8px; + border: 3px solid transparent; + background-clip: padding-box; +} + +.flexlayout__tab_moveable::-webkit-scrollbar-thumb:hover { + background: rgba(100, 100, 100, 0.7); + background-clip: padding-box; +} + +.flexlayout__tab_moveable::-webkit-scrollbar-corner { + background: transparent; +} + .flexlayout__tabset-selected { background: #1e1e1e; } @@ -187,10 +223,12 @@ } .flexlayout__tab_toolbar { - display: flex; + display: flex !important; align-items: center; gap: 4px; padding: 0 8px; + visibility: visible !important; + opacity: 1 !important; } .flexlayout__tab_toolbar_button { @@ -198,9 +236,14 @@ border: none; color: #cccccc; cursor: pointer; - padding: 4px 8px; + padding: 4px; border-radius: 3px; transition: background-color 0.15s ease; + display: flex !important; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; } .flexlayout__tab_toolbar_button:hover { @@ -213,6 +256,13 @@ height: 14px; } +/* 确保最小化和最大化按钮可见 */ +.flexlayout__tab_toolbar_button-min, +.flexlayout__tab_toolbar_button-max { + display: flex !important; + visibility: visible !important; +} + .flexlayout__popup_menu { background: #252526; border: 1px solid #454545; diff --git a/packages/editor-app/src/styles/SceneHierarchy.css b/packages/editor-app/src/styles/SceneHierarchy.css index 709d0b85..700a51f2 100644 --- a/packages/editor-app/src/styles/SceneHierarchy.css +++ b/packages/editor-app/src/styles/SceneHierarchy.css @@ -150,10 +150,35 @@ .hierarchy-content { flex: 1; - overflow-y: auto; + overflow: auto; padding: var(--spacing-xs) 0; } +.hierarchy-content::-webkit-scrollbar { + width: 14px; + height: 14px; +} + +.hierarchy-content::-webkit-scrollbar-track { + background: transparent; +} + +.hierarchy-content::-webkit-scrollbar-thumb { + background: rgba(121, 121, 121, 0.4); + border-radius: 8px; + border: 3px solid transparent; + background-clip: padding-box; +} + +.hierarchy-content::-webkit-scrollbar-thumb:hover { + background: rgba(100, 100, 100, 0.7); + background-clip: padding-box; +} + +.hierarchy-content::-webkit-scrollbar-corner { + background: transparent; +} + .empty-state { display: flex; flex-direction: column;