feat: 添加跨平台运行时、资产系统和UI适配功能 (#256)
* feat(platform-common): 添加WASM加载器和环境检测API * feat(rapier2d): 新增Rapier2D WASM绑定包 * feat(physics-rapier2d): 添加跨平台WASM加载器 * feat(asset-system): 添加运行时资产目录和bundle格式 * feat(asset-system-editor): 新增编辑器资产管理包 * feat(editor-core): 添加构建系统和模块管理 * feat(editor-app): 重构浏览器预览使用import maps * feat(platform-web): 添加BrowserRuntime和资产读取 * feat(engine): 添加材质系统和着色器管理 * feat(material): 新增材质系统和着色器编辑器 * feat(tilemap): 增强tilemap编辑器和动画系统 * feat(modules): 添加module.json配置 * feat(core): 添加module.json和类型定义更新 * chore: 更新依赖和构建配置 * refactor(plugins): 更新插件模板使用ModuleManifest * chore: 添加第三方依赖库 * chore: 移除BehaviourTree-ai和ecs-astar子模块 * docs: 更新README和文档主题样式 * fix: 修复Rust文档测试和添加rapier2d WASM绑定 * fix(tilemap-editor): 修复画布高DPI屏幕分辨率适配问题 * feat(ui): 添加UI屏幕适配系统(CanvasScaler/SafeArea) * fix(ecs-engine-bindgen): 添加缺失的ecs-framework-math依赖 * fix: 添加缺失的包依赖修复CI构建 * fix: 修复CodeQL检测到的代码问题 * fix: 修复构建错误和缺失依赖 * fix: 修复类型检查错误 * fix(material-system): 修复tsconfig配置支持TypeScript项目引用 * fix(editor-core): 修复Rollup构建配置添加tauri external * fix: 修复CodeQL检测到的代码问题 * fix: 修复CodeQL检测到的代码问题
This commit is contained in:
243
packages/editor-app/src/services/BuildFileSystemService.ts
Normal file
243
packages/editor-app/src/services/BuildFileSystemService.ts
Normal file
@@ -0,0 +1,243 @@
|
||||
/**
|
||||
* Build File System Service.
|
||||
* 构建文件系统服务。
|
||||
*
|
||||
* Provides file operations for build pipelines via Tauri commands.
|
||||
* 通过 Tauri 命令为构建管线提供文件操作。
|
||||
*/
|
||||
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
|
||||
/**
|
||||
* Bundle options.
|
||||
* 打包选项。
|
||||
*/
|
||||
export interface BundleOptions {
|
||||
/** Entry files | 入口文件 */
|
||||
entryPoints: string[];
|
||||
/** Output directory | 输出目录 */
|
||||
outputDir: string;
|
||||
/** Output format (esm or iife) | 输出格式 */
|
||||
format: 'esm' | 'iife';
|
||||
/** Bundle name | 打包名称 */
|
||||
bundleName: string;
|
||||
/** Whether to minify | 是否压缩 */
|
||||
minify: boolean;
|
||||
/** Whether to generate source map | 是否生成 source map */
|
||||
sourceMap: boolean;
|
||||
/** External dependencies | 外部依赖 */
|
||||
external: string[];
|
||||
/** Project root for resolving imports | 项目根目录 */
|
||||
projectRoot: string;
|
||||
/** Define replacements | 宏定义替换 */
|
||||
define?: Record<string, string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bundle result.
|
||||
* 打包结果。
|
||||
*/
|
||||
export interface BundleResult {
|
||||
/** Whether bundling succeeded | 是否打包成功 */
|
||||
success: boolean;
|
||||
/** Output file path | 输出文件路径 */
|
||||
outputFile?: string;
|
||||
/** Output file size in bytes | 输出文件大小 */
|
||||
outputSize?: number;
|
||||
/** Error message if failed | 失败时的错误信息 */
|
||||
error?: string;
|
||||
/** Warnings | 警告 */
|
||||
warnings: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Build File System Service.
|
||||
* 构建文件系统服务。
|
||||
*/
|
||||
export class BuildFileSystemService {
|
||||
/**
|
||||
* Prepare build directory (clean and recreate).
|
||||
* 准备构建目录(清理并重建)。
|
||||
*
|
||||
* @param outputPath - Output directory path | 输出目录路径
|
||||
*/
|
||||
async prepareBuildDirectory(outputPath: string): Promise<void> {
|
||||
await invoke('prepare_build_directory', { outputPath });
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy directory recursively.
|
||||
* 递归复制目录。
|
||||
*
|
||||
* @param src - Source directory | 源目录
|
||||
* @param dst - Destination directory | 目标目录
|
||||
* @param patterns - File patterns to include (e.g. ["*.png", "*.json"]) | 要包含的文件模式
|
||||
* @returns Number of files copied | 复制的文件数量
|
||||
*/
|
||||
async copyDirectory(
|
||||
src: string,
|
||||
dst: string,
|
||||
patterns?: string[]
|
||||
): Promise<number> {
|
||||
return await invoke('copy_directory', { src, dst, patterns });
|
||||
}
|
||||
|
||||
/**
|
||||
* Bundle scripts using esbuild.
|
||||
* 使用 esbuild 打包脚本。
|
||||
*
|
||||
* @param options - Bundle options | 打包选项
|
||||
* @returns Bundle result | 打包结果
|
||||
*/
|
||||
async bundleScripts(options: BundleOptions): Promise<BundleResult> {
|
||||
return await invoke('bundle_scripts', { options });
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate HTML file.
|
||||
* 生成 HTML 文件。
|
||||
*
|
||||
* @param outputPath - Output file path | 输出文件路径
|
||||
* @param title - Page title | 页面标题
|
||||
* @param scripts - Script paths to include | 要包含的脚本路径
|
||||
* @param bodyContent - Custom body content | 自定义 body 内容
|
||||
*/
|
||||
async generateHtml(
|
||||
outputPath: string,
|
||||
title: string,
|
||||
scripts: string[],
|
||||
bodyContent?: string
|
||||
): Promise<void> {
|
||||
await invoke('generate_html', { outputPath, title, scripts, bodyContent });
|
||||
}
|
||||
|
||||
/**
|
||||
* Get file size.
|
||||
* 获取文件大小。
|
||||
*
|
||||
* @param filePath - File path | 文件路径
|
||||
* @returns File size in bytes | 文件大小(字节)
|
||||
*/
|
||||
async getFileSize(filePath: string): Promise<number> {
|
||||
return await invoke('get_file_size', { filePath });
|
||||
}
|
||||
|
||||
/**
|
||||
* Get directory size recursively.
|
||||
* 递归获取目录大小。
|
||||
*
|
||||
* @param dirPath - Directory path | 目录路径
|
||||
* @returns Total size in bytes | 总大小(字节)
|
||||
*/
|
||||
async getDirectorySize(dirPath: string): Promise<number> {
|
||||
return await invoke('get_directory_size', { dirPath });
|
||||
}
|
||||
|
||||
/**
|
||||
* Write JSON file.
|
||||
* 写入 JSON 文件。
|
||||
*
|
||||
* @param filePath - File path | 文件路径
|
||||
* @param content - JSON content as string | JSON 内容字符串
|
||||
*/
|
||||
async writeJsonFile(filePath: string, content: string): Promise<void> {
|
||||
await invoke('write_json_file', { filePath, content });
|
||||
}
|
||||
|
||||
/**
|
||||
* List files by extension.
|
||||
* 按扩展名列出文件。
|
||||
*
|
||||
* @param dirPath - Directory path | 目录路径
|
||||
* @param extensions - File extensions (without dot) | 文件扩展名(不含点)
|
||||
* @param recursive - Whether to search recursively | 是否递归搜索
|
||||
* @returns List of file paths | 文件路径列表
|
||||
*/
|
||||
async listFilesByExtension(
|
||||
dirPath: string,
|
||||
extensions: string[],
|
||||
recursive: boolean = true
|
||||
): Promise<string[]> {
|
||||
return await invoke('list_files_by_extension', { dirPath, extensions, recursive });
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy single file.
|
||||
* 复制单个文件。
|
||||
*
|
||||
* @param src - Source file path | 源文件路径
|
||||
* @param dst - Destination file path | 目标文件路径
|
||||
*/
|
||||
async copyFile(src: string, dst: string): Promise<void> {
|
||||
await invoke('copy_file', { src, dst });
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if path exists.
|
||||
* 检查路径是否存在。
|
||||
*
|
||||
* @param path - Path to check | 要检查的路径
|
||||
* @returns Whether path exists | 路径是否存在
|
||||
*/
|
||||
async pathExists(path: string): Promise<boolean> {
|
||||
return await invoke('path_exists', { path });
|
||||
}
|
||||
|
||||
/**
|
||||
* Read file content.
|
||||
* 读取文件内容。
|
||||
*
|
||||
* @param path - File path | 文件路径
|
||||
* @returns File content | 文件内容
|
||||
*/
|
||||
async readFile(path: string): Promise<string> {
|
||||
return await invoke('read_file_content', { path });
|
||||
}
|
||||
|
||||
/**
|
||||
* Write file content.
|
||||
* 写入文件内容。
|
||||
*
|
||||
* @param path - File path | 文件路径
|
||||
* @param content - Content to write | 要写入的内容
|
||||
*/
|
||||
async writeFile(path: string, content: string): Promise<void> {
|
||||
await invoke('write_file_content', { path, content });
|
||||
}
|
||||
|
||||
/**
|
||||
* Read JSON file.
|
||||
* 读取 JSON 文件。
|
||||
*
|
||||
* @param path - File path | 文件路径
|
||||
* @returns Parsed JSON object | 解析后的 JSON 对象
|
||||
*/
|
||||
async readJson<T>(path: string): Promise<T> {
|
||||
const content = await invoke<string>('read_file_content', { path });
|
||||
return JSON.parse(content) as T;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create directory.
|
||||
* 创建目录。
|
||||
*
|
||||
* @param path - Directory path | 目录路径
|
||||
*/
|
||||
async createDirectory(path: string): Promise<void> {
|
||||
await invoke('create_directory', { path });
|
||||
}
|
||||
|
||||
/**
|
||||
* Read binary file as base64.
|
||||
* 读取二进制文件为 base64。
|
||||
*
|
||||
* @param path - File path | 文件路径
|
||||
* @returns Base64 encoded content | Base64 编码的内容
|
||||
*/
|
||||
async readBinaryFileAsBase64(path: string): Promise<string> {
|
||||
return await invoke('read_binary_file_as_base64', { path });
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton instance | 单例实例
|
||||
export const buildFileSystem = new BuildFileSystemService();
|
||||
@@ -27,8 +27,10 @@ import {
|
||||
EditorPlatformAdapter,
|
||||
type GameRuntimeConfig
|
||||
} from '@esengine/runtime-core';
|
||||
import { getMaterialManager } from '@esengine/material-system';
|
||||
import { convertFileSrc } from '@tauri-apps/api/core';
|
||||
import { IdGenerator } from '../utils/idGenerator';
|
||||
import { TauriAssetReader } from './TauriAssetReader';
|
||||
|
||||
/**
|
||||
* Engine service singleton for editor integration.
|
||||
@@ -191,7 +193,7 @@ export class EngineService {
|
||||
|
||||
// 创建系统上下文
|
||||
const context: SystemContext = {
|
||||
core: Core,
|
||||
services: Core.services,
|
||||
engineBridge: this._runtime.bridge,
|
||||
renderSystem: this._runtime.renderSystem,
|
||||
assetManager: this._assetManager,
|
||||
@@ -345,11 +347,25 @@ export class EngineService {
|
||||
try {
|
||||
this._assetManager = new AssetManager();
|
||||
|
||||
// Set up asset reader for Tauri environment.
|
||||
// 为 Tauri 环境设置资产读取器。
|
||||
const assetReader = new TauriAssetReader();
|
||||
this._assetManager.setReader(assetReader);
|
||||
|
||||
// Set project root when project is open.
|
||||
// 当项目打开时设置项目根路径。
|
||||
const projectService = Core.services.tryResolve<ProjectService>(ProjectService);
|
||||
if (projectService && projectService.isProjectOpen()) {
|
||||
const projectInfo = projectService.getCurrentProject();
|
||||
if (projectInfo) {
|
||||
this._assetManager.setProjectRoot(projectInfo.path);
|
||||
}
|
||||
}
|
||||
|
||||
const pathTransformerFn = (path: string) => {
|
||||
if (!path.startsWith('http://') && !path.startsWith('https://') &&
|
||||
!path.startsWith('data:') && !path.startsWith('asset://')) {
|
||||
if (!path.startsWith('/') && !path.match(/^[a-zA-Z]:/)) {
|
||||
const projectService = Core.services.tryResolve<ProjectService>(ProjectService);
|
||||
if (projectService && projectService.isProjectOpen()) {
|
||||
const projectInfo = projectService.getCurrentProject();
|
||||
if (projectInfo) {
|
||||
@@ -386,6 +402,13 @@ export class EngineService {
|
||||
}
|
||||
}
|
||||
|
||||
// Set asset manager for MaterialManager.
|
||||
// 为 MaterialManager 设置 asset manager。
|
||||
const materialManager = getMaterialManager();
|
||||
if (materialManager) {
|
||||
materialManager.setAssetManager(this._assetManager);
|
||||
}
|
||||
|
||||
this._assetSystemInitialized = true;
|
||||
this._initializationError = null;
|
||||
} catch (error) {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import { PluginManager, LocaleService, MessageHub } from '@esengine/editor-core';
|
||||
import type { IPluginLoader, PluginDescriptor } from '@esengine/editor-core';
|
||||
import type { IPluginLoader, ModuleManifest } from '@esengine/editor-core';
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
import { TauriAPI } from '../api/tauri';
|
||||
import { PluginSDKRegistry } from './PluginSDKRegistry';
|
||||
@@ -132,7 +132,7 @@ export class PluginLoader {
|
||||
pluginManager.register(pluginLoader);
|
||||
|
||||
// 8. 初始化编辑器模块(注册面板、文件处理器等)
|
||||
const pluginId = pluginLoader.descriptor.id;
|
||||
const pluginId = pluginLoader.manifest.id;
|
||||
await pluginManager.initializePluginEditor(pluginId, Core.services);
|
||||
|
||||
// 9. 记录已加载
|
||||
@@ -285,7 +285,7 @@ export class PluginLoader {
|
||||
}
|
||||
|
||||
// 新的 IPluginLoader 接口检查
|
||||
if (obj.descriptor && this.isPluginDescriptor(obj.descriptor)) {
|
||||
if (obj.manifest && this.isModuleManifest(obj.manifest)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -293,9 +293,9 @@ export class PluginLoader {
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证对象是否为有效的插件描述符
|
||||
* 验证对象是否为有效的模块清单
|
||||
*/
|
||||
private isPluginDescriptor(obj: any): obj is PluginDescriptor {
|
||||
private isModuleManifest(obj: any): obj is ModuleManifest {
|
||||
return (
|
||||
obj &&
|
||||
typeof obj.id === 'string' &&
|
||||
|
||||
@@ -1,56 +1,32 @@
|
||||
/**
|
||||
* Runtime Module Resolver
|
||||
* 运行时模块解析器
|
||||
*
|
||||
* Resolves runtime module paths based on environment and configuration
|
||||
* 根据环境和配置解析运行时模块路径
|
||||
*
|
||||
* 运行时文件打包在编辑器内,离线可用
|
||||
* Runtime Module Resolver
|
||||
*/
|
||||
|
||||
import { TauriAPI } from '../api/tauri';
|
||||
|
||||
// Sanitize path by removing path traversal sequences and normalizing
|
||||
const sanitizePath = (path: string): string => {
|
||||
// Split by path separators, filter out '..' and empty segments, rejoin
|
||||
const segments = path.split(/[/\\]/).filter((segment) =>
|
||||
segment !== '..' && segment !== '.' && segment !== ''
|
||||
);
|
||||
// Use Windows backslash for consistency
|
||||
return segments.join('\\');
|
||||
};
|
||||
|
||||
// Check if we're in development mode
|
||||
const isDevelopment = (): boolean => {
|
||||
try {
|
||||
// Vite environment variable - this is the most reliable check
|
||||
const viteDev = (import.meta as any).env?.DEV === true;
|
||||
// Also check if MODE is 'development'
|
||||
const viteMode = (import.meta as any).env?.MODE === 'development';
|
||||
return viteDev || viteMode;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export interface RuntimeModule {
|
||||
type: 'javascript' | 'wasm' | 'binary';
|
||||
files: string[];
|
||||
sourcePath: string;
|
||||
}
|
||||
|
||||
export interface RuntimeConfig {
|
||||
runtime: {
|
||||
version: string;
|
||||
modules: Record<string, any>;
|
||||
};
|
||||
/**
|
||||
* 运行时模块清单
|
||||
* Module manifest for runtime modules
|
||||
*/
|
||||
export interface ModuleManifest {
|
||||
id: string;
|
||||
name: string;
|
||||
version: string;
|
||||
dependencies: string[];
|
||||
hasRuntime: boolean;
|
||||
pluginExport?: string;
|
||||
requiresWasm?: boolean;
|
||||
wasmPaths?: string[];
|
||||
runtimeWasmPath?: string;
|
||||
externalDependencies?: string[];
|
||||
}
|
||||
|
||||
export class RuntimeResolver {
|
||||
private static instance: RuntimeResolver;
|
||||
private config: RuntimeConfig | null = null;
|
||||
private baseDir: string = '';
|
||||
private isDev: boolean = false; // Store dev mode state at initialization time
|
||||
private engineModulesPath: string = '';
|
||||
private initialized: boolean = false;
|
||||
|
||||
private constructor() {}
|
||||
|
||||
@@ -62,67 +38,40 @@ export class RuntimeResolver {
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the runtime resolver
|
||||
* 初始化运行时解析器
|
||||
* Initialize the runtime resolver
|
||||
*/
|
||||
async initialize(): Promise<void> {
|
||||
// Load runtime configuration
|
||||
const response = await fetch('/runtime.config.json');
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to load runtime configuration: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
const contentType = response.headers.get('content-type');
|
||||
if (!contentType || !contentType.includes('application/json')) {
|
||||
throw new Error(`Invalid runtime configuration response type: ${contentType}. Expected JSON but received ${await response.text().then(t => t.substring(0, 100))}`);
|
||||
}
|
||||
this.config = await response.json();
|
||||
if (this.initialized) return;
|
||||
|
||||
// 查找 workspace 根目录
|
||||
// 查找工作区根目录 | Find workspace root
|
||||
const currentDir = await TauriAPI.getCurrentDir();
|
||||
const workspaceRoot = await this.findWorkspaceRoot(currentDir);
|
||||
this.baseDir = await this.findWorkspaceRoot(currentDir);
|
||||
|
||||
// 优先使用 workspace 中的开发文件(如果存在)
|
||||
// Prefer workspace dev files if they exist
|
||||
if (await this.hasRuntimeFilesInWorkspace(workspaceRoot)) {
|
||||
this.baseDir = workspaceRoot;
|
||||
this.isDev = true;
|
||||
} else {
|
||||
// 回退到打包的资源目录(生产模式)
|
||||
this.baseDir = await TauriAPI.getAppResourceDir();
|
||||
this.isDev = false;
|
||||
}
|
||||
// 查找引擎模块路径 | Find engine modules path
|
||||
this.engineModulesPath = await this.findEngineModulesPath();
|
||||
|
||||
this.initialized = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if runtime files exist in workspace
|
||||
* 检查 workspace 中是否存在运行时文件
|
||||
*/
|
||||
private async hasRuntimeFilesInWorkspace(workspaceRoot: string): Promise<boolean> {
|
||||
const runtimePath = `${workspaceRoot}\\packages\\platform-web\\dist\\runtime.browser.js`;
|
||||
return await TauriAPI.pathExists(runtimePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find workspace root by looking for package.json or specific markers
|
||||
* 通过查找 package.json 或特定标记来找到工作区根目录
|
||||
* 查找工作区根目录
|
||||
* Find workspace root by looking for workspace markers
|
||||
*/
|
||||
private async findWorkspaceRoot(startPath: string): Promise<string> {
|
||||
let currentPath = startPath;
|
||||
|
||||
// Try to find the workspace root by looking for key files
|
||||
// We'll check up to 3 levels up from current directory
|
||||
for (let i = 0; i < 3; i++) {
|
||||
// Check if we're in src-tauri
|
||||
for (let i = 0; i < 5; i++) {
|
||||
// 检查是否在 src-tauri 目录 | Check if we're in src-tauri
|
||||
if (currentPath.endsWith('src-tauri')) {
|
||||
// Go up two levels to get to workspace root
|
||||
const parts = currentPath.split(/[/\\]/);
|
||||
parts.pop(); // Remove src-tauri
|
||||
parts.pop(); // Remove editor-app
|
||||
parts.pop(); // Remove packages
|
||||
parts.pop();
|
||||
parts.pop();
|
||||
parts.pop();
|
||||
return parts.join('\\');
|
||||
}
|
||||
|
||||
// Check for workspace markers
|
||||
// 检查工作区标记 | Check for workspace markers
|
||||
const workspaceMarkers = [
|
||||
`${currentPath}\\pnpm-workspace.yaml`,
|
||||
`${currentPath}\\packages\\editor-app`,
|
||||
@@ -141,103 +90,336 @@ export class RuntimeResolver {
|
||||
currentPath = parts.join('\\');
|
||||
}
|
||||
|
||||
// Fallback to current directory
|
||||
return startPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get runtime module files
|
||||
* 获取运行时模块文件
|
||||
* Find engine modules path (where compiled modules with module.json are)
|
||||
* 查找引擎模块路径(编译后的模块和 module.json 所在位置)
|
||||
*/
|
||||
async getModuleFiles(moduleName: string): Promise<RuntimeModule> {
|
||||
if (!this.config) {
|
||||
await this.initialize();
|
||||
private async findEngineModulesPath(): Promise<string> {
|
||||
// Try installed editor location first
|
||||
const installedPath = 'C:/Program Files/ESEngine Editor/engine';
|
||||
if (await TauriAPI.pathExists(`${installedPath}/index.json`)) {
|
||||
return installedPath;
|
||||
}
|
||||
|
||||
const moduleConfig = this.config!.runtime.modules[moduleName];
|
||||
if (!moduleConfig) {
|
||||
throw new Error(`Runtime module ${moduleName} not found in configuration`);
|
||||
// Try workspace packages directory (dev mode)
|
||||
const workspacePath = `${this.baseDir}\\packages`;
|
||||
if (await TauriAPI.pathExists(`${workspacePath}\\core\\module.json`)) {
|
||||
return workspacePath;
|
||||
}
|
||||
|
||||
const files: string[] = [];
|
||||
let sourcePath: string;
|
||||
|
||||
if (this.isDev) {
|
||||
// Development mode - use relative paths from workspace root
|
||||
const devPath = moduleConfig.development.path;
|
||||
const sanitizedPath = sanitizePath(devPath);
|
||||
sourcePath = `${this.baseDir}\\packages\\${sanitizedPath}`;
|
||||
|
||||
if (moduleConfig.main) {
|
||||
files.push(`${sourcePath}\\${moduleConfig.main}`);
|
||||
}
|
||||
if (moduleConfig.files) {
|
||||
for (const file of moduleConfig.files) {
|
||||
files.push(`${sourcePath}\\${file}`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Production mode - files are bundled with the app
|
||||
sourcePath = this.baseDir;
|
||||
|
||||
if (moduleConfig.main) {
|
||||
files.push(`${sourcePath}\\${moduleConfig.main}`);
|
||||
}
|
||||
if (moduleConfig.files) {
|
||||
for (const file of moduleConfig.files) {
|
||||
files.push(`${sourcePath}\\${file}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
type: moduleConfig.type,
|
||||
files,
|
||||
sourcePath
|
||||
};
|
||||
return workspacePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare runtime files for browser preview
|
||||
* 为浏览器预览准备运行时文件
|
||||
* Get list of available runtime modules
|
||||
* 获取可用的运行时模块列表
|
||||
*
|
||||
* 开发模式:从本地 workspace 复制
|
||||
* 生产模式:从编辑器内置资源复制
|
||||
* Scans the packages directory for module.json files instead of hardcoding
|
||||
* 扫描 packages 目录查找 module.json 文件,而不是硬编码
|
||||
*/
|
||||
async prepareRuntimeFiles(targetDir: string): Promise<void> {
|
||||
async getAvailableModules(): Promise<ModuleManifest[]> {
|
||||
if (!this.initialized) {
|
||||
await this.initialize();
|
||||
}
|
||||
|
||||
const modules: ModuleManifest[] = [];
|
||||
|
||||
// Try to read index.json if it exists (installed editor)
|
||||
const indexPath = `${this.engineModulesPath}\\index.json`;
|
||||
if (await TauriAPI.pathExists(indexPath)) {
|
||||
try {
|
||||
const indexContent = await TauriAPI.readFileContent(indexPath);
|
||||
const indexData = JSON.parse(indexContent) as { modules: ModuleManifest[] };
|
||||
return indexData.modules.filter(m => m.hasRuntime);
|
||||
} catch (e) {
|
||||
console.warn('[RuntimeResolver] Failed to read index.json:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Scan packages directory for module.json files
|
||||
const packageEntries = await TauriAPI.listDirectory(this.engineModulesPath);
|
||||
for (const entry of packageEntries) {
|
||||
if (!entry.is_dir) continue;
|
||||
|
||||
const manifestPath = `${this.engineModulesPath}\\${entry.name}\\module.json`;
|
||||
if (await TauriAPI.pathExists(manifestPath)) {
|
||||
try {
|
||||
const content = await TauriAPI.readFileContent(manifestPath);
|
||||
const manifest = JSON.parse(content) as ModuleManifest;
|
||||
if (manifest.hasRuntime !== false) {
|
||||
modules.push(manifest);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(`[RuntimeResolver] Failed to read module.json for ${entry.name}:`, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by dependencies
|
||||
return this.sortModulesByDependencies(modules);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort modules by dependencies (topological sort)
|
||||
* 按依赖排序模块(拓扑排序)
|
||||
*/
|
||||
private sortModulesByDependencies(modules: ModuleManifest[]): ModuleManifest[] {
|
||||
const sorted: ModuleManifest[] = [];
|
||||
const visited = new Set<string>();
|
||||
const moduleMap = new Map(modules.map(m => [m.id, m]));
|
||||
|
||||
const visit = (module: ModuleManifest) => {
|
||||
if (visited.has(module.id)) return;
|
||||
visited.add(module.id);
|
||||
for (const depId of (module.dependencies || [])) {
|
||||
const dep = moduleMap.get(depId);
|
||||
if (dep) visit(dep);
|
||||
}
|
||||
sorted.push(module);
|
||||
};
|
||||
|
||||
for (const module of modules) {
|
||||
visit(module);
|
||||
}
|
||||
return sorted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare runtime files for browser preview using ES Modules
|
||||
* 使用 ES 模块为浏览器预览准备运行时文件
|
||||
*
|
||||
* Creates libs/{moduleId}/{moduleId}.js structure matching published builds
|
||||
* 创建与发布构建一致的 libs/{moduleId}/{moduleId}.js 结构
|
||||
*/
|
||||
async prepareRuntimeFiles(targetDir: string): Promise<{ modules: ModuleManifest[], importMap: Record<string, string> }> {
|
||||
if (!this.initialized) {
|
||||
await this.initialize();
|
||||
}
|
||||
|
||||
// Ensure target directory exists
|
||||
const dirExists = await TauriAPI.pathExists(targetDir);
|
||||
if (!dirExists) {
|
||||
if (!await TauriAPI.pathExists(targetDir)) {
|
||||
await TauriAPI.createDirectory(targetDir);
|
||||
}
|
||||
|
||||
// Copy platform-web runtime
|
||||
const platformWeb = await this.getModuleFiles('platform-web');
|
||||
for (const srcFile of platformWeb.files) {
|
||||
const filename = srcFile.split(/[/\\]/).pop() || '';
|
||||
const dstFile = `${targetDir}\\${filename}`;
|
||||
const libsDir = `${targetDir}\\libs`;
|
||||
if (!await TauriAPI.pathExists(libsDir)) {
|
||||
await TauriAPI.createDirectory(libsDir);
|
||||
}
|
||||
|
||||
const srcExists = await TauriAPI.pathExists(srcFile);
|
||||
if (srcExists) {
|
||||
const modules = await this.getAvailableModules();
|
||||
const importMap: Record<string, string> = {};
|
||||
const copiedModules: string[] = [];
|
||||
|
||||
// Copy each module's dist files
|
||||
for (const module of modules) {
|
||||
const moduleDistDir = `${this.engineModulesPath}\\${module.id}\\dist`;
|
||||
const moduleSrcFile = `${moduleDistDir}\\index.mjs`;
|
||||
|
||||
// Check for index.mjs or index.js
|
||||
let srcFile = moduleSrcFile;
|
||||
if (!await TauriAPI.pathExists(srcFile)) {
|
||||
srcFile = `${moduleDistDir}\\index.js`;
|
||||
}
|
||||
|
||||
if (await TauriAPI.pathExists(srcFile)) {
|
||||
const dstModuleDir = `${libsDir}\\${module.id}`;
|
||||
if (!await TauriAPI.pathExists(dstModuleDir)) {
|
||||
await TauriAPI.createDirectory(dstModuleDir);
|
||||
}
|
||||
|
||||
const dstFile = `${dstModuleDir}\\${module.id}.js`;
|
||||
await TauriAPI.copyFile(srcFile, dstFile);
|
||||
} else {
|
||||
throw new Error(`Runtime file not found: ${srcFile}`);
|
||||
|
||||
// Copy all chunk files (code splitting creates chunk-*.js files)
|
||||
// 复制所有 chunk 文件(代码分割会创建 chunk-*.js 文件)
|
||||
await this.copyChunkFiles(moduleDistDir, dstModuleDir);
|
||||
|
||||
// Add to import map
|
||||
importMap[`@esengine/${module.id}`] = `./libs/${module.id}/${module.id}.js`;
|
||||
|
||||
// Also add common aliases
|
||||
if (module.id === 'core') {
|
||||
importMap['@esengine/ecs-framework'] = `./libs/${module.id}/${module.id}.js`;
|
||||
}
|
||||
if (module.id === 'math') {
|
||||
importMap['@esengine/ecs-framework-math'] = `./libs/${module.id}/${module.id}.js`;
|
||||
}
|
||||
|
||||
copiedModules.push(module.id);
|
||||
}
|
||||
}
|
||||
|
||||
// Copy engine WASM files
|
||||
const engine = await this.getModuleFiles('engine');
|
||||
for (const srcFile of engine.files) {
|
||||
const filename = srcFile.split(/[/\\]/).pop() || '';
|
||||
const dstFile = `${targetDir}\\${filename}`;
|
||||
// Copy external dependencies (e.g., rapier2d)
|
||||
await this.copyExternalDependencies(modules, libsDir, importMap);
|
||||
|
||||
const srcExists = await TauriAPI.pathExists(srcFile);
|
||||
if (srcExists) {
|
||||
await TauriAPI.copyFile(srcFile, dstFile);
|
||||
} else {
|
||||
throw new Error(`Engine file not found: ${srcFile}`);
|
||||
// Copy engine WASM files to libs/es-engine/
|
||||
await this.copyEngineWasm(libsDir);
|
||||
|
||||
// Copy module-specific WASM files
|
||||
await this.copyModuleWasm(modules, targetDir);
|
||||
|
||||
console.log(`[RuntimeResolver] Prepared ${copiedModules.length} modules for browser preview`);
|
||||
|
||||
return { modules, importMap };
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy chunk files from dist directory (for code-split modules)
|
||||
* 复制 dist 目录中的 chunk 文件(用于代码分割的模块)
|
||||
*/
|
||||
private async copyChunkFiles(srcDir: string, dstDir: string): Promise<void> {
|
||||
try {
|
||||
const entries = await TauriAPI.listDirectory(srcDir);
|
||||
for (const entry of entries) {
|
||||
// Copy chunk-*.js files and any other .js files (except index.*)
|
||||
if (!entry.is_dir && entry.name.endsWith('.js') && !entry.name.startsWith('index.')) {
|
||||
const srcFile = `${srcDir}\\${entry.name}`;
|
||||
const dstFile = `${dstDir}\\${entry.name}`;
|
||||
await TauriAPI.copyFile(srcFile, dstFile);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore errors - some modules may not have chunk files
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy external dependencies like rapier2d
|
||||
* 复制外部依赖如 rapier2d
|
||||
*/
|
||||
private async copyExternalDependencies(
|
||||
modules: ModuleManifest[],
|
||||
libsDir: string,
|
||||
importMap: Record<string, string>
|
||||
): Promise<void> {
|
||||
const externalDeps = new Set<string>();
|
||||
for (const m of modules) {
|
||||
if (m.externalDependencies) {
|
||||
for (const dep of m.externalDependencies) {
|
||||
externalDeps.add(dep);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const dep of externalDeps) {
|
||||
const depId = dep.startsWith('@esengine/') ? dep.slice(10) : dep.replace(/^@[^/]+\//, '');
|
||||
const srcDistDir = `${this.engineModulesPath}\\${depId}\\dist`;
|
||||
let srcFile = `${srcDistDir}\\index.mjs`;
|
||||
if (!await TauriAPI.pathExists(srcFile)) {
|
||||
srcFile = `${srcDistDir}\\index.js`;
|
||||
}
|
||||
|
||||
if (await TauriAPI.pathExists(srcFile)) {
|
||||
const dstModuleDir = `${libsDir}\\${depId}`;
|
||||
if (!await TauriAPI.pathExists(dstModuleDir)) {
|
||||
await TauriAPI.createDirectory(dstModuleDir);
|
||||
}
|
||||
|
||||
const dstFile = `${dstModuleDir}\\${depId}.js`;
|
||||
await TauriAPI.copyFile(srcFile, dstFile);
|
||||
|
||||
// Copy chunk files for external dependencies too
|
||||
await this.copyChunkFiles(srcDistDir, dstModuleDir);
|
||||
|
||||
importMap[dep] = `./libs/${depId}/${depId}.js`;
|
||||
console.log(`[RuntimeResolver] Copied external dependency: ${depId}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy engine WASM files
|
||||
* 复制引擎 WASM 文件
|
||||
*/
|
||||
private async copyEngineWasm(libsDir: string): Promise<void> {
|
||||
const esEngineDir = `${libsDir}\\es-engine`;
|
||||
if (!await TauriAPI.pathExists(esEngineDir)) {
|
||||
await TauriAPI.createDirectory(esEngineDir);
|
||||
}
|
||||
|
||||
// Try different locations for engine WASM
|
||||
const wasmSearchPaths = [
|
||||
`${this.baseDir}\\packages\\engine\\pkg`,
|
||||
`${this.engineModulesPath}\\..\\..\\engine\\pkg`,
|
||||
'C:/Program Files/ESEngine Editor/wasm'
|
||||
];
|
||||
|
||||
const filesToCopy = ['es_engine_bg.wasm', 'es_engine.js', 'es_engine_bg.js'];
|
||||
|
||||
for (const searchPath of wasmSearchPaths) {
|
||||
if (await TauriAPI.pathExists(searchPath)) {
|
||||
for (const file of filesToCopy) {
|
||||
const srcFile = `${searchPath}\\${file}`;
|
||||
if (await TauriAPI.pathExists(srcFile)) {
|
||||
const dstFile = `${esEngineDir}\\${file}`;
|
||||
await TauriAPI.copyFile(srcFile, dstFile);
|
||||
}
|
||||
}
|
||||
console.log('[RuntimeResolver] Copied engine WASM from:', searchPath);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
console.warn('[RuntimeResolver] Engine WASM files not found');
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy module-specific WASM files (e.g., physics)
|
||||
* 复制模块特定的 WASM 文件(如物理)
|
||||
*/
|
||||
private async copyModuleWasm(modules: ModuleManifest[], targetDir: string): Promise<void> {
|
||||
for (const module of modules) {
|
||||
if (!module.requiresWasm || !module.wasmPaths?.length) continue;
|
||||
|
||||
const runtimePath = module.runtimeWasmPath || `wasm/${module.wasmPaths[0]}`;
|
||||
const dstPath = `${targetDir}\\${runtimePath.replace(/\//g, '\\')}`;
|
||||
const dstDir = dstPath.substring(0, dstPath.lastIndexOf('\\'));
|
||||
|
||||
if (!await TauriAPI.pathExists(dstDir)) {
|
||||
await TauriAPI.createDirectory(dstDir);
|
||||
}
|
||||
|
||||
// Search for the WASM file
|
||||
const wasmPath = module.wasmPaths[0];
|
||||
if (!wasmPath) continue;
|
||||
const wasmFileName = wasmPath.split(/[/\\]/).pop() || wasmPath;
|
||||
|
||||
// Build search paths - check module's own pkg, external deps, and common locations
|
||||
const searchPaths: string[] = [
|
||||
`${this.engineModulesPath}\\${module.id}\\pkg\\${wasmFileName}`,
|
||||
`${this.baseDir}\\packages\\${module.id}\\pkg\\${wasmFileName}`,
|
||||
];
|
||||
|
||||
// Check external dependencies for WASM (e.g., physics-rapier2d uses rapier2d's WASM)
|
||||
if (module.externalDependencies) {
|
||||
for (const dep of module.externalDependencies) {
|
||||
const depId = dep.startsWith('@esengine/') ? dep.slice(10) : dep.replace(/^@[^/]+\//, '');
|
||||
searchPaths.push(`${this.engineModulesPath}\\${depId}\\pkg\\${wasmFileName}`);
|
||||
searchPaths.push(`${this.baseDir}\\packages\\${depId}\\pkg\\${wasmFileName}`);
|
||||
}
|
||||
}
|
||||
|
||||
for (const srcPath of searchPaths) {
|
||||
if (await TauriAPI.pathExists(srcPath)) {
|
||||
await TauriAPI.copyFile(srcPath, dstPath);
|
||||
console.log(`[RuntimeResolver] Copied ${module.id} WASM to ${runtimePath}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate import map for runtime HTML
|
||||
* 生成运行时 HTML 的 import map
|
||||
*/
|
||||
generateImportMapHtml(importMap: Record<string, string>): string {
|
||||
return `<script type="importmap">
|
||||
${JSON.stringify({ imports: importMap }, null, 2).split('\n').join('\n ')}
|
||||
</script>`;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -247,4 +429,12 @@ export class RuntimeResolver {
|
||||
getBaseDir(): string {
|
||||
return this.baseDir;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get engine modules path
|
||||
* 获取引擎模块路径
|
||||
*/
|
||||
getEngineModulesPath(): string {
|
||||
return this.engineModulesPath;
|
||||
}
|
||||
}
|
||||
|
||||
80
packages/editor-app/src/services/TauriAssetReader.ts
Normal file
80
packages/editor-app/src/services/TauriAssetReader.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* Tauri Asset Reader
|
||||
* Tauri 资产读取器
|
||||
*
|
||||
* Implements IAssetReader for Tauri/editor environment.
|
||||
* 为 Tauri/编辑器环境实现 IAssetReader。
|
||||
*/
|
||||
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { convertFileSrc } from '@tauri-apps/api/core';
|
||||
import type { IAssetReader } from '@esengine/asset-system';
|
||||
|
||||
/**
|
||||
* Asset reader implementation for Tauri.
|
||||
* Tauri 的资产读取器实现。
|
||||
*/
|
||||
export class TauriAssetReader implements IAssetReader {
|
||||
/**
|
||||
* Read file as text.
|
||||
* 读取文件为文本。
|
||||
*/
|
||||
async readText(absolutePath: string): Promise<string> {
|
||||
return await invoke<string>('read_file_content', { path: absolutePath });
|
||||
}
|
||||
|
||||
/**
|
||||
* Read file as binary.
|
||||
* 读取文件为二进制。
|
||||
*/
|
||||
async readBinary(absolutePath: string): Promise<ArrayBuffer> {
|
||||
const bytes = await invoke<number[]>('read_binary_file', { filePath: absolutePath });
|
||||
return new Uint8Array(bytes).buffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load image from file.
|
||||
* 从文件加载图片。
|
||||
*/
|
||||
async loadImage(absolutePath: string): Promise<HTMLImageElement> {
|
||||
// Only convert if not already a URL.
|
||||
// 仅当不是 URL 时才转换。
|
||||
let assetUrl = absolutePath;
|
||||
if (!absolutePath.startsWith('http://') &&
|
||||
!absolutePath.startsWith('https://') &&
|
||||
!absolutePath.startsWith('data:') &&
|
||||
!absolutePath.startsWith('asset://')) {
|
||||
assetUrl = convertFileSrc(absolutePath);
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const image = new Image();
|
||||
image.onload = () => resolve(image);
|
||||
image.onerror = () => reject(new Error(`Failed to load image: ${absolutePath}`));
|
||||
image.src = assetUrl;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Load audio from file.
|
||||
* 从文件加载音频。
|
||||
*/
|
||||
async loadAudio(absolutePath: string): Promise<AudioBuffer> {
|
||||
const binary = await this.readBinary(absolutePath);
|
||||
const audioContext = new AudioContext();
|
||||
return await audioContext.decodeAudioData(binary);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if file exists.
|
||||
* 检查文件是否存在。
|
||||
*/
|
||||
async exists(absolutePath: string): Promise<boolean> {
|
||||
try {
|
||||
await invoke('read_file_content', { path: absolutePath });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -51,7 +51,7 @@ export class TauriFileSystemService implements IFileSystem {
|
||||
}
|
||||
|
||||
async scanFiles(basePath: string, pattern: string): Promise<string[]> {
|
||||
return await invoke<string[]>('scan_files', { basePath, pattern });
|
||||
return await invoke<string[]>('scan_directory', { path: basePath, pattern });
|
||||
}
|
||||
|
||||
convertToAssetUrl(filePath: string): string {
|
||||
|
||||
143
packages/editor-app/src/services/TauriModuleFileSystem.ts
Normal file
143
packages/editor-app/src/services/TauriModuleFileSystem.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
/**
|
||||
* Tauri Module File System
|
||||
* Tauri 模块文件系统
|
||||
*
|
||||
* Implements IModuleFileSystem interface for Tauri environment.
|
||||
* 为 Tauri 环境实现 IModuleFileSystem 接口。
|
||||
*
|
||||
* This reads module files via Tauri commands from the local file system.
|
||||
* 通过 Tauri 命令从本地文件系统读取模块文件。
|
||||
*/
|
||||
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import type { IModuleFileSystem } from '@esengine/editor-core';
|
||||
|
||||
/**
|
||||
* Module index structure from Tauri backend.
|
||||
* 来自 Tauri 后端的模块索引结构。
|
||||
*/
|
||||
interface ModuleIndex {
|
||||
version: string;
|
||||
generatedAt: string;
|
||||
modules: Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
displayName: string;
|
||||
hasRuntime: boolean;
|
||||
editorPackage?: string;
|
||||
isCore: boolean;
|
||||
category: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tauri-based module file system for reading module manifests.
|
||||
* 基于 Tauri 的模块文件系统,用于读取模块清单。
|
||||
*/
|
||||
export class TauriModuleFileSystem implements IModuleFileSystem {
|
||||
private _basePath: string = '';
|
||||
private _indexCache: ModuleIndex | null = null;
|
||||
|
||||
/**
|
||||
* Read JSON file via Tauri command.
|
||||
* 通过 Tauri 命令读取 JSON 文件。
|
||||
*/
|
||||
async readJson<T>(path: string): Promise<T> {
|
||||
// Check if reading index.json
|
||||
// 检查是否读取 index.json
|
||||
if (path.endsWith('/index.json') || path === 'index.json') {
|
||||
const index = await invoke<ModuleIndex>('read_engine_modules_index');
|
||||
this._indexCache = index;
|
||||
return index as unknown as T;
|
||||
}
|
||||
|
||||
// Extract module ID from path like "/engine/sprite/module.json"
|
||||
// 从路径中提取模块 ID,如 "/engine/sprite/module.json"
|
||||
const match = path.match(/\/([^/]+)\/module\.json$/);
|
||||
if (match) {
|
||||
const moduleId = match[1];
|
||||
return await invoke<T>('read_module_manifest', { moduleId });
|
||||
}
|
||||
|
||||
throw new Error(`Unsupported path: ${path}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Write JSON file - not supported for engine modules.
|
||||
* 写入 JSON 文件 - 引擎模块不支持。
|
||||
*/
|
||||
async writeJson(_path: string, _data: unknown): Promise<void> {
|
||||
throw new Error('Write operation not supported for engine modules');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if path exists.
|
||||
* 检查路径是否存在。
|
||||
*/
|
||||
async pathExists(path: string): Promise<boolean> {
|
||||
try {
|
||||
// For index.json, try to read it
|
||||
// 对于 index.json,尝试读取它
|
||||
if (path.endsWith('/index.json') || path === 'index.json') {
|
||||
console.log('[TauriModuleFileSystem] Checking index.json via Tauri command...');
|
||||
await invoke('read_engine_modules_index');
|
||||
console.log('[TauriModuleFileSystem] index.json exists');
|
||||
return true;
|
||||
}
|
||||
|
||||
// For module.json, check if module exists in index
|
||||
// 对于 module.json,检查模块是否存在于索引中
|
||||
const match = path.match(/\/([^/]+)\/module\.json$/);
|
||||
if (match) {
|
||||
const moduleId = match[1];
|
||||
// Use cached index if available
|
||||
// 如果有缓存的索引则使用
|
||||
if (this._indexCache) {
|
||||
return this._indexCache.modules.some(m => m.id === moduleId);
|
||||
}
|
||||
// Otherwise try to read the manifest
|
||||
// 否则尝试读取清单
|
||||
try {
|
||||
await invoke('read_module_manifest', { moduleId });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (err) {
|
||||
console.error('[TauriModuleFileSystem] pathExists error:', err);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* List files - not needed for module loading.
|
||||
* 列出文件 - 模块加载不需要。
|
||||
*/
|
||||
async listFiles(_dir: string, _extensions: string[], _recursive?: boolean): Promise<string[]> {
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Read file as text.
|
||||
* 读取文件为文本。
|
||||
*/
|
||||
async readText(path: string): Promise<string> {
|
||||
const json = await this.readJson(path);
|
||||
return JSON.stringify(json);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the base path to engine modules.
|
||||
* 获取引擎模块的基础路径。
|
||||
*/
|
||||
async getBasePath(): Promise<string> {
|
||||
if (!this._basePath) {
|
||||
this._basePath = await invoke<string>('get_engine_modules_base_path');
|
||||
}
|
||||
return this._basePath;
|
||||
}
|
||||
}
|
||||
|
||||
120
packages/editor-app/src/services/ViewportService.ts
Normal file
120
packages/editor-app/src/services/ViewportService.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
/**
|
||||
* Viewport Service Implementation
|
||||
* 视口服务实现
|
||||
*
|
||||
* Implements IViewportService using EngineService.
|
||||
* 使用 EngineService 实现 IViewportService。
|
||||
*/
|
||||
|
||||
import type { IViewportService, ViewportCameraConfig } from '@esengine/editor-core';
|
||||
import { EngineService } from './EngineService';
|
||||
|
||||
/**
|
||||
* ViewportService - Wraps EngineService for IViewportService interface
|
||||
* ViewportService - 为 IViewportService 接口包装 EngineService
|
||||
*/
|
||||
export class ViewportService implements IViewportService {
|
||||
private static _instance: ViewportService | null = null;
|
||||
private _engineService: EngineService;
|
||||
|
||||
private constructor() {
|
||||
this._engineService = EngineService.getInstance();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get singleton instance
|
||||
* 获取单例实例
|
||||
*/
|
||||
static getInstance(): ViewportService {
|
||||
if (!ViewportService._instance) {
|
||||
ViewportService._instance = new ViewportService();
|
||||
}
|
||||
return ViewportService._instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the service is initialized
|
||||
* 检查服务是否已初始化
|
||||
*/
|
||||
isInitialized(): boolean {
|
||||
return this._engineService.isInitialized();
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a viewport with a canvas element
|
||||
* 注册一个视口和画布元素
|
||||
*/
|
||||
registerViewport(viewportId: string, canvasId: string): void {
|
||||
this._engineService.registerViewport(viewportId, canvasId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister a viewport
|
||||
* 注销一个视口
|
||||
*/
|
||||
unregisterViewport(viewportId: string): void {
|
||||
this._engineService.unregisterViewport(viewportId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set camera for a specific viewport
|
||||
* 设置特定视口的相机
|
||||
*/
|
||||
setViewportCamera(viewportId: string, config: ViewportCameraConfig): void {
|
||||
this._engineService.setViewportCamera(viewportId, {
|
||||
x: config.x,
|
||||
y: config.y,
|
||||
zoom: config.zoom,
|
||||
rotation: config.rotation ?? 0
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get camera for a specific viewport
|
||||
* 获取特定视口的相机
|
||||
*/
|
||||
getViewportCamera(viewportId: string): ViewportCameraConfig | null {
|
||||
return this._engineService.getViewportCamera(viewportId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set viewport configuration (grid, gizmos visibility)
|
||||
* 设置视口配置(网格、辅助线可见性)
|
||||
*/
|
||||
setViewportConfig(viewportId: string, showGrid: boolean, showGizmos: boolean): void {
|
||||
this._engineService.setViewportConfig(viewportId, showGrid, showGizmos);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resize a specific viewport
|
||||
* 调整特定视口的大小
|
||||
*/
|
||||
resizeViewport(viewportId: string, width: number, height: number): void {
|
||||
this._engineService.resizeViewport(viewportId, width, height);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render to a specific viewport
|
||||
* 渲染到特定视口
|
||||
*/
|
||||
renderToViewport(viewportId: string): void {
|
||||
this._engineService.renderToViewport(viewportId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a texture and return its ID
|
||||
* 加载纹理并返回其 ID
|
||||
*/
|
||||
async loadTexture(path: string): Promise<number> {
|
||||
return await this._engineService.loadTextureAsset(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispose resources
|
||||
* 释放资源
|
||||
*/
|
||||
dispose(): void {
|
||||
// ViewportService is a lightweight wrapper, no resources to dispose
|
||||
// The underlying EngineService manages its own lifecycle
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user