beaa1d09de
* feat(prefab): 实现预制体系统和编辑器 UX 改进 ## 预制体系统 - 新增 PrefabSerializer: 预制体序列化/反序列化 - 新增 PrefabInstanceComponent: 追踪预制体实例来源和修改 - 新增 PrefabService: 预制体核心服务 - 新增 PrefabLoader: 预制体资产加载器 - 新增预制体命令: Create/Instantiate/Apply/Revert/BreakLink ## 预制体编辑模式 - 支持双击 .prefab 文件进入编辑模式 - 预制体编辑模式工具栏 (保存/退出) - 预制体实例指示器和操作菜单 ## 编辑器 UX 改进 - SceneHierarchy 快捷键: F2 重命名, Ctrl+D 复制, ↑↓ 导航 - 支持双击实体名称内联编辑 - 删除实体时显示子节点数量警告 - 右键菜单添加重命名/复制选项及快捷键提示 - 布局持久化和重置功能 ## Bug 修复 - 修复 editor-runtime 组件类重复导致的 TransformComponent 不识别问题 - 修复 .prefab-name 样式覆盖导致预制体工具栏文字不可见 - 修复 Inspector 资源字段高度不正确问题 * feat(editor): 改进编辑器 UX 交互体验 - ContentBrowser: 加载动画 spinner、搜索高亮、改进空状态设计 - SceneHierarchy: 选中项自动滚动到视图、搜索清除按钮 - PropertyInspector: 输入框本地状态管理、Enter/Escape 键处理 - EntityInspector: 组件折叠状态持久化、属性搜索清除按钮 - Viewport: 变换操作实时数值显示 - 国际化: 添加相关文本 (en/zh) * fix(build): 修复 Web 构建资产加载和编辑器 UX 改进 构建系统修复: - 修复 asset-catalog.json 字段名不匹配 (entries vs assets) - 修复 BrowserFileSystemService 支持两种目录格式 - 修复 bundle 策略检测逻辑 (空对象判断) - 修复 module.json 中 assetExtensions 声明和类型推断 行为树修复: - 修复 BehaviorTreeExecutionSystem 使用 loadAsset 替代 loadAssetByPath - 修复 BehaviorTreeAssetType 常量与 module.json 类型名一致 (behavior-tree) 编辑器 UX 改进: - 构建完成对话框添加"打开文件夹"按钮 - 构建完成对话框样式优化 (圆形图标背景、按钮布局) - SceneHierarchy 响应式布局 (窄窗口自动隐藏 Type 列) - SceneHierarchy 隐藏滚动条 错误追踪: - 添加全局错误处理器写入日志文件 (%TEMP%/esengine-editor-crash.log) - 添加 append_to_log Tauri 命令 * feat(render): 修复 UI 渲染和点击特效系统 ## UI 渲染修复 - 修复 GUID 验证 bug,使用统一的 isValidGUID() 函数 - 修复 UI 渲染顺序随机问题,Rust 端使用 IndexMap 替代 HashMap - Web 运行时添加 assetPathResolver 支持 GUID 解析 - UIInteractableComponent.blockEvents 默认值改为 false ## 点击特效系统 - 新增 ClickFxComponent 和 ClickFxSystem - 支持在点击位置播放粒子效果 - 支持多种触发模式和粒子轮换 ## Camera 系统重构 - CameraSystem 从 ecs-engine-bindgen 移至 camera 包 - 新增 CameraManager 统一管理相机 ## 编辑器改进 - 改进属性面板 UI 交互 - 粒子编辑器面板优化 - Transform 命令系统 * feat(render): 实现 Sorting Layer 系统和 Overlay 渲染层 - 新增 SortingLayerManager 管理排序层级 (Background, Default, Foreground, UI, Overlay) - 实现 ISortable 接口,统一 Sprite、UI、Particle 的排序属性 - 修复粒子 Overlay 层被 UI 遮挡问题:添加独立的 Overlay Pass 在 UI 之后渲染 - 更新粒子资产格式:从 sortingOrder 改为 sortingLayer + orderInLayer - 更新粒子编辑器面板支持新的排序属性 - 优化 UI 渲染系统使用新的排序层级 * feat(ci): 集成 SignPath 代码签名服务 - 添加 SignPath 自动签名工作流(Windows) - 配置 release-editor.yml 支持代码签名 - 将构建改为草稿模式,等待签名完成后发布 - 添加证书文件到 .gitignore 防止泄露 * fix(asset): 修复 Web 构建资产路径解析和全局单例移除 ## 资产路径修复 - 修复 Tauri 本地服务器 `/asset?path=...` 路径解析,正确与 root 目录连接 - BrowserPathResolver 支持两种模式: - 'proxy': 使用 /asset?path=... 格式(编辑器 Run in Browser) - 'direct': 使用直接路径 /assets/path.png(独立 Web 构建) - BrowserRuntime 使用 'direct' 模式,无需 Tauri 代理 ## 架构改进 - 移除全局单例 - 移除 globalAssetManager 导出,改用 AssetManagerToken 依赖注入 - 移除 globalPathResolver 导出,改用 PathResolutionService - 移除 globalPathResolutionService 导出 - ParticleUpdateSystem/ClickFxSystem 通过 setAssetManager() 注入依赖 - EngineService 使用 new AssetManager() 替代全局实例 ## 新增服务 - PathResolutionService: 统一路径解析接口 - RuntimeModeService: 运行时模式查询服务 - SerializationContext: EntityRef 序列化上下文 ## 其他改进 - 完善 ServiceToken 注释说明本地定义的意图 - 导出 BrowserPathResolveMode 类型 * fix(build): 添加 world-streaming composite 设置修复类型检查 * fix(build): 移除 world-streaming 引用避免 composite 冲突 * fix(build): 将 const enum 改为 enum 兼容 isolatedModules * fix(build): 添加缺失的 IAssetManager 导入
515 lines
18 KiB
TypeScript
515 lines
18 KiB
TypeScript
/**
|
|
* Build Settings Store - 构建设置状态管理
|
|
* Build Settings State Management
|
|
*
|
|
* 使用 Zustand 替代 BuildSettingsPanel 中的大量 useEffect 和 useState
|
|
* Using Zustand to replace numerous useEffect and useState in BuildSettingsPanel
|
|
*/
|
|
|
|
import { create } from 'zustand';
|
|
import { subscribeWithSelector } from 'zustand/middleware';
|
|
import type {
|
|
BuildService,
|
|
BuildProgress,
|
|
BuildConfig,
|
|
WebBuildConfig,
|
|
WeChatBuildConfig,
|
|
ProjectService,
|
|
BuildSettingsConfig
|
|
} from '@esengine/editor-core';
|
|
import { BuildPlatform, BuildStatus } from '@esengine/editor-core';
|
|
import { EngineService } from '../services/EngineService';
|
|
|
|
// ============= Types =============
|
|
|
|
export type PlatformType =
|
|
| 'windows'
|
|
| 'macos'
|
|
| 'linux'
|
|
| 'android'
|
|
| 'ios'
|
|
| 'web'
|
|
| 'wechat-minigame';
|
|
|
|
export interface BuildProfile {
|
|
id: string;
|
|
name: string;
|
|
platform: PlatformType;
|
|
isActive?: boolean;
|
|
}
|
|
|
|
export interface SceneEntry {
|
|
path: string;
|
|
enabled: boolean;
|
|
}
|
|
|
|
export interface BuildSettings {
|
|
scenes: SceneEntry[];
|
|
scriptingDefines: string[];
|
|
companyName: string;
|
|
productName: string;
|
|
version: string;
|
|
developmentBuild: boolean;
|
|
sourceMap: boolean;
|
|
compressionMethod: 'Default' | 'LZ4' | 'LZ4HC';
|
|
buildMode: 'split-bundles' | 'single-bundle' | 'single-file';
|
|
}
|
|
|
|
export interface BuildResult {
|
|
success: boolean;
|
|
outputPath: string;
|
|
duration: number;
|
|
warnings: string[];
|
|
error?: string;
|
|
}
|
|
|
|
interface BuildSettingsState {
|
|
// 配置状态 | Profile state
|
|
profiles: BuildProfile[];
|
|
selectedPlatform: PlatformType;
|
|
selectedProfile: BuildProfile | null;
|
|
settings: BuildSettings;
|
|
|
|
// UI 状态 | UI state
|
|
expandedSections: Record<string, boolean>;
|
|
|
|
// 构建状态 | Build state
|
|
isBuilding: boolean;
|
|
buildProgress: BuildProgress | null;
|
|
buildResult: BuildResult | null;
|
|
showBuildProgress: boolean;
|
|
|
|
// 服务引用 | Service references
|
|
_buildService: BuildService | null;
|
|
_projectService: ProjectService | null;
|
|
_projectPath: string | null;
|
|
|
|
// 内部状态 | Internal state
|
|
_initialized: boolean;
|
|
_saveTimeout: NodeJS.Timeout | null;
|
|
_progressInterval: NodeJS.Timeout | null;
|
|
}
|
|
|
|
interface BuildSettingsActions {
|
|
// 初始化 | Initialization
|
|
initialize: (params: {
|
|
projectPath: string;
|
|
buildService?: BuildService;
|
|
projectService?: ProjectService;
|
|
availableScenes?: string[];
|
|
}) => void;
|
|
cleanup: () => void;
|
|
|
|
// 配置操作 | Profile actions
|
|
setSelectedPlatform: (platform: PlatformType) => void;
|
|
setSelectedProfile: (profile: BuildProfile | null) => void;
|
|
addProfile: () => void;
|
|
|
|
// 设置操作 | Settings actions
|
|
updateSettings: (partial: Partial<BuildSettings>) => void;
|
|
setSceneEnabled: (index: number, enabled: boolean) => void;
|
|
addScene: (path: string) => void;
|
|
addDefine: (define: string) => void;
|
|
removeDefine: (index: number) => void;
|
|
|
|
// UI 操作 | UI actions
|
|
toggleSection: (section: string) => void;
|
|
|
|
// 构建操作 | Build actions
|
|
startBuild: () => Promise<void>;
|
|
cancelBuild: () => void;
|
|
closeBuildProgress: () => void;
|
|
}
|
|
|
|
export type BuildSettingsStore = BuildSettingsState & BuildSettingsActions;
|
|
|
|
// ============= Constants =============
|
|
|
|
const DEFAULT_SETTINGS: BuildSettings = {
|
|
scenes: [],
|
|
scriptingDefines: [],
|
|
companyName: 'DefaultCompany',
|
|
productName: 'MyGame',
|
|
version: '0.1.0',
|
|
developmentBuild: false,
|
|
sourceMap: false,
|
|
compressionMethod: 'Default',
|
|
buildMode: 'split-bundles',
|
|
};
|
|
|
|
const DEFAULT_PROFILES: BuildProfile[] = [
|
|
{ id: 'web-dev', name: 'Web - Development', platform: 'web', isActive: true },
|
|
{ id: 'web-prod', name: 'Web - Production', platform: 'web' },
|
|
{ id: 'wechat', name: 'WeChat Mini Game', platform: 'wechat-minigame' },
|
|
];
|
|
|
|
// ============= Helper Functions =============
|
|
|
|
const getPlatformEnum = (platformType: PlatformType): BuildPlatform => {
|
|
const platformMap: Record<PlatformType, BuildPlatform> = {
|
|
'web': BuildPlatform.Web,
|
|
'wechat-minigame': BuildPlatform.WeChatMiniGame,
|
|
'windows': BuildPlatform.Desktop,
|
|
'macos': BuildPlatform.Desktop,
|
|
'linux': BuildPlatform.Desktop,
|
|
'android': BuildPlatform.Android,
|
|
'ios': BuildPlatform.iOS
|
|
};
|
|
return platformMap[platformType];
|
|
};
|
|
|
|
// ============= Store =============
|
|
|
|
export const useBuildSettingsStore = create<BuildSettingsStore>()(
|
|
subscribeWithSelector((set, get) => ({
|
|
// 初始状态 | Initial state
|
|
profiles: DEFAULT_PROFILES,
|
|
selectedPlatform: 'web',
|
|
selectedProfile: DEFAULT_PROFILES[0] ?? null,
|
|
settings: DEFAULT_SETTINGS,
|
|
expandedSections: {
|
|
sceneList: true,
|
|
scriptingDefines: true,
|
|
platformSettings: true,
|
|
playerSettings: true,
|
|
},
|
|
isBuilding: false,
|
|
buildProgress: null,
|
|
buildResult: null,
|
|
showBuildProgress: false,
|
|
_buildService: null,
|
|
_projectService: null,
|
|
_projectPath: null,
|
|
_initialized: false,
|
|
_saveTimeout: null,
|
|
_progressInterval: null,
|
|
|
|
// ===== 初始化 | Initialization =====
|
|
initialize: ({ projectPath, buildService, projectService, availableScenes }) => {
|
|
const state = get();
|
|
if (state._initialized) return;
|
|
|
|
set({
|
|
_buildService: buildService || null,
|
|
_projectService: projectService || null,
|
|
_projectPath: projectPath,
|
|
_initialized: true,
|
|
});
|
|
|
|
// 从 projectService 加载已保存的设置
|
|
// Load saved settings from projectService
|
|
if (projectService) {
|
|
const savedSettings = projectService.getBuildSettings();
|
|
if (savedSettings) {
|
|
set(prev => ({
|
|
settings: {
|
|
...prev.settings,
|
|
scriptingDefines: savedSettings.scriptingDefines || [],
|
|
companyName: savedSettings.companyName || prev.settings.companyName,
|
|
productName: savedSettings.productName || prev.settings.productName,
|
|
version: savedSettings.version || prev.settings.version,
|
|
developmentBuild: savedSettings.developmentBuild ?? prev.settings.developmentBuild,
|
|
sourceMap: savedSettings.sourceMap ?? prev.settings.sourceMap,
|
|
compressionMethod: savedSettings.compressionMethod || prev.settings.compressionMethod,
|
|
buildMode: savedSettings.buildMode || prev.settings.buildMode
|
|
}
|
|
}));
|
|
}
|
|
|
|
// 初始化场景列表
|
|
// Initialize scene list
|
|
if (availableScenes && availableScenes.length > 0) {
|
|
const savedScenes = savedSettings?.scenes || [];
|
|
set(prev => ({
|
|
settings: {
|
|
...prev.settings,
|
|
scenes: availableScenes.map(path => ({
|
|
path,
|
|
enabled: savedScenes.length > 0 ? savedScenes.includes(path) : true
|
|
}))
|
|
}
|
|
}));
|
|
}
|
|
}
|
|
},
|
|
|
|
cleanup: () => {
|
|
const state = get();
|
|
|
|
// 清理定时器 | Clear timers
|
|
if (state._saveTimeout) {
|
|
clearTimeout(state._saveTimeout);
|
|
}
|
|
if (state._progressInterval) {
|
|
clearInterval(state._progressInterval);
|
|
}
|
|
|
|
set({
|
|
_buildService: null,
|
|
_projectService: null,
|
|
_projectPath: null,
|
|
_initialized: false,
|
|
_saveTimeout: null,
|
|
_progressInterval: null,
|
|
isBuilding: false,
|
|
buildProgress: null,
|
|
buildResult: null,
|
|
showBuildProgress: false,
|
|
});
|
|
},
|
|
|
|
// ===== 配置操作 | Profile actions =====
|
|
setSelectedPlatform: (platform) => {
|
|
const { profiles } = get();
|
|
const profile = profiles.find(p => p.platform === platform);
|
|
set({
|
|
selectedPlatform: platform,
|
|
selectedProfile: profile || null
|
|
});
|
|
},
|
|
|
|
setSelectedProfile: (profile) => {
|
|
set({
|
|
selectedProfile: profile,
|
|
selectedPlatform: profile?.platform || get().selectedPlatform
|
|
});
|
|
},
|
|
|
|
addProfile: () => {
|
|
const { selectedPlatform, profiles } = get();
|
|
const newProfile: BuildProfile = {
|
|
id: `profile-${Date.now()}`,
|
|
name: `${selectedPlatform} - New Profile`,
|
|
platform: selectedPlatform,
|
|
};
|
|
set({
|
|
profiles: [...profiles, newProfile],
|
|
selectedProfile: newProfile
|
|
});
|
|
},
|
|
|
|
// ===== 设置操作 | Settings actions =====
|
|
updateSettings: (partial) => {
|
|
set(prev => ({
|
|
settings: { ...prev.settings, ...partial }
|
|
}));
|
|
|
|
// 防抖保存 | Debounced save
|
|
const state = get();
|
|
if (state._saveTimeout) {
|
|
clearTimeout(state._saveTimeout);
|
|
}
|
|
|
|
const timeout = setTimeout(() => {
|
|
const { _projectService, settings } = get();
|
|
if (_projectService) {
|
|
const configToSave: BuildSettingsConfig = {
|
|
scenes: settings.scenes.filter(s => s.enabled).map(s => s.path),
|
|
scriptingDefines: settings.scriptingDefines,
|
|
companyName: settings.companyName,
|
|
productName: settings.productName,
|
|
version: settings.version,
|
|
developmentBuild: settings.developmentBuild,
|
|
sourceMap: settings.sourceMap,
|
|
compressionMethod: settings.compressionMethod,
|
|
buildMode: settings.buildMode
|
|
};
|
|
_projectService.updateBuildSettings(configToSave);
|
|
}
|
|
}, 500);
|
|
|
|
set({ _saveTimeout: timeout });
|
|
},
|
|
|
|
setSceneEnabled: (index, enabled) => {
|
|
set(prev => ({
|
|
settings: {
|
|
...prev.settings,
|
|
scenes: prev.settings.scenes.map((s, i) =>
|
|
i === index ? { ...s, enabled } : s
|
|
)
|
|
}
|
|
}));
|
|
// 触发保存 | Trigger save
|
|
get().updateSettings({});
|
|
},
|
|
|
|
addScene: (path) => {
|
|
const { settings } = get();
|
|
if (settings.scenes.some(s => s.path === path)) return;
|
|
|
|
set(prev => ({
|
|
settings: {
|
|
...prev.settings,
|
|
scenes: [...prev.settings.scenes, { path, enabled: true }]
|
|
}
|
|
}));
|
|
get().updateSettings({});
|
|
},
|
|
|
|
addDefine: (define) => {
|
|
set(prev => ({
|
|
settings: {
|
|
...prev.settings,
|
|
scriptingDefines: [...prev.settings.scriptingDefines, define]
|
|
}
|
|
}));
|
|
get().updateSettings({});
|
|
},
|
|
|
|
removeDefine: (index) => {
|
|
set(prev => ({
|
|
settings: {
|
|
...prev.settings,
|
|
scriptingDefines: prev.settings.scriptingDefines.filter((_, i) => i !== index)
|
|
}
|
|
}));
|
|
get().updateSettings({});
|
|
},
|
|
|
|
// ===== UI 操作 | UI actions =====
|
|
toggleSection: (section) => {
|
|
set(prev => ({
|
|
expandedSections: {
|
|
...prev.expandedSections,
|
|
[section]: !prev.expandedSections[section]
|
|
}
|
|
}));
|
|
},
|
|
|
|
// ===== 构建操作 | Build actions =====
|
|
startBuild: async () => {
|
|
const { selectedProfile, settings, _buildService, _projectPath } = get();
|
|
|
|
if (!selectedProfile || !_projectPath || !_buildService) {
|
|
console.warn('Cannot start build: missing profile, path, or service');
|
|
return;
|
|
}
|
|
|
|
set({
|
|
isBuilding: true,
|
|
buildProgress: null,
|
|
buildResult: null,
|
|
showBuildProgress: true,
|
|
});
|
|
|
|
// 启动进度轮询(但不用 setInterval,用 buildService 的回调)
|
|
// Start progress polling (but use buildService callback, not setInterval)
|
|
|
|
try {
|
|
const platform = getPlatformEnum(selectedProfile.platform);
|
|
const baseConfig = {
|
|
platform,
|
|
outputPath: `${_projectPath}/build/${selectedProfile.platform}`,
|
|
isRelease: !settings.developmentBuild,
|
|
sourceMap: settings.sourceMap,
|
|
scenes: settings.scenes.filter(s => s.enabled).map(s => s.path)
|
|
};
|
|
|
|
let buildConfig: BuildConfig;
|
|
if (platform === BuildPlatform.Web) {
|
|
// 从 AssetLoaderFactory 获取插件注册的扩展名
|
|
// Get plugin-registered extensions from AssetLoaderFactory
|
|
let assetExtensions: string[] | undefined;
|
|
let assetTypeMap: Record<string, string> | undefined;
|
|
|
|
try {
|
|
const assetManager = EngineService.getInstance().getAssetManager();
|
|
if (assetManager) {
|
|
const loaderFactory = assetManager.getLoaderFactory();
|
|
assetExtensions = loaderFactory.getAllSupportedExtensions();
|
|
assetTypeMap = loaderFactory.getExtensionTypeMap();
|
|
}
|
|
} catch (e) {
|
|
console.warn('Failed to get asset extensions from loader factory:', e);
|
|
}
|
|
|
|
const webConfig: WebBuildConfig = {
|
|
...baseConfig,
|
|
platform: BuildPlatform.Web,
|
|
buildMode: settings.buildMode,
|
|
generateHtml: true,
|
|
minify: !settings.developmentBuild,
|
|
generateAssetCatalog: true,
|
|
assetLoadingStrategy: 'on-demand',
|
|
assetExtensions,
|
|
assetTypeMap
|
|
};
|
|
buildConfig = webConfig;
|
|
} else if (platform === BuildPlatform.WeChatMiniGame) {
|
|
const wechatConfig: WeChatBuildConfig = {
|
|
...baseConfig,
|
|
platform: BuildPlatform.WeChatMiniGame,
|
|
appId: '',
|
|
useSubpackages: false,
|
|
mainPackageLimit: 4096,
|
|
usePlugins: false
|
|
};
|
|
buildConfig = wechatConfig;
|
|
} else {
|
|
buildConfig = baseConfig;
|
|
}
|
|
|
|
// 使用回调更新进度,而不是轮询
|
|
// Use callback to update progress, not polling
|
|
const result = await _buildService.build(buildConfig, (progress) => {
|
|
set({ buildProgress: progress });
|
|
});
|
|
|
|
set({
|
|
buildResult: {
|
|
success: result.success,
|
|
outputPath: result.outputPath,
|
|
duration: result.duration,
|
|
warnings: result.warnings,
|
|
error: result.error
|
|
}
|
|
});
|
|
} catch (error) {
|
|
console.error('Build failed:', error);
|
|
set({
|
|
buildResult: {
|
|
success: false,
|
|
outputPath: '',
|
|
duration: 0,
|
|
warnings: [],
|
|
error: error instanceof Error ? error.message : String(error)
|
|
}
|
|
});
|
|
} finally {
|
|
set({ isBuilding: false });
|
|
}
|
|
},
|
|
|
|
cancelBuild: () => {
|
|
const { _buildService } = get();
|
|
if (_buildService) {
|
|
_buildService.cancelBuild();
|
|
}
|
|
},
|
|
|
|
closeBuildProgress: () => {
|
|
const { isBuilding } = get();
|
|
if (!isBuilding) {
|
|
set({
|
|
showBuildProgress: false,
|
|
buildProgress: null,
|
|
buildResult: null,
|
|
});
|
|
}
|
|
},
|
|
}))
|
|
);
|
|
|
|
// ============= Selectors =============
|
|
|
|
/** 获取当前平台的配置列表 | Get profiles for current platform */
|
|
export const selectProfilesForPlatform = (state: BuildSettingsStore): BuildProfile[] => {
|
|
return state.profiles.filter(p => p.platform === state.selectedPlatform);
|
|
};
|
|
|
|
/** 获取启用的场景列表 | Get enabled scenes */
|
|
export const selectEnabledScenes = (state: BuildSettingsStore): string[] => {
|
|
return state.settings.scenes.filter(s => s.enabled).map(s => s.path);
|
|
};
|