feat: 预制体系统与架构改进 (#303)
* 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 导入
This commit is contained in:
@@ -0,0 +1,514 @@
|
||||
/**
|
||||
* 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);
|
||||
};
|
||||
Reference in New Issue
Block a user