feat(i18n): 统一国际化系统架构,支持插件独立翻译 (#301)
* feat(i18n): 统一国际化系统架构,支持插件独立翻译 ## 主要改动 ### 核心架构 - 增强 LocaleService,支持插件命名空间翻译扩展 - 新增 editor-runtime/i18n 模块,提供 createPluginLocale/createPluginTranslator - 新增 editor-core/tokens.ts,定义 LocaleServiceToken 等服务令牌 - 改进 PluginAPI 类型安全,使用 ServiceToken<T> 替代 any ### 编辑器本地化 - 扩展 en.ts/zh.ts 翻译文件,覆盖所有 UI 组件 - 新增 es.ts 西班牙语支持 - 重构 40+ 组件使用 useLocale() hook ### 插件本地化系统 - behavior-tree-editor: 新增 locales/ 和 useBTLocale hook - material-editor: 新增 locales/ 和 useMaterialLocale hook - particle-editor: 新增 locales/ 和 useParticleLocale hook - tilemap-editor: 新增 locales/ 和 useTilemapLocale hook - ui-editor: 新增 locales/ 和 useUILocale hook ### 类型安全改进 - 修复 Debug 工具使用公共接口替代 as any - 修复 ChunkStreamingSystem 添加 forEachChunk 公共方法 - 修复 blueprint-editor 移除不必要的向后兼容代码 * fix(behavior-tree-editor): 使用 ServiceToken 模式修复服务解析 - 创建 BehaviorTreeServiceToken 遵循"谁定义接口,谁导出Token"原则 - 使用 ServiceToken.id (symbol) 注册服务到 ServiceContainer - 更新 PluginSDKRegistry.resolveService 支持 ServiceToken 检测 - BehaviorTreeEditorPanel 现在使用类型安全的 PluginAPI.resolve * fix(behavior-tree-editor): 使用 ServiceContainer.resolve 获取类注册的服务 * fix: 修复多个包的依赖和类型问题 - core: EntityDataCollector.getEntityDetails 使用 HierarchySystem 获取父实体 - ui-editor: 添加 @esengine/editor-runtime 依赖 - tilemap-editor: 添加 @esengine/editor-runtime 依赖 - particle-editor: 添加 @esengine/editor-runtime 依赖
This commit is contained in:
@@ -2,6 +2,7 @@ import { React, useRef, useEffect, useState, useMemo, Icons } from '@esengine/ed
|
||||
import type { LucideIcon } from '@esengine/editor-runtime';
|
||||
import type { NodeTemplate } from '@esengine/behavior-tree';
|
||||
import { NodeFactory } from '../../infrastructure/factories/NodeFactory';
|
||||
import { useBTLocale } from '../../hooks/useBTLocale';
|
||||
|
||||
const { Search, X, ChevronDown, ChevronRight } = Icons;
|
||||
|
||||
@@ -35,6 +36,7 @@ export const QuickCreateMenu: React.FC<QuickCreateMenuProps> = ({
|
||||
onNodeSelect,
|
||||
onClose
|
||||
}) => {
|
||||
const { t } = useBTLocale();
|
||||
const selectedNodeRef = useRef<HTMLDivElement>(null);
|
||||
const [expandedCategories, setExpandedCategories] = useState<Set<string>>(new Set());
|
||||
const [shouldAutoScroll, setShouldAutoScroll] = useState(false);
|
||||
@@ -52,11 +54,12 @@ export const QuickCreateMenu: React.FC<QuickCreateMenuProps> = ({
|
||||
})
|
||||
: allTemplates;
|
||||
|
||||
const uncategorizedLabel = t('quickCreate.uncategorized');
|
||||
const categoryGroups: CategoryGroup[] = React.useMemo(() => {
|
||||
const groups = new Map<string, NodeTemplate[]>();
|
||||
|
||||
filteredTemplates.forEach((template: NodeTemplate) => {
|
||||
const category = template.category || '未分类';
|
||||
const category = template.category || uncategorizedLabel;
|
||||
if (!groups.has(category)) {
|
||||
groups.set(category, []);
|
||||
}
|
||||
@@ -68,7 +71,7 @@ export const QuickCreateMenu: React.FC<QuickCreateMenuProps> = ({
|
||||
templates,
|
||||
isExpanded: searchTextLower ? true : expandedCategories.has(category)
|
||||
})).sort((a, b) => a.category.localeCompare(b.category));
|
||||
}, [filteredTemplates, expandedCategories, searchTextLower]);
|
||||
}, [filteredTemplates, expandedCategories, searchTextLower, uncategorizedLabel]);
|
||||
|
||||
const flattenedTemplates = React.useMemo(() => {
|
||||
return categoryGroups.flatMap((group) =>
|
||||
@@ -90,10 +93,10 @@ export const QuickCreateMenu: React.FC<QuickCreateMenuProps> = ({
|
||||
|
||||
useEffect(() => {
|
||||
if (allTemplates.length > 0 && expandedCategories.size === 0) {
|
||||
const categories = new Set(allTemplates.map((t) => t.category || '未分类'));
|
||||
const categories = new Set(allTemplates.map((tmpl) => tmpl.category || uncategorizedLabel));
|
||||
setExpandedCategories(categories);
|
||||
}
|
||||
}, [allTemplates, expandedCategories.size]);
|
||||
}, [allTemplates, expandedCategories.size, uncategorizedLabel]);
|
||||
|
||||
useEffect(() => {
|
||||
if (shouldAutoScroll && selectedNodeRef.current) {
|
||||
@@ -161,7 +164,7 @@ export const QuickCreateMenu: React.FC<QuickCreateMenuProps> = ({
|
||||
<Search size={16} style={{ color: '#999', flexShrink: 0 }} />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="搜索节点..."
|
||||
placeholder={t('quickCreate.searchPlaceholder')}
|
||||
autoFocus
|
||||
value={searchText}
|
||||
onChange={(e) => {
|
||||
@@ -229,7 +232,7 @@ export const QuickCreateMenu: React.FC<QuickCreateMenuProps> = ({
|
||||
color: '#666',
|
||||
fontSize: '12px'
|
||||
}}>
|
||||
未找到匹配的节点
|
||||
{t('quickCreate.noMatchingNodes')}
|
||||
</div>
|
||||
) : (
|
||||
categoryGroups.map((group) => {
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
} from '@esengine/editor-runtime';
|
||||
import { useBehaviorTreeDataStore } from '../../stores';
|
||||
import { BehaviorTreeEditor } from '../BehaviorTreeEditor';
|
||||
import { BehaviorTreeService } from '../../services/BehaviorTreeService';
|
||||
import { BehaviorTreeServiceToken } from '../../tokens';
|
||||
import { showToast } from '../../services/NotificationService';
|
||||
import { Node as BehaviorTreeNode } from '../../domain/models/Node';
|
||||
import { BehaviorTree } from '../../domain/models/BehaviorTree';
|
||||
@@ -171,7 +171,7 @@ export const BehaviorTreeEditorPanel: React.FC<BehaviorTreeEditorPanelProps> = (
|
||||
filePath = selected;
|
||||
}
|
||||
|
||||
const service = PluginAPI.resolve<BehaviorTreeService>(BehaviorTreeService);
|
||||
const service = PluginAPI.resolve(BehaviorTreeServiceToken);
|
||||
await service.saveToFile(filePath);
|
||||
|
||||
setCurrentFilePath(filePath);
|
||||
@@ -205,7 +205,7 @@ export const BehaviorTreeEditorPanel: React.FC<BehaviorTreeEditorPanelProps> = (
|
||||
if (!selected) return;
|
||||
|
||||
const filePath = selected as string;
|
||||
const service = PluginAPI.resolve<BehaviorTreeService>(BehaviorTreeService);
|
||||
const service = PluginAPI.resolve(BehaviorTreeServiceToken);
|
||||
await service.loadFromFile(filePath);
|
||||
|
||||
setCurrentFilePath(filePath);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { React, Icons } from '@esengine/editor-runtime';
|
||||
import { useBTLocale } from '../../hooks/useBTLocale';
|
||||
|
||||
const { Play, Pause, Square, SkipForward, Undo, Redo, ZoomIn, Save, FolderOpen, Download, Clipboard, Home } = Icons;
|
||||
|
||||
@@ -43,6 +44,8 @@ export const EditorToolbar: React.FC<EditorToolbarProps> = ({
|
||||
onCopyToClipboard,
|
||||
onGoToRoot
|
||||
}) => {
|
||||
const { t } = useBTLocale();
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
@@ -81,7 +84,7 @@ export const EditorToolbar: React.FC<EditorToolbarProps> = ({
|
||||
alignItems: 'center',
|
||||
transition: 'all 0.15s'
|
||||
}}
|
||||
title="打开文件 (Ctrl+O)"
|
||||
title={t('toolbar.openFile')}
|
||||
onMouseEnter={(e) => e.currentTarget.style.backgroundColor = '#4a4a4a'}
|
||||
onMouseLeave={(e) => e.currentTarget.style.backgroundColor = '#3c3c3c'}
|
||||
>
|
||||
@@ -104,7 +107,7 @@ export const EditorToolbar: React.FC<EditorToolbarProps> = ({
|
||||
alignItems: 'center',
|
||||
transition: 'all 0.15s'
|
||||
}}
|
||||
title={`保存 (Ctrl+S)${hasUnsavedChanges ? ' - 有未保存的更改' : ''}`}
|
||||
title={hasUnsavedChanges ? t('toolbar.saveUnsaved') : t('toolbar.save')}
|
||||
onMouseEnter={(e) => e.currentTarget.style.backgroundColor = hasUnsavedChanges ? '#1d4ed8' : '#4a4a4a'}
|
||||
onMouseLeave={(e) => e.currentTarget.style.backgroundColor = hasUnsavedChanges ? '#2563eb' : '#3c3c3c'}
|
||||
>
|
||||
@@ -127,7 +130,7 @@ export const EditorToolbar: React.FC<EditorToolbarProps> = ({
|
||||
alignItems: 'center',
|
||||
transition: 'all 0.15s'
|
||||
}}
|
||||
title="导出运行时配置"
|
||||
title={t('toolbar.export')}
|
||||
onMouseEnter={(e) => e.currentTarget.style.backgroundColor = '#4a4a4a'}
|
||||
onMouseLeave={(e) => e.currentTarget.style.backgroundColor = '#3c3c3c'}
|
||||
>
|
||||
@@ -150,7 +153,7 @@ export const EditorToolbar: React.FC<EditorToolbarProps> = ({
|
||||
alignItems: 'center',
|
||||
transition: 'all 0.15s'
|
||||
}}
|
||||
title="复制JSON到剪贴板"
|
||||
title={t('toolbar.copyToClipboard')}
|
||||
onMouseEnter={(e) => e.currentTarget.style.backgroundColor = '#4a4a4a'}
|
||||
onMouseLeave={(e) => e.currentTarget.style.backgroundColor = '#3c3c3c'}
|
||||
>
|
||||
@@ -192,7 +195,7 @@ export const EditorToolbar: React.FC<EditorToolbarProps> = ({
|
||||
gap: '4px',
|
||||
transition: 'all 0.15s'
|
||||
}}
|
||||
title="运行 (Play)"
|
||||
title={t('toolbar.run')}
|
||||
onMouseEnter={(e) => {
|
||||
if (executionMode !== 'running') {
|
||||
e.currentTarget.style.backgroundColor = '#15803d';
|
||||
@@ -223,7 +226,7 @@ export const EditorToolbar: React.FC<EditorToolbarProps> = ({
|
||||
alignItems: 'center',
|
||||
transition: 'all 0.15s'
|
||||
}}
|
||||
title={executionMode === 'paused' ? '继续' : '暂停'}
|
||||
title={executionMode === 'paused' ? t('toolbar.resume') : t('toolbar.pause')}
|
||||
onMouseEnter={(e) => {
|
||||
if (executionMode !== 'idle') {
|
||||
e.currentTarget.style.backgroundColor = '#d97706';
|
||||
@@ -254,7 +257,7 @@ export const EditorToolbar: React.FC<EditorToolbarProps> = ({
|
||||
alignItems: 'center',
|
||||
transition: 'all 0.15s'
|
||||
}}
|
||||
title="停止"
|
||||
title={t('toolbar.stop')}
|
||||
onMouseEnter={(e) => {
|
||||
if (executionMode !== 'idle') {
|
||||
e.currentTarget.style.backgroundColor = '#b91c1c';
|
||||
@@ -285,7 +288,7 @@ export const EditorToolbar: React.FC<EditorToolbarProps> = ({
|
||||
alignItems: 'center',
|
||||
transition: 'all 0.15s'
|
||||
}}
|
||||
title="单步执行"
|
||||
title={t('toolbar.step')}
|
||||
onMouseEnter={(e) => {
|
||||
if (executionMode === 'idle' || executionMode === 'paused') {
|
||||
e.currentTarget.style.backgroundColor = '#2563eb';
|
||||
@@ -324,7 +327,7 @@ export const EditorToolbar: React.FC<EditorToolbarProps> = ({
|
||||
gap: '4px',
|
||||
transition: 'all 0.15s'
|
||||
}}
|
||||
title="重置视图 (滚轮缩放, Alt+拖动平移)"
|
||||
title={t('toolbar.resetView')}
|
||||
onMouseEnter={(e) => e.currentTarget.style.backgroundColor = '#4a4a4a'}
|
||||
onMouseLeave={(e) => e.currentTarget.style.backgroundColor = '#3c3c3c'}
|
||||
>
|
||||
@@ -362,7 +365,7 @@ export const EditorToolbar: React.FC<EditorToolbarProps> = ({
|
||||
alignItems: 'center',
|
||||
transition: 'all 0.15s'
|
||||
}}
|
||||
title="撤销 (Ctrl+Z)"
|
||||
title={t('toolbar.undo')}
|
||||
onMouseEnter={(e) => {
|
||||
if (canUndo) {
|
||||
e.currentTarget.style.backgroundColor = '#4a4a4a';
|
||||
@@ -392,7 +395,7 @@ export const EditorToolbar: React.FC<EditorToolbarProps> = ({
|
||||
alignItems: 'center',
|
||||
transition: 'all 0.15s'
|
||||
}}
|
||||
title="重做 (Ctrl+Shift+Z / Ctrl+Y)"
|
||||
title={t('toolbar.redo')}
|
||||
onMouseEnter={(e) => {
|
||||
if (canRedo) {
|
||||
e.currentTarget.style.backgroundColor = '#4a4a4a';
|
||||
@@ -438,8 +441,8 @@ export const EditorToolbar: React.FC<EditorToolbarProps> = ({
|
||||
color: executionMode === 'running' ? '#16a34a' :
|
||||
executionMode === 'paused' ? '#f59e0b' : '#888'
|
||||
}}>
|
||||
{executionMode === 'idle' ? 'Idle' :
|
||||
executionMode === 'running' ? 'Running' : 'Paused'}
|
||||
{executionMode === 'idle' ? t('execution.idle') :
|
||||
executionMode === 'running' ? t('execution.running') : t('execution.paused')}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -465,7 +468,7 @@ export const EditorToolbar: React.FC<EditorToolbarProps> = ({
|
||||
gap: '4px',
|
||||
transition: 'all 0.15s'
|
||||
}}
|
||||
title="回到根节点"
|
||||
title={t('toolbar.goToRoot')}
|
||||
onMouseEnter={(e) => e.currentTarget.style.backgroundColor = '#4a4a4a'}
|
||||
onMouseLeave={(e) => e.currentTarget.style.backgroundColor = '#3c3c3c'}
|
||||
>
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import { DomainError } from './DomainError';
|
||||
import { translateBT } from '../../hooks/useBTLocale';
|
||||
|
||||
/**
|
||||
* 验证错误
|
||||
* Validation Error
|
||||
*
|
||||
* 当业务规则验证失败时抛出
|
||||
* Thrown when business rule validation fails
|
||||
*/
|
||||
export class ValidationError extends DomainError {
|
||||
constructor(
|
||||
@@ -15,28 +19,28 @@ export class ValidationError extends DomainError {
|
||||
|
||||
static rootNodeMaxChildren(): ValidationError {
|
||||
return new ValidationError(
|
||||
'根节点只能连接一个子节点',
|
||||
translateBT('validation.rootNodeMaxChildren'),
|
||||
'children'
|
||||
);
|
||||
}
|
||||
|
||||
static decoratorNodeMaxChildren(): ValidationError {
|
||||
return new ValidationError(
|
||||
'装饰节点只能连接一个子节点',
|
||||
translateBT('validation.decoratorNodeMaxChildren'),
|
||||
'children'
|
||||
);
|
||||
}
|
||||
|
||||
static leafNodeNoChildren(): ValidationError {
|
||||
return new ValidationError(
|
||||
'叶子节点不能有子节点',
|
||||
translateBT('validation.leafNodeNoChildren'),
|
||||
'children'
|
||||
);
|
||||
}
|
||||
|
||||
static circularReference(nodeId: string): ValidationError {
|
||||
return new ValidationError(
|
||||
`检测到循环引用,节点 ${nodeId} 不能连接到自己或其子节点`,
|
||||
translateBT('validation.circularReference', undefined, { nodeId }),
|
||||
'connection',
|
||||
nodeId
|
||||
);
|
||||
@@ -44,7 +48,7 @@ export class ValidationError extends DomainError {
|
||||
|
||||
static invalidConnection(from: string, to: string, reason: string): ValidationError {
|
||||
return new ValidationError(
|
||||
`无效的连接:${reason}`,
|
||||
translateBT('validation.invalidConnection', undefined, { reason }),
|
||||
'connection',
|
||||
{ from, to }
|
||||
);
|
||||
|
||||
@@ -2,3 +2,4 @@ export { useCommandHistory } from './useCommandHistory';
|
||||
export { useNodeOperations } from './useNodeOperations';
|
||||
export { useConnectionOperations } from './useConnectionOperations';
|
||||
export { useCanvasInteraction } from './useCanvasInteraction';
|
||||
export { useBTLocale, translateBT } from './useBTLocale';
|
||||
|
||||
73
packages/behavior-tree-editor/src/hooks/useBTLocale.ts
Normal file
73
packages/behavior-tree-editor/src/hooks/useBTLocale.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* Behavior Tree Editor Locale Hook
|
||||
* 行为树编辑器语言钩子
|
||||
*
|
||||
* Uses the unified plugin i18n infrastructure from editor-runtime.
|
||||
* 使用 editor-runtime 的统一插件国际化基础设施。
|
||||
*/
|
||||
import {
|
||||
createPluginLocale,
|
||||
createPluginTranslator,
|
||||
getCurrentLocale
|
||||
} from '@esengine/editor-runtime';
|
||||
import { en, zh, es } from '../locales';
|
||||
import type { Locale, TranslationParams } from '@esengine/editor-core';
|
||||
|
||||
// Create translations bundle
|
||||
// 创建翻译包
|
||||
const translations = { en, zh, es };
|
||||
|
||||
/**
|
||||
* Hook for accessing behavior tree editor translations
|
||||
* 访问行为树编辑器翻译的 Hook
|
||||
*
|
||||
* Uses the unified createPluginLocale factory from editor-runtime.
|
||||
* 使用 editor-runtime 的统一 createPluginLocale 工厂。
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* const { t, locale } = useBTLocale();
|
||||
* return <button title={t('toolbar.save')}>{t('toolbar.saveUnsaved')}</button>;
|
||||
* ```
|
||||
*/
|
||||
export const useBTLocale = createPluginLocale(translations);
|
||||
|
||||
// Create non-React translator using the unified infrastructure
|
||||
// 使用统一基础设施创建非 React 翻译器
|
||||
const btTranslator = createPluginTranslator(translations);
|
||||
|
||||
/**
|
||||
* Non-React translation function for behavior tree editor
|
||||
* 行为树编辑器的非 React 翻译函数
|
||||
*
|
||||
* Use this in services, utilities, and other non-React contexts.
|
||||
* 在服务、工具类和其他非 React 上下文中使用。
|
||||
*
|
||||
* @param key - Translation key | 翻译键
|
||||
* @param locale - Optional locale, defaults to current locale | 可选语言,默认使用当前语言
|
||||
* @param params - Optional interpolation parameters | 可选插值参数
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // With explicit locale
|
||||
* translateBT('errors.notFound', 'zh');
|
||||
*
|
||||
* // With current locale (auto-detected)
|
||||
* translateBT('errors.notFound');
|
||||
*
|
||||
* // With parameters
|
||||
* translateBT('messages.saved', undefined, { name: 'MyTree' });
|
||||
* ```
|
||||
*/
|
||||
export function translateBT(
|
||||
key: string,
|
||||
locale?: Locale,
|
||||
params?: TranslationParams
|
||||
): string {
|
||||
const targetLocale = locale || getCurrentLocale();
|
||||
return btTranslator(key, targetLocale, params);
|
||||
}
|
||||
|
||||
// Re-export for external use
|
||||
// 重新导出供外部使用
|
||||
export { getCurrentLocale } from '@esengine/editor-runtime';
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
type IFileSystem,
|
||||
createLogger,
|
||||
PluginAPI,
|
||||
LocaleService,
|
||||
} from '@esengine/editor-runtime';
|
||||
|
||||
// Runtime imports from @esengine/behavior-tree package
|
||||
@@ -35,6 +36,7 @@ import { BehaviorTreeRuntimeComponent, BehaviorTreeRuntimeModule } from '@esengi
|
||||
// Editor components and services
|
||||
import { BehaviorTreeService } from './services/BehaviorTreeService';
|
||||
import { FileSystemService } from './services/FileSystemService';
|
||||
import { BehaviorTreeServiceToken, type IBehaviorTreeService } from './tokens';
|
||||
import { BehaviorTreeCompiler } from './compiler/BehaviorTreeCompiler';
|
||||
import { BehaviorTreeNodeInspectorProvider } from './providers/BehaviorTreeNodeInspectorProvider';
|
||||
import { BehaviorTreeEditorPanel } from './components/panels/BehaviorTreeEditorPanel';
|
||||
@@ -45,6 +47,9 @@ import { PluginContext } from './PluginContext';
|
||||
// Import manifest from local file
|
||||
import { manifest } from './BehaviorTreePlugin';
|
||||
|
||||
// Import locale translations
|
||||
import { en, zh, es } from './locales';
|
||||
|
||||
// 导入编辑器 CSS 样式(会被 vite 自动处理并注入到 DOM)
|
||||
// Import editor CSS styles (automatically handled and injected by vite)
|
||||
import './styles/BehaviorTreeNode.css';
|
||||
@@ -81,6 +86,9 @@ export class BehaviorTreeEditorModule implements IEditorModuleLoader {
|
||||
// 订阅创建资产消息
|
||||
this.subscribeToMessages(services);
|
||||
|
||||
// 注册翻译 | Register translations
|
||||
this.registerTranslations(services);
|
||||
|
||||
logger.info('BehaviorTree editor module installed');
|
||||
}
|
||||
|
||||
@@ -173,7 +181,7 @@ export class BehaviorTreeEditorModule implements IEditorModuleLoader {
|
||||
|
||||
if (this.services) {
|
||||
this.services.unregister(FileSystemService);
|
||||
this.services.unregister(BehaviorTreeService);
|
||||
this.services.unregister(BehaviorTreeServiceToken.id);
|
||||
}
|
||||
|
||||
useBehaviorTreeDataStore.getState().reset();
|
||||
@@ -190,11 +198,16 @@ export class BehaviorTreeEditorModule implements IEditorModuleLoader {
|
||||
}
|
||||
services.registerSingleton(FileSystemService);
|
||||
|
||||
// BehaviorTreeService
|
||||
if (services.isRegistered(BehaviorTreeService)) {
|
||||
services.unregister(BehaviorTreeService);
|
||||
// BehaviorTreeService - 使用 ServiceToken.id (symbol) 注册
|
||||
// BehaviorTreeService - register with ServiceToken.id (symbol)
|
||||
// ServiceContainer 支持 symbol 作为 ServiceIdentifier
|
||||
// ServiceContainer supports symbol as ServiceIdentifier
|
||||
const tokenId = BehaviorTreeServiceToken.id;
|
||||
if (services.isRegistered(tokenId)) {
|
||||
services.unregister(tokenId);
|
||||
}
|
||||
services.registerSingleton(BehaviorTreeService);
|
||||
const btService = new BehaviorTreeService();
|
||||
services.registerInstance(tokenId, btService);
|
||||
}
|
||||
|
||||
private registerCompilers(services: ServiceContainer): void {
|
||||
@@ -208,6 +221,22 @@ export class BehaviorTreeEditorModule implements IEditorModuleLoader {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册插件翻译到 LocaleService
|
||||
* Register plugin translations to LocaleService
|
||||
*/
|
||||
private registerTranslations(services: ServiceContainer): void {
|
||||
try {
|
||||
const localeService = services.tryResolve<LocaleService>(LocaleService);
|
||||
if (localeService) {
|
||||
localeService.extendTranslations('behaviorTree', { en, zh, es });
|
||||
logger.info('BehaviorTree translations registered');
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn('Failed to register translations:', error);
|
||||
}
|
||||
}
|
||||
|
||||
private registerInspectorProviders(services: ServiceContainer): void {
|
||||
try {
|
||||
const inspectorRegistry = services.resolve<InspectorRegistry>(IInspectorRegistry);
|
||||
@@ -284,7 +313,9 @@ export class BehaviorTreeEditorModule implements IEditorModuleLoader {
|
||||
extensions: ['btree'],
|
||||
onDoubleClick: async (filePath: string) => {
|
||||
if (this.services) {
|
||||
const service = this.services.resolve(BehaviorTreeService);
|
||||
// 使用 ServiceToken.id 解析服务
|
||||
// Resolve service using ServiceToken.id
|
||||
const service = this.services.resolve<IBehaviorTreeService>(BehaviorTreeServiceToken.id);
|
||||
if (service) {
|
||||
await service.loadFromFile(filePath);
|
||||
}
|
||||
@@ -351,6 +382,7 @@ export { BehaviorTreeRuntimeModule };
|
||||
export { PluginContext } from './PluginContext';
|
||||
export { BehaviorTreeEditorPanel } from './components/panels/BehaviorTreeEditorPanel';
|
||||
export * from './services/BehaviorTreeService';
|
||||
export * from './tokens';
|
||||
export * from './providers/BehaviorTreeNodeInspectorProvider';
|
||||
|
||||
export * from './domain';
|
||||
|
||||
@@ -2,9 +2,11 @@ import { IValidator, ValidationResult, ValidationError } from '../../domain/inte
|
||||
import { BehaviorTree } from '../../domain/models/BehaviorTree';
|
||||
import { Node } from '../../domain/models/Node';
|
||||
import { Connection } from '../../domain/models/Connection';
|
||||
import { translateBT } from '../../hooks/useBTLocale';
|
||||
|
||||
/**
|
||||
* 行为树验证器实现
|
||||
* Behavior Tree Validator Implementation
|
||||
*/
|
||||
export class BehaviorTreeValidator implements IValidator {
|
||||
/**
|
||||
@@ -37,21 +39,22 @@ export class BehaviorTreeValidator implements IValidator {
|
||||
|
||||
/**
|
||||
* 验证节点
|
||||
* Validate node
|
||||
*/
|
||||
validateNode(node: Node): ValidationResult {
|
||||
const errors: ValidationError[] = [];
|
||||
|
||||
// 验证节点必填字段
|
||||
// 验证节点必填字段 | Validate required fields
|
||||
if (!node.id) {
|
||||
errors.push({
|
||||
message: '节点 ID 不能为空',
|
||||
message: translateBT('validation.nodeIdRequired'),
|
||||
nodeId: node.id
|
||||
});
|
||||
}
|
||||
|
||||
if (!node.template) {
|
||||
errors.push({
|
||||
message: '节点模板不能为空',
|
||||
message: translateBT('validation.nodeTemplateRequired'),
|
||||
nodeId: node.id
|
||||
});
|
||||
}
|
||||
@@ -64,30 +67,31 @@ export class BehaviorTreeValidator implements IValidator {
|
||||
|
||||
/**
|
||||
* 验证连接
|
||||
* Validate connection
|
||||
*/
|
||||
validateConnection(connection: Connection, tree: BehaviorTree): ValidationResult {
|
||||
const errors: ValidationError[] = [];
|
||||
|
||||
// 验证连接的源节点和目标节点都存在
|
||||
// 验证连接的源节点和目标节点都存在 | Validate source and target nodes exist
|
||||
const fromNode = tree.nodes.find((n) => n.id === connection.from);
|
||||
const toNode = tree.nodes.find((n) => n.id === connection.to);
|
||||
|
||||
if (!fromNode) {
|
||||
errors.push({
|
||||
message: `连接的源节点不存在: ${connection.from}`
|
||||
message: translateBT('validation.sourceNodeNotFound', undefined, { nodeId: connection.from })
|
||||
});
|
||||
}
|
||||
|
||||
if (!toNode) {
|
||||
errors.push({
|
||||
message: `连接的目标节点不存在: ${connection.to}`
|
||||
message: translateBT('validation.targetNodeNotFound', undefined, { nodeId: connection.to })
|
||||
});
|
||||
}
|
||||
|
||||
// 不能自己连接自己
|
||||
// 不能自己连接自己 | Cannot connect to self
|
||||
if (connection.from === connection.to) {
|
||||
errors.push({
|
||||
message: '节点不能连接到自己',
|
||||
message: translateBT('validation.selfConnection'),
|
||||
nodeId: connection.from
|
||||
});
|
||||
}
|
||||
@@ -100,6 +104,7 @@ export class BehaviorTreeValidator implements IValidator {
|
||||
|
||||
/**
|
||||
* 验证是否会产生循环引用
|
||||
* Validate no cycles exist
|
||||
*/
|
||||
validateNoCycles(tree: BehaviorTree): ValidationResult {
|
||||
const errors: ValidationError[] = [];
|
||||
@@ -134,7 +139,7 @@ export class BehaviorTreeValidator implements IValidator {
|
||||
for (const node of tree.nodes) {
|
||||
if (hasCycle(node.id)) {
|
||||
errors.push({
|
||||
message: '行为树中存在循环引用',
|
||||
message: translateBT('validation.cycleDetected'),
|
||||
nodeId: node.id
|
||||
});
|
||||
break;
|
||||
|
||||
137
packages/behavior-tree-editor/src/locales/en.ts
Normal file
137
packages/behavior-tree-editor/src/locales/en.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
/**
|
||||
* English translations for Behavior Tree Editor
|
||||
* 行为树编辑器英文翻译
|
||||
*/
|
||||
export const en = {
|
||||
// ========================================
|
||||
// Toolbar
|
||||
// ========================================
|
||||
toolbar: {
|
||||
openFile: 'Open File (Ctrl+O)',
|
||||
save: 'Save (Ctrl+S)',
|
||||
saveUnsaved: 'Save (Ctrl+S) - Unsaved changes',
|
||||
export: 'Export Runtime Config',
|
||||
copyToClipboard: 'Copy JSON to Clipboard',
|
||||
run: 'Run (Play)',
|
||||
resume: 'Resume',
|
||||
pause: 'Pause',
|
||||
stop: 'Stop',
|
||||
step: 'Step',
|
||||
resetView: 'Reset View (scroll to zoom, Alt+drag to pan)',
|
||||
undo: 'Undo (Ctrl+Z)',
|
||||
redo: 'Redo (Ctrl+Shift+Z / Ctrl+Y)',
|
||||
goToRoot: 'Go to Root Node'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Execution Status
|
||||
// ========================================
|
||||
execution: {
|
||||
idle: 'Idle',
|
||||
running: 'Running',
|
||||
paused: 'Paused'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Node
|
||||
// ========================================
|
||||
node: {
|
||||
executionOrder: 'Execution Order: {{order}}',
|
||||
initialValue: 'Initial Value',
|
||||
currentValue: 'Current Value'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Context Menu
|
||||
// ========================================
|
||||
contextMenu: {
|
||||
delete: 'Delete',
|
||||
duplicate: 'Duplicate',
|
||||
copy: 'Copy',
|
||||
paste: 'Paste'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Quick Create Menu
|
||||
// ========================================
|
||||
quickCreate: {
|
||||
searchPlaceholder: 'Search nodes...',
|
||||
uncategorized: 'Uncategorized',
|
||||
noMatchingNodes: 'No matching nodes found'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Blackboard Panel
|
||||
// ========================================
|
||||
blackboard: {
|
||||
title: 'Blackboard',
|
||||
variableName: 'variable.name',
|
||||
copy: 'Copy',
|
||||
edit: 'Edit',
|
||||
delete: 'Delete',
|
||||
addVariable: 'Add Variable'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Compiler
|
||||
// ========================================
|
||||
compiler: {
|
||||
name: 'Behavior Tree Compiler',
|
||||
description: 'Compile behavior tree assets',
|
||||
selectAssetOutput: 'Select asset output directory...',
|
||||
selectTypeOutput: 'Select type definition output directory...',
|
||||
compile: 'Compile',
|
||||
compiling: 'Compiling...',
|
||||
success: 'Compilation successful',
|
||||
failed: 'Compilation failed'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Notifications
|
||||
// ========================================
|
||||
notifications: {
|
||||
fileSaved: 'File saved: {{path}}',
|
||||
fileSaveFailed: 'Failed to save file',
|
||||
fileOpened: 'File opened: {{path}}',
|
||||
fileOpenFailed: 'Failed to open file',
|
||||
copiedToClipboard: 'Copied to clipboard',
|
||||
exportSuccess: 'Export successful',
|
||||
exportFailed: 'Export failed',
|
||||
validationError: 'Validation error: {{message}}'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Dialogs
|
||||
// ========================================
|
||||
dialogs: {
|
||||
createBehaviorTree: 'Create Behavior Tree Asset',
|
||||
confirmDelete: 'Are you sure you want to delete this node?',
|
||||
unsavedChanges: 'You have unsaved changes. Do you want to save before closing?'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Panel
|
||||
// ========================================
|
||||
panel: {
|
||||
title: 'Behavior Tree Editor',
|
||||
noFileOpen: 'No behavior tree file is open',
|
||||
dropToOpen: 'Drop a .btree file here or use Open button'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Validation Errors
|
||||
// ========================================
|
||||
validation: {
|
||||
rootNodeMaxChildren: 'Root node can only connect to one child node',
|
||||
decoratorNodeMaxChildren: 'Decorator node can only connect to one child node',
|
||||
leafNodeNoChildren: 'Leaf node cannot have children',
|
||||
circularReference: 'Circular reference detected, node {{nodeId}} cannot connect to itself or its descendants',
|
||||
invalidConnection: 'Invalid connection: {{reason}}',
|
||||
nodeIdRequired: 'Node ID cannot be empty',
|
||||
nodeTemplateRequired: 'Node template cannot be empty',
|
||||
sourceNodeNotFound: 'Connection source node not found: {{nodeId}}',
|
||||
targetNodeNotFound: 'Connection target node not found: {{nodeId}}',
|
||||
selfConnection: 'Node cannot connect to itself',
|
||||
cycleDetected: 'Circular reference detected in behavior tree'
|
||||
}
|
||||
};
|
||||
138
packages/behavior-tree-editor/src/locales/es.ts
Normal file
138
packages/behavior-tree-editor/src/locales/es.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
/**
|
||||
* Spanish translations for Behavior Tree Editor
|
||||
* Traducciones en español del Editor de Árbol de Comportamiento
|
||||
*/
|
||||
export const es = {
|
||||
// ========================================
|
||||
// Toolbar
|
||||
// ========================================
|
||||
toolbar: {
|
||||
openFile: 'Abrir Archivo (Ctrl+O)',
|
||||
save: 'Guardar (Ctrl+S)',
|
||||
saveUnsaved: 'Guardar (Ctrl+S) - Cambios sin guardar',
|
||||
export: 'Exportar Configuración de Ejecución',
|
||||
copyToClipboard: 'Copiar JSON al Portapapeles',
|
||||
run: 'Ejecutar (Play)',
|
||||
resume: 'Continuar',
|
||||
pause: 'Pausar',
|
||||
stop: 'Detener',
|
||||
step: 'Paso a Paso',
|
||||
resetView: 'Restablecer Vista (scroll para zoom, Alt+arrastrar para desplazar)',
|
||||
undo: 'Deshacer (Ctrl+Z)',
|
||||
redo: 'Rehacer (Ctrl+Shift+Z / Ctrl+Y)',
|
||||
goToRoot: 'Ir al Nodo Raíz'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Execution Status
|
||||
// ========================================
|
||||
execution: {
|
||||
idle: 'Inactivo',
|
||||
running: 'Ejecutando',
|
||||
paused: 'Pausado'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Node
|
||||
// ========================================
|
||||
node: {
|
||||
executionOrder: 'Orden de Ejecución: {{order}}',
|
||||
initialValue: 'Valor Inicial',
|
||||
currentValue: 'Valor Actual'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Context Menu
|
||||
// ========================================
|
||||
contextMenu: {
|
||||
delete: 'Eliminar',
|
||||
duplicate: 'Duplicar',
|
||||
copy: 'Copiar',
|
||||
paste: 'Pegar'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Quick Create Menu
|
||||
// ========================================
|
||||
quickCreate: {
|
||||
searchPlaceholder: 'Buscar nodos...',
|
||||
uncategorized: 'Sin categoría',
|
||||
noMatchingNodes: 'No se encontraron nodos coincidentes'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Blackboard Panel
|
||||
// ========================================
|
||||
blackboard: {
|
||||
title: 'Pizarra',
|
||||
variableName: 'nombre.variable',
|
||||
copy: 'Copiar',
|
||||
edit: 'Editar',
|
||||
delete: 'Eliminar',
|
||||
addVariable: 'Agregar Variable'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Compiler
|
||||
// ========================================
|
||||
compiler: {
|
||||
name: 'Compilador de Árbol de Comportamiento',
|
||||
description: 'Compilar recursos de árbol de comportamiento',
|
||||
selectAssetOutput: 'Seleccionar directorio de salida de recursos...',
|
||||
selectTypeOutput: 'Seleccionar directorio de salida de definiciones de tipo...',
|
||||
compile: 'Compilar',
|
||||
compiling: 'Compilando...',
|
||||
success: 'Compilación exitosa',
|
||||
failed: 'Compilación fallida'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Notifications
|
||||
// ========================================
|
||||
notifications: {
|
||||
fileSaved: 'Archivo guardado: {{path}}',
|
||||
fileSaveFailed: 'Error al guardar archivo',
|
||||
fileOpened: 'Archivo abierto: {{path}}',
|
||||
fileOpenFailed: 'Error al abrir archivo',
|
||||
copiedToClipboard: 'Copiado al portapapeles',
|
||||
exportSuccess: 'Exportación exitosa',
|
||||
exportFailed: 'Exportación fallida',
|
||||
validationError: 'Error de validación: {{message}}'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Dialogs
|
||||
// ========================================
|
||||
dialogs: {
|
||||
createBehaviorTree: 'Crear Recurso de Árbol de Comportamiento',
|
||||
confirmDelete: '¿Está seguro de que desea eliminar este nodo?',
|
||||
unsavedChanges: 'Tiene cambios sin guardar. ¿Desea guardar antes de cerrar?'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Panel
|
||||
// ========================================
|
||||
panel: {
|
||||
title: 'Editor de Árbol de Comportamiento',
|
||||
noFileOpen: 'No hay archivo de árbol de comportamiento abierto',
|
||||
dropToOpen: 'Arrastre un archivo .btree aquí o use el botón Abrir'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Validation Errors
|
||||
// Errores de validación
|
||||
// ========================================
|
||||
validation: {
|
||||
rootNodeMaxChildren: 'El nodo raíz solo puede conectar a un nodo hijo',
|
||||
decoratorNodeMaxChildren: 'El nodo decorador solo puede conectar a un nodo hijo',
|
||||
leafNodeNoChildren: 'El nodo hoja no puede tener hijos',
|
||||
circularReference: 'Referencia circular detectada, el nodo {{nodeId}} no puede conectarse a sí mismo o a sus descendientes',
|
||||
invalidConnection: 'Conexión inválida: {{reason}}',
|
||||
nodeIdRequired: 'El ID del nodo no puede estar vacío',
|
||||
nodeTemplateRequired: 'La plantilla del nodo no puede estar vacía',
|
||||
sourceNodeNotFound: 'Nodo fuente de conexión no encontrado: {{nodeId}}',
|
||||
targetNodeNotFound: 'Nodo destino de conexión no encontrado: {{nodeId}}',
|
||||
selfConnection: 'El nodo no puede conectarse a sí mismo',
|
||||
cycleDetected: 'Referencia circular detectada en el árbol de comportamiento'
|
||||
}
|
||||
};
|
||||
9
packages/behavior-tree-editor/src/locales/index.ts
Normal file
9
packages/behavior-tree-editor/src/locales/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Behavior Tree Editor Locales
|
||||
* 行为树编辑器多语言支持
|
||||
*/
|
||||
export { en } from './en';
|
||||
export { zh } from './zh';
|
||||
export { es } from './es';
|
||||
|
||||
export type BehaviorTreeTranslations = typeof import('./en').en;
|
||||
138
packages/behavior-tree-editor/src/locales/zh.ts
Normal file
138
packages/behavior-tree-editor/src/locales/zh.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
/**
|
||||
* Chinese translations for Behavior Tree Editor
|
||||
* 行为树编辑器中文翻译
|
||||
*/
|
||||
export const zh = {
|
||||
// ========================================
|
||||
// Toolbar
|
||||
// ========================================
|
||||
toolbar: {
|
||||
openFile: '打开文件 (Ctrl+O)',
|
||||
save: '保存 (Ctrl+S)',
|
||||
saveUnsaved: '保存 (Ctrl+S) - 有未保存的更改',
|
||||
export: '导出运行时配置',
|
||||
copyToClipboard: '复制JSON到剪贴板',
|
||||
run: '运行 (Play)',
|
||||
resume: '继续',
|
||||
pause: '暂停',
|
||||
stop: '停止',
|
||||
step: '单步执行',
|
||||
resetView: '重置视图 (滚轮缩放, Alt+拖动平移)',
|
||||
undo: '撤销 (Ctrl+Z)',
|
||||
redo: '重做 (Ctrl+Shift+Z / Ctrl+Y)',
|
||||
goToRoot: '回到根节点'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Execution Status
|
||||
// ========================================
|
||||
execution: {
|
||||
idle: '空闲',
|
||||
running: '运行中',
|
||||
paused: '已暂停'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Node
|
||||
// ========================================
|
||||
node: {
|
||||
executionOrder: '执行顺序: {{order}}',
|
||||
initialValue: '初始值',
|
||||
currentValue: '当前值'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Context Menu
|
||||
// ========================================
|
||||
contextMenu: {
|
||||
delete: '删除',
|
||||
duplicate: '复制',
|
||||
copy: '复制',
|
||||
paste: '粘贴'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Quick Create Menu
|
||||
// ========================================
|
||||
quickCreate: {
|
||||
searchPlaceholder: '搜索节点...',
|
||||
uncategorized: '未分类',
|
||||
noMatchingNodes: '未找到匹配的节点'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Blackboard Panel
|
||||
// ========================================
|
||||
blackboard: {
|
||||
title: '黑板',
|
||||
variableName: '变量名',
|
||||
copy: '复制',
|
||||
edit: '编辑',
|
||||
delete: '删除',
|
||||
addVariable: '添加变量'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Compiler
|
||||
// ========================================
|
||||
compiler: {
|
||||
name: '行为树编译器',
|
||||
description: '编译行为树资产',
|
||||
selectAssetOutput: '选择资产输出目录...',
|
||||
selectTypeOutput: '选择类型定义输出目录...',
|
||||
compile: '编译',
|
||||
compiling: '编译中...',
|
||||
success: '编译成功',
|
||||
failed: '编译失败'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Notifications
|
||||
// ========================================
|
||||
notifications: {
|
||||
fileSaved: '文件已保存: {{path}}',
|
||||
fileSaveFailed: '保存文件失败',
|
||||
fileOpened: '文件已打开: {{path}}',
|
||||
fileOpenFailed: '打开文件失败',
|
||||
copiedToClipboard: '已复制到剪贴板',
|
||||
exportSuccess: '导出成功',
|
||||
exportFailed: '导出失败',
|
||||
validationError: '验证错误: {{message}}'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Dialogs
|
||||
// ========================================
|
||||
dialogs: {
|
||||
createBehaviorTree: '创建行为树资产',
|
||||
confirmDelete: '确定要删除这个节点吗?',
|
||||
unsavedChanges: '有未保存的更改。关闭前是否保存?'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Panel
|
||||
// ========================================
|
||||
panel: {
|
||||
title: '行为树编辑器',
|
||||
noFileOpen: '没有打开的行为树文件',
|
||||
dropToOpen: '拖放 .btree 文件到这里或使用打开按钮'
|
||||
},
|
||||
|
||||
// ========================================
|
||||
// Validation Errors
|
||||
// 验证错误
|
||||
// ========================================
|
||||
validation: {
|
||||
rootNodeMaxChildren: '根节点只能连接一个子节点',
|
||||
decoratorNodeMaxChildren: '装饰节点只能连接一个子节点',
|
||||
leafNodeNoChildren: '叶子节点不能有子节点',
|
||||
circularReference: '检测到循环引用,节点 {{nodeId}} 不能连接到自己或其子节点',
|
||||
invalidConnection: '无效的连接:{{reason}}',
|
||||
nodeIdRequired: '节点 ID 不能为空',
|
||||
nodeTemplateRequired: '节点模板不能为空',
|
||||
sourceNodeNotFound: '连接的源节点不存在: {{nodeId}}',
|
||||
targetNodeNotFound: '连接的目标节点不存在: {{nodeId}}',
|
||||
selfConnection: '节点不能连接到自己',
|
||||
cycleDetected: '行为树中存在循环引用'
|
||||
}
|
||||
};
|
||||
@@ -29,7 +29,9 @@ const PropertyEditor: React.FC<PropertyEditorProps> = ({ property, value, onChan
|
||||
const renderInput = () => {
|
||||
// 特殊处理 treeAssetId 字段使用 asset 编辑器
|
||||
if (property.name === 'treeAssetId') {
|
||||
const fieldRegistry = PluginAPI.resolve<FieldEditorRegistry>(FieldEditorRegistry);
|
||||
// 使用 ServiceContainer.resolve 直接获取类注册的服务
|
||||
// Use ServiceContainer.resolve to get class-registered service directly
|
||||
const fieldRegistry = PluginAPI.services.resolve(FieldEditorRegistry);
|
||||
const assetEditor = fieldRegistry.getEditor('asset');
|
||||
|
||||
if (assetEditor) {
|
||||
@@ -52,7 +54,9 @@ const PropertyEditor: React.FC<PropertyEditorProps> = ({ property, value, onChan
|
||||
|
||||
// 检查是否有特定的字段编辑器类型
|
||||
if (property.fieldEditor) {
|
||||
const fieldRegistry = PluginAPI.resolve<FieldEditorRegistry>(FieldEditorRegistry);
|
||||
// 使用 ServiceContainer.resolve 直接获取类注册的服务
|
||||
// Use ServiceContainer.resolve to get class-registered service directly
|
||||
const fieldRegistry = PluginAPI.services.resolve(FieldEditorRegistry);
|
||||
const editor = fieldRegistry.getEditor(property.fieldEditor.type);
|
||||
|
||||
if (editor) {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import {
|
||||
singleton,
|
||||
type IService,
|
||||
createLogger,
|
||||
MessageHub,
|
||||
IMessageHub,
|
||||
@@ -9,11 +8,12 @@ import { useBehaviorTreeDataStore } from '../application/state/BehaviorTreeDataS
|
||||
import type { BehaviorTree } from '../domain/models/BehaviorTree';
|
||||
import { FileSystemService } from './FileSystemService';
|
||||
import { PluginContext } from '../PluginContext';
|
||||
import type { IBehaviorTreeService } from '../tokens';
|
||||
|
||||
const logger = createLogger('BehaviorTreeService');
|
||||
|
||||
@singleton()
|
||||
export class BehaviorTreeService implements IService {
|
||||
export class BehaviorTreeService implements IBehaviorTreeService {
|
||||
async createNew(): Promise<void> {
|
||||
useBehaviorTreeDataStore.getState().reset();
|
||||
}
|
||||
|
||||
66
packages/behavior-tree-editor/src/tokens.ts
Normal file
66
packages/behavior-tree-editor/src/tokens.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* Behavior Tree Editor Service Tokens
|
||||
* 行为树编辑器服务令牌
|
||||
*
|
||||
* 遵循"谁定义接口,谁导出 Token"的原则。
|
||||
* Following the "who defines interface, who exports token" principle.
|
||||
*/
|
||||
|
||||
import { createServiceToken } from '@esengine/engine-core';
|
||||
import type { IService } from '@esengine/ecs-framework';
|
||||
import type { BehaviorTree } from './domain/models/BehaviorTree';
|
||||
|
||||
/**
|
||||
* 行为树服务接口
|
||||
* Behavior Tree Service Interface
|
||||
*/
|
||||
export interface IBehaviorTreeService extends IService {
|
||||
/**
|
||||
* 创建新的行为树
|
||||
* Create a new behavior tree
|
||||
*/
|
||||
createNew(): Promise<void>;
|
||||
|
||||
/**
|
||||
* 从文件加载行为树
|
||||
* Load behavior tree from file
|
||||
* @param filePath 文件路径 | File path
|
||||
*/
|
||||
loadFromFile(filePath: string): Promise<void>;
|
||||
|
||||
/**
|
||||
* 保存行为树到文件
|
||||
* Save behavior tree to file
|
||||
* @param filePath 文件路径 | File path
|
||||
* @param metadata 可选的元数据 | Optional metadata
|
||||
*/
|
||||
saveToFile(filePath: string, metadata?: { name: string; description: string }): Promise<void>;
|
||||
|
||||
/**
|
||||
* 获取当前行为树
|
||||
* Get current behavior tree
|
||||
*/
|
||||
getCurrentTree(): BehaviorTree;
|
||||
|
||||
/**
|
||||
* 设置行为树
|
||||
* Set behavior tree
|
||||
* @param tree 行为树 | Behavior tree
|
||||
*/
|
||||
setTree(tree: BehaviorTree): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 行为树服务令牌
|
||||
* Behavior Tree Service Token
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { BehaviorTreeServiceToken } from '@esengine/behavior-tree-editor';
|
||||
* import { PluginAPI } from '@esengine/editor-runtime';
|
||||
*
|
||||
* const service = PluginAPI.resolve(BehaviorTreeServiceToken);
|
||||
* await service.loadFromFile('/path/to/tree.btree');
|
||||
* ```
|
||||
*/
|
||||
export const BehaviorTreeServiceToken = createServiceToken<IBehaviorTreeService>('behaviorTreeService');
|
||||
@@ -130,14 +130,21 @@ export class BehaviorTreeExecutor {
|
||||
): BehaviorTreeData {
|
||||
const rootNode = nodes.find((n) => n.id === rootNodeId);
|
||||
if (!rootNode) {
|
||||
throw new Error('未找到根节点');
|
||||
throw new Error('Root node not found');
|
||||
}
|
||||
|
||||
// 如果根节点是编辑器特有的"根节点",跳过它,使用其子节点
|
||||
// If root node is the editor-specific "Root" node, skip it and use its child
|
||||
// 如果根节点是编辑器特有的"Root"节点,跳过它,使用其子节点
|
||||
let actualRootId = rootNodeId;
|
||||
let skipRootNode = false;
|
||||
|
||||
if (rootNode.template.displayName === '根节点') {
|
||||
// Check by className (preferred) or displayName (fallback for old data)
|
||||
// 通过 className(首选)或 displayName(旧数据回退)检查
|
||||
const isEditorRootNode = rootNode.template.className === 'Root' ||
|
||||
rootNode.template.displayName === '根节点' ||
|
||||
rootNode.template.displayName === 'Root';
|
||||
|
||||
if (isEditorRootNode) {
|
||||
skipRootNode = true;
|
||||
|
||||
if (rootNode.children.length === 0) {
|
||||
|
||||
Reference in New Issue
Block a user