Feature/render pipeline (#232)
* refactor(engine): 重构2D渲染管线坐标系统 * feat(engine): 完善2D渲染管线和编辑器视口功能 * feat(editor): 实现Viewport变换工具系统 * feat(editor): 优化Inspector渲染性能并修复Gizmo变换工具显示 * feat(editor): 实现Run on Device移动预览功能 * feat(editor): 添加组件属性控制和依赖关系系统 * feat(editor): 实现动画预览功能和优化SpriteAnimator编辑器 * feat(editor): 修复SpriteAnimator动画预览功能并迁移CI到pnpm * feat(editor): 修复SpriteAnimator动画预览并迁移到pnpm * feat(editor): 修复SpriteAnimator动画预览并迁移到pnpm * feat(editor): 修复SpriteAnimator动画预览并迁移到pnpm * feat(editor): 修复SpriteAnimator动画预览并迁移到pnpm * feat(ci): 迁移项目到pnpm并修复CI构建问题 * chore: 迁移CI工作流到pnpm并添加WASM构建支持 * chore: 迁移CI工作流到pnpm并添加WASM构建支持 * chore: 迁移CI工作流到pnpm并添加WASM构建支持 * chore: 迁移CI工作流到pnpm并添加WASM构建支持 * chore: 迁移CI工作流到pnpm并添加WASM构建支持 * chore: 迁移CI工作流到pnpm并添加WASM构建支持 * chore: 移除 network 相关包 * chore: 移除 network 相关包
This commit is contained in:
331
packages/editor-app/src/services/EditorEngineSync.ts
Normal file
331
packages/editor-app/src/services/EditorEngineSync.ts
Normal file
@@ -0,0 +1,331 @@
|
||||
/**
|
||||
* Editor-Engine Sync Service
|
||||
* 编辑器-引擎同步服务
|
||||
*
|
||||
* Synchronizes editor entities to Rust engine for rendering.
|
||||
* 将编辑器实体同步到Rust引擎进行渲染。
|
||||
*/
|
||||
|
||||
import { Entity, Component } from '@esengine/ecs-framework';
|
||||
import { MessageHub, EntityStoreService } from '@esengine/editor-core';
|
||||
import { TransformComponent, SpriteComponent, SpriteAnimatorComponent } from '@esengine/ecs-components';
|
||||
import { EngineService } from './EngineService';
|
||||
|
||||
export class EditorEngineSync {
|
||||
private static instance: EditorEngineSync | null = null;
|
||||
|
||||
private engineService: EngineService;
|
||||
private messageHub: MessageHub | null = null;
|
||||
private entityStore: EntityStoreService | null = null;
|
||||
|
||||
// Track synced entities: editor entity id -> engine entity id
|
||||
private syncedEntities: Map<number, Entity> = new Map();
|
||||
|
||||
// Subscription IDs
|
||||
private subscriptions: Array<() => void> = [];
|
||||
|
||||
private initialized = false;
|
||||
|
||||
private constructor() {
|
||||
this.engineService = EngineService.getInstance();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get singleton instance.
|
||||
* 获取单例实例。
|
||||
*/
|
||||
static getInstance(): EditorEngineSync {
|
||||
if (!EditorEngineSync.instance) {
|
||||
EditorEngineSync.instance = new EditorEngineSync();
|
||||
}
|
||||
return EditorEngineSync.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize sync service.
|
||||
* 初始化同步服务。
|
||||
*/
|
||||
initialize(messageHub: MessageHub, entityStore: EntityStoreService): void {
|
||||
if (this.initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.messageHub = messageHub;
|
||||
this.entityStore = entityStore;
|
||||
|
||||
// Subscribe to entity events
|
||||
this.subscribeToEvents();
|
||||
|
||||
// Sync existing entities
|
||||
this.syncAllEntities();
|
||||
|
||||
this.initialized = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to MessageHub events.
|
||||
* 订阅MessageHub事件。
|
||||
*/
|
||||
private subscribeToEvents(): void {
|
||||
if (!this.messageHub) return;
|
||||
|
||||
// Entity added
|
||||
const unsubAdd = this.messageHub.subscribe('entity:added', (data: { entity: Entity }) => {
|
||||
this.syncEntity(data.entity);
|
||||
});
|
||||
this.subscriptions.push(unsubAdd);
|
||||
|
||||
// Entity removed
|
||||
const unsubRemove = this.messageHub.subscribe('entity:removed', (data: { entity: Entity }) => {
|
||||
this.removeEntityFromEngine(data.entity);
|
||||
});
|
||||
this.subscriptions.push(unsubRemove);
|
||||
|
||||
// Component property changed - need to re-sync entity
|
||||
const unsubComponent = this.messageHub.subscribe('component:property:changed', (data: { entity: Entity; component: Component; propertyName: string; value: any }) => {
|
||||
this.updateEntityInEngine(data.entity, data.component, data.propertyName, data.value);
|
||||
});
|
||||
this.subscriptions.push(unsubComponent);
|
||||
|
||||
// Component added - sync entity if it has sprite
|
||||
const unsubComponentAdded = this.messageHub.subscribe('component:added', (data: { entity: Entity; component: Component }) => {
|
||||
this.syncEntity(data.entity);
|
||||
});
|
||||
this.subscriptions.push(unsubComponentAdded);
|
||||
|
||||
// Entities cleared
|
||||
const unsubClear = this.messageHub.subscribe('entities:cleared', () => {
|
||||
this.clearAllFromEngine();
|
||||
});
|
||||
this.subscriptions.push(unsubClear);
|
||||
|
||||
// Entity selected - update gizmo display
|
||||
const unsubSelected = this.messageHub.subscribe('entity:selected', (data: { entity: Entity | null }) => {
|
||||
this.updateSelectedEntity(data.entity);
|
||||
});
|
||||
this.subscriptions.push(unsubSelected);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update selected entity for gizmo display.
|
||||
* 更新选中的实体用于Gizmo显示。
|
||||
*/
|
||||
private updateSelectedEntity(entity: Entity | null): void {
|
||||
if (entity) {
|
||||
this.engineService.setSelectedEntityIds([entity.id]);
|
||||
} else {
|
||||
this.engineService.setSelectedEntityIds([]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync all existing entities.
|
||||
* 同步所有现有实体。
|
||||
*/
|
||||
private syncAllEntities(): void {
|
||||
if (!this.entityStore) return;
|
||||
|
||||
const entities = this.entityStore.getAllEntities();
|
||||
for (const entity of entities) {
|
||||
this.syncEntity(entity);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync a single entity to engine.
|
||||
* 将单个实体同步到引擎。
|
||||
*
|
||||
* Note: Texture loading is now handled automatically by EngineRenderSystem
|
||||
* via Rust engine's path-based texture loading.
|
||||
* 注意:纹理加载现在由EngineRenderSystem通过Rust引擎的路径加载自动处理。
|
||||
*/
|
||||
private syncEntity(entity: Entity): void {
|
||||
// Check if entity has sprite component
|
||||
const spriteComponent = entity.getComponent(SpriteComponent);
|
||||
if (!spriteComponent) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Preload animator textures and set first frame
|
||||
// 预加载动画纹理并设置第一帧
|
||||
const animator = entity.getComponent(SpriteAnimatorComponent);
|
||||
if (animator && animator.clips) {
|
||||
const bridge = this.engineService.getBridge();
|
||||
if (bridge) {
|
||||
for (const clip of animator.clips) {
|
||||
for (const frame of clip.frames) {
|
||||
if (frame.texture) {
|
||||
// Trigger texture loading
|
||||
bridge.getOrLoadTextureByPath(frame.texture);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set sprite texture to first frame (static preview in editor)
|
||||
// 设置精灵纹理为第一帧(编辑器中的静态预览)
|
||||
if (animator.clips && animator.clips.length > 0) {
|
||||
const firstClip = animator.clips[0];
|
||||
if (firstClip && firstClip.frames && firstClip.frames.length > 0) {
|
||||
const firstFrame = firstClip.frames[0];
|
||||
if (firstFrame && firstFrame.texture && spriteComponent) {
|
||||
spriteComponent.texture = firstFrame.texture;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Track synced entity
|
||||
this.syncedEntities.set(entity.id, entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove entity from tracking.
|
||||
* 从跟踪中移除实体。
|
||||
*/
|
||||
private removeEntityFromEngine(entity: Entity): void {
|
||||
if (!entity) {
|
||||
return;
|
||||
}
|
||||
// Just remove from tracking, entity destruction is handled by the command
|
||||
this.syncedEntities.delete(entity.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update entity in engine when component changes.
|
||||
* 当组件变化时更新引擎中的实体。
|
||||
*/
|
||||
private updateEntityInEngine(entity: Entity, component: Component, propertyName: string, value: any): void {
|
||||
const engineEntity = this.syncedEntities.get(entity.id);
|
||||
if (!engineEntity) {
|
||||
// Entity not synced yet, try to sync it
|
||||
this.syncEntity(entity);
|
||||
return;
|
||||
}
|
||||
|
||||
// Update based on component type
|
||||
if (component instanceof TransformComponent) {
|
||||
this.updateTransform(engineEntity, component);
|
||||
} else if (component instanceof SpriteComponent) {
|
||||
this.updateSprite(engineEntity, component, propertyName, value);
|
||||
} else if (component instanceof SpriteAnimatorComponent) {
|
||||
this.updateAnimator(engineEntity, component, propertyName);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update animator - preload textures and set initial frame.
|
||||
* 更新动画器 - 预加载纹理并设置初始帧。
|
||||
*/
|
||||
private updateAnimator(entity: Entity, animator: SpriteAnimatorComponent, propertyName: string): void {
|
||||
// In editor mode, only preload textures and show first frame (no animation playback)
|
||||
// 编辑模式下只预加载纹理并显示第一帧(不播放动画)
|
||||
const bridge = this.engineService.getBridge();
|
||||
const sprite = entity.getComponent(SpriteComponent);
|
||||
|
||||
if (bridge && animator.clips) {
|
||||
// Preload all frame textures
|
||||
for (const clip of animator.clips) {
|
||||
for (const frame of clip.frames) {
|
||||
if (frame.texture) {
|
||||
bridge.getOrLoadTextureByPath(frame.texture);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set sprite texture to first frame if available (static preview in editor)
|
||||
// 设置精灵纹理为第一帧(编辑器中的静态预览)
|
||||
if (sprite && animator.clips && animator.clips.length > 0) {
|
||||
const firstClip = animator.clips[0];
|
||||
if (firstClip && firstClip.frames && firstClip.frames.length > 0) {
|
||||
const firstFrame = firstClip.frames[0];
|
||||
if (firstFrame && firstFrame.texture) {
|
||||
sprite.texture = firstFrame.texture;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update transform in engine entity.
|
||||
* 更新引擎实体的变换。
|
||||
*/
|
||||
private updateTransform(engineEntity: Entity, transform: TransformComponent): void {
|
||||
// Get engine transform component (same type as editor)
|
||||
const engineTransform = engineEntity.getComponent(TransformComponent);
|
||||
if (engineTransform) {
|
||||
engineTransform.position = {
|
||||
x: transform.position?.x ?? 0,
|
||||
y: transform.position?.y ?? 0,
|
||||
z: transform.position?.z ?? 0
|
||||
};
|
||||
engineTransform.rotation = {
|
||||
x: transform.rotation?.x ?? 0,
|
||||
y: transform.rotation?.y ?? 0,
|
||||
z: transform.rotation?.z ?? 0
|
||||
};
|
||||
engineTransform.scale = {
|
||||
x: transform.scale?.x ?? 1,
|
||||
y: transform.scale?.y ?? 1,
|
||||
z: transform.scale?.z ?? 1
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update sprite in engine entity.
|
||||
* 更新引擎实体的精灵。
|
||||
*
|
||||
* Note: Texture loading is now handled automatically by EngineRenderSystem.
|
||||
* 注意:纹理加载现在由EngineRenderSystem自动处理。
|
||||
*/
|
||||
private updateSprite(entity: Entity, sprite: SpriteComponent, property: string, value: any): void {
|
||||
// No manual texture loading needed - EngineRenderSystem handles it
|
||||
// 不需要手动加载纹理 - EngineRenderSystem会处理
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all synced entities from tracking.
|
||||
* 清除所有已同步实体的跟踪。
|
||||
*/
|
||||
private clearAllFromEngine(): void {
|
||||
// Just clear tracking, entity destruction is handled elsewhere
|
||||
this.syncedEntities.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if initialized.
|
||||
* 检查是否已初始化。
|
||||
*/
|
||||
isInitialized(): boolean {
|
||||
return this.initialized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get synced entity count.
|
||||
* 获取已同步实体数量。
|
||||
*/
|
||||
getSyncedCount(): number {
|
||||
return this.syncedEntities.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispose sync service.
|
||||
* 释放同步服务。
|
||||
*/
|
||||
dispose(): void {
|
||||
// Unsubscribe from all events
|
||||
for (const unsub of this.subscriptions) {
|
||||
unsub();
|
||||
}
|
||||
this.subscriptions = [];
|
||||
|
||||
// Clear synced entities
|
||||
this.syncedEntities.clear();
|
||||
|
||||
this.initialized = false;
|
||||
}
|
||||
}
|
||||
|
||||
export default EditorEngineSync;
|
||||
@@ -3,20 +3,14 @@
|
||||
* 管理Rust引擎生命周期的服务。
|
||||
*/
|
||||
|
||||
import { EngineBridge, SpriteComponent, EngineRenderSystem, ITransformComponent } from '@esengine/ecs-engine-bindgen';
|
||||
import { Core, Scene, Entity, Component, ECSComponent } from '@esengine/ecs-framework';
|
||||
import { EngineBridge, EngineRenderSystem, CameraConfig } from '@esengine/ecs-engine-bindgen';
|
||||
import { Core, Scene, Entity, SceneSerializer } from '@esengine/ecs-framework';
|
||||
import { TransformComponent, SpriteComponent, SpriteAnimatorSystem, SpriteAnimatorComponent } from '@esengine/ecs-components';
|
||||
import { EntityStoreService, MessageHub } from '@esengine/editor-core';
|
||||
import * as esEngine from '@esengine/engine';
|
||||
|
||||
/**
|
||||
* Transform component for editor entities.
|
||||
* 编辑器实体的变换组件。
|
||||
*/
|
||||
@ECSComponent('Transform')
|
||||
export class TransformComponent extends Component implements ITransformComponent {
|
||||
position = { x: 0, y: 0 };
|
||||
rotation = 0;
|
||||
scale = { x: 1, y: 1 };
|
||||
}
|
||||
import { AssetManager, EngineIntegration, AssetPathResolver, AssetPlatform } from '@esengine/asset-system';
|
||||
import { convertFileSrc } from '@tauri-apps/api/core';
|
||||
import { IdGenerator } from '../utils/idGenerator';
|
||||
|
||||
/**
|
||||
* Engine service singleton for editor integration.
|
||||
@@ -28,10 +22,17 @@ export class EngineService {
|
||||
private bridge: EngineBridge | null = null;
|
||||
private scene: Scene | null = null;
|
||||
private renderSystem: EngineRenderSystem | null = null;
|
||||
private animatorSystem: SpriteAnimatorSystem | null = null;
|
||||
private initialized = false;
|
||||
private running = false;
|
||||
private animationFrameId: number | null = null;
|
||||
private lastTime = 0;
|
||||
private sceneSnapshot: string | null = null;
|
||||
private assetManager: AssetManager | null = null;
|
||||
private engineIntegration: EngineIntegration | null = null;
|
||||
private assetPathResolver: AssetPathResolver | null = null;
|
||||
private assetSystemInitialized = false;
|
||||
private initializationError: Error | null = null;
|
||||
|
||||
private constructor() {}
|
||||
|
||||
@@ -64,29 +65,119 @@ export class EngineService {
|
||||
// Initialize WASM with pre-imported module | 使用预导入模块初始化WASM
|
||||
await this.bridge.initializeWithModule(esEngine);
|
||||
|
||||
// Set path resolver for Tauri asset URLs | 设置Tauri资产URL的路径解析器
|
||||
this.bridge.setPathResolver((path: string) => {
|
||||
// If already a URL, return as-is
|
||||
if (path.startsWith('http://') ||
|
||||
path.startsWith('https://') ||
|
||||
path.startsWith('data:') ||
|
||||
path.startsWith('asset://')) {
|
||||
return path;
|
||||
}
|
||||
// Convert file path to Tauri asset URL
|
||||
return convertFileSrc(path);
|
||||
});
|
||||
|
||||
// Initialize Core if not already | 初始化Core(如果尚未初始化)
|
||||
if (!Core.scene) {
|
||||
Core.create({ debug: false });
|
||||
}
|
||||
|
||||
// Create ECS scene and set it via Core | 通过Core创建并设置ECS场景
|
||||
this.scene = new Scene({ name: 'EditorScene' });
|
||||
// Use existing Core scene or create new one | 使用现有Core场景或创建新的
|
||||
if (Core.scene) {
|
||||
this.scene = Core.scene as Scene;
|
||||
} else {
|
||||
this.scene = new Scene({ name: 'EditorScene' });
|
||||
Core.setScene(this.scene);
|
||||
}
|
||||
|
||||
// Add render system | 添加渲染系统
|
||||
// Add sprite animator system (disabled by default in editor mode)
|
||||
// 添加精灵动画系统(编辑器模式下默认禁用)
|
||||
this.animatorSystem = new SpriteAnimatorSystem();
|
||||
this.animatorSystem.enabled = false;
|
||||
this.scene!.addSystem(this.animatorSystem);
|
||||
|
||||
// Add render system to the scene | 将渲染系统添加到场景
|
||||
this.renderSystem = new EngineRenderSystem(this.bridge, TransformComponent);
|
||||
this.scene.addSystem(this.renderSystem);
|
||||
this.scene!.addSystem(this.renderSystem);
|
||||
|
||||
// Set scene via Core | 通过Core设置场景
|
||||
Core.setScene(this.scene);
|
||||
// Initialize asset system | 初始化资产系统
|
||||
await this.initializeAssetSystem();
|
||||
|
||||
// Start the default world to enable system updates
|
||||
// 启动默认world以启用系统更新
|
||||
const defaultWorld = Core.worldManager.getWorld('__default__');
|
||||
if (defaultWorld && !defaultWorld.isActive) {
|
||||
defaultWorld.start();
|
||||
}
|
||||
|
||||
this.initialized = true;
|
||||
console.log('EngineService initialized | 引擎服务初始化完成');
|
||||
|
||||
// Sync viewport size immediately after initialization
|
||||
// 初始化后立即同步视口尺寸
|
||||
const canvas = document.getElementById(canvasId) as HTMLCanvasElement;
|
||||
if (canvas && canvas.parentElement) {
|
||||
// Get container size in CSS pixels
|
||||
// 获取容器尺寸(CSS像素)
|
||||
const rect = canvas.parentElement.getBoundingClientRect();
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
|
||||
// Canvas internal size uses DPR for sharpness
|
||||
// Canvas内部尺寸使用DPR以保持清晰
|
||||
canvas.width = Math.floor(rect.width * dpr);
|
||||
canvas.height = Math.floor(rect.height * dpr);
|
||||
canvas.style.width = `${rect.width}px`;
|
||||
canvas.style.height = `${rect.height}px`;
|
||||
|
||||
// Camera uses actual canvas pixels for correct rendering
|
||||
// 相机使用实际canvas像素以保证正确渲染
|
||||
this.bridge.resize(canvas.width, canvas.height);
|
||||
}
|
||||
|
||||
// Auto-start render loop for editor preview | 自动启动渲染循环用于编辑器预览
|
||||
this.startRenderLoop();
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize engine | 引擎初始化失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start render loop (editor preview mode).
|
||||
* 启动渲染循环(编辑器预览模式)。
|
||||
*/
|
||||
private startRenderLoop(): void {
|
||||
if (this.animationFrameId !== null) {
|
||||
return;
|
||||
}
|
||||
this.lastTime = performance.now();
|
||||
this.renderLoop();
|
||||
}
|
||||
|
||||
private frameCount = 0;
|
||||
|
||||
/**
|
||||
* Render loop for editor preview (always runs).
|
||||
* 编辑器预览的渲染循环(始终运行)。
|
||||
*/
|
||||
private renderLoop = (): void => {
|
||||
const currentTime = performance.now();
|
||||
const deltaTime = (currentTime - this.lastTime) / 1000;
|
||||
this.lastTime = currentTime;
|
||||
|
||||
this.frameCount++;
|
||||
|
||||
// Update via Core (handles deltaTime internally) | 通过Core更新
|
||||
Core.update(deltaTime);
|
||||
|
||||
// Note: Rendering is handled by EngineRenderSystem.process()
|
||||
// Texture loading is handled automatically via Rust engine's path-based loading
|
||||
// 注意:渲染由 EngineRenderSystem.process() 处理
|
||||
// 纹理加载由Rust引擎的路径加载自动处理
|
||||
|
||||
this.animationFrameId = requestAnimationFrame(this.renderLoop);
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if engine is initialized.
|
||||
* 检查引擎是否已初始化。
|
||||
@@ -114,19 +205,82 @@ export class EngineService {
|
||||
|
||||
this.running = true;
|
||||
this.lastTime = performance.now();
|
||||
|
||||
// Enable animator system and start auto-play animations
|
||||
// 启用动画系统并启动自动播放的动画
|
||||
if (this.animatorSystem) {
|
||||
this.animatorSystem.enabled = true;
|
||||
}
|
||||
this.startAutoPlayAnimations();
|
||||
|
||||
this.gameLoop();
|
||||
}
|
||||
|
||||
/**
|
||||
* Start all auto-play animations.
|
||||
* 启动所有自动播放的动画。
|
||||
*/
|
||||
private startAutoPlayAnimations(): void {
|
||||
if (!this.scene) return;
|
||||
|
||||
const entities = this.scene.entities.findEntitiesWithComponent(SpriteAnimatorComponent);
|
||||
for (const entity of entities) {
|
||||
const animator = entity.getComponent(SpriteAnimatorComponent);
|
||||
if (animator && animator.autoPlay && animator.defaultAnimation) {
|
||||
animator.play();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop all animations and reset to first frame.
|
||||
* 停止所有动画并重置到第一帧。
|
||||
*/
|
||||
private stopAllAnimations(): void {
|
||||
if (!this.scene) return;
|
||||
|
||||
const entities = this.scene.entities.findEntitiesWithComponent(SpriteAnimatorComponent);
|
||||
for (const entity of entities) {
|
||||
const animator = entity.getComponent(SpriteAnimatorComponent);
|
||||
if (animator) {
|
||||
animator.stop();
|
||||
|
||||
// Reset sprite texture to first frame
|
||||
// 重置精灵纹理到第一帧
|
||||
const sprite = entity.getComponent(SpriteComponent);
|
||||
if (sprite && animator.clips && animator.clips.length > 0) {
|
||||
const firstClip = animator.clips[0];
|
||||
if (firstClip && firstClip.frames && firstClip.frames.length > 0) {
|
||||
const firstFrame = firstClip.frames[0];
|
||||
if (firstFrame && firstFrame.texture) {
|
||||
sprite.texture = firstFrame.texture;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the game loop.
|
||||
* 停止游戏循环。
|
||||
*/
|
||||
stop(): void {
|
||||
this.running = false;
|
||||
if (this.animationFrameId !== null) {
|
||||
cancelAnimationFrame(this.animationFrameId);
|
||||
this.animationFrameId = null;
|
||||
|
||||
// Disable animator system and stop all animations
|
||||
// 禁用动画系统并停止所有动画
|
||||
if (this.animatorSystem) {
|
||||
this.animatorSystem.enabled = false;
|
||||
}
|
||||
this.stopAllAnimations();
|
||||
|
||||
// Note: Don't cancel animationFrameId here, as renderLoop should keep running
|
||||
// for editor preview. The renderLoop will continue but gameLoop will stop
|
||||
// because this.running is false.
|
||||
// 注意:这里不要取消 animationFrameId,因为 renderLoop 应该继续运行
|
||||
// 用于编辑器预览。renderLoop 会继续运行,但 gameLoop 会停止
|
||||
// 因为 this.running 是 false。
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -185,6 +339,53 @@ export class EngineService {
|
||||
return entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize asset system
|
||||
* 初始化资产系统
|
||||
*/
|
||||
private async initializeAssetSystem(): Promise<void> {
|
||||
try {
|
||||
// 创建资产管理器 / Create asset manager
|
||||
this.assetManager = new AssetManager();
|
||||
|
||||
// 创建路径解析器 / Create path resolver
|
||||
this.assetPathResolver = new AssetPathResolver({
|
||||
platform: AssetPlatform.Editor,
|
||||
pathTransformer: (path: string) => {
|
||||
// 编辑器平台使用Tauri的convertFileSrc
|
||||
// Use Tauri's convertFileSrc for editor platform
|
||||
if (!path.startsWith('http://') && !path.startsWith('https://') && !path.startsWith('data:')) {
|
||||
return convertFileSrc(path);
|
||||
}
|
||||
return path;
|
||||
}
|
||||
});
|
||||
|
||||
// 创建引擎集成 / Create engine integration
|
||||
if (this.bridge) {
|
||||
this.engineIntegration = new EngineIntegration(this.assetManager, this.bridge);
|
||||
}
|
||||
|
||||
this.assetSystemInitialized = true;
|
||||
this.initializationError = null;
|
||||
} catch (error) {
|
||||
this.assetSystemInitialized = false;
|
||||
this.initializationError = error instanceof Error ? error : new Error(String(error));
|
||||
console.error('Failed to initialize asset system:', error);
|
||||
|
||||
// Notify user of failure
|
||||
const messageHub = Core.services.tryResolve<MessageHub>(MessageHub);
|
||||
if (messageHub) {
|
||||
messageHub.publish('notification:error', {
|
||||
title: 'Asset System Error',
|
||||
message: 'Failed to initialize asset system. Some features may not work properly.'
|
||||
});
|
||||
}
|
||||
|
||||
throw this.initializationError;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load texture.
|
||||
* 加载纹理。
|
||||
@@ -195,6 +396,70 @@ export class EngineService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load texture through asset system
|
||||
* 通过资产系统加载纹理
|
||||
*/
|
||||
async loadTextureAsset(path: string): Promise<number> {
|
||||
// Check if asset system is properly initialized
|
||||
if (!this.assetSystemInitialized || this.initializationError) {
|
||||
console.warn('Asset system not initialized, using fallback texture loading');
|
||||
const textureId = IdGenerator.nextId('texture-fallback');
|
||||
this.loadTexture(textureId, path);
|
||||
return textureId;
|
||||
}
|
||||
|
||||
if (!this.engineIntegration) {
|
||||
// 回退到直接加载 / Fallback to direct loading
|
||||
const textureId = IdGenerator.nextId('texture');
|
||||
this.loadTexture(textureId, path);
|
||||
return textureId;
|
||||
}
|
||||
|
||||
try {
|
||||
return await this.engineIntegration.loadTextureForComponent(path);
|
||||
} catch (error) {
|
||||
console.error('Failed to load texture asset:', error);
|
||||
// Return a valid fallback ID instead of 0
|
||||
const fallbackId = IdGenerator.nextId('texture-fallback');
|
||||
|
||||
// Notify about texture loading failure
|
||||
const messageHub = Core.services.tryResolve<MessageHub>(MessageHub);
|
||||
if (messageHub) {
|
||||
messageHub.publish('notification:warning', {
|
||||
title: 'Texture Loading Failed',
|
||||
message: `Could not load texture: ${path}`
|
||||
});
|
||||
}
|
||||
|
||||
return fallbackId;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get asset manager
|
||||
* 获取资产管理器
|
||||
*/
|
||||
getAssetManager(): AssetManager | null {
|
||||
return this.assetManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get engine integration
|
||||
* 获取引擎集成
|
||||
*/
|
||||
getEngineIntegration(): EngineIntegration | null {
|
||||
return this.engineIntegration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get asset path resolver
|
||||
* 获取资产路径解析器
|
||||
*/
|
||||
getAssetPathResolver(): AssetPathResolver | null {
|
||||
return this.assetPathResolver;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get engine statistics.
|
||||
* 获取引擎统计信息。
|
||||
@@ -220,6 +485,45 @@ export class EngineService {
|
||||
return this.scene;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable animation preview in editor mode.
|
||||
* 在编辑器模式下启用动画预览。
|
||||
*/
|
||||
enableAnimationPreview(): void {
|
||||
if (this.animatorSystem && !this.running) {
|
||||
// Clear entity cache to force re-query when enabled
|
||||
// 清除实体缓存以便启用时强制重新查询
|
||||
this.animatorSystem.clearEntityCache();
|
||||
this.animatorSystem.enabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable animation preview in editor mode.
|
||||
* 在编辑器模式下禁用动画预览。
|
||||
*/
|
||||
disableAnimationPreview(): void {
|
||||
if (this.animatorSystem && !this.running) {
|
||||
this.animatorSystem.enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if animation preview is enabled.
|
||||
* 检查动画预览是否启用。
|
||||
*/
|
||||
isAnimationPreviewEnabled(): boolean {
|
||||
return this.animatorSystem?.enabled ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the engine bridge.
|
||||
* 获取引擎桥接。
|
||||
*/
|
||||
getBridge(): EngineBridge | null {
|
||||
return this.bridge;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resize the engine viewport.
|
||||
* 调整引擎视口大小。
|
||||
@@ -230,6 +534,278 @@ export class EngineService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set camera position, zoom, and rotation.
|
||||
* 设置相机位置、缩放和旋转。
|
||||
*/
|
||||
setCamera(config: CameraConfig): void {
|
||||
if (this.bridge) {
|
||||
this.bridge.setCamera(config);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get camera state.
|
||||
* 获取相机状态。
|
||||
*/
|
||||
getCamera(): CameraConfig {
|
||||
if (this.bridge) {
|
||||
return this.bridge.getCamera();
|
||||
}
|
||||
return { x: 0, y: 0, zoom: 1, rotation: 0 };
|
||||
}
|
||||
|
||||
/**
|
||||
* Set grid visibility.
|
||||
* 设置网格可见性。
|
||||
*/
|
||||
setShowGrid(show: boolean): void {
|
||||
if (this.bridge) {
|
||||
this.bridge.setShowGrid(show);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set clear color (background color).
|
||||
* 设置清除颜色(背景颜色)。
|
||||
*/
|
||||
setClearColor(r: number, g: number, b: number, a: number = 1.0): void {
|
||||
if (this.bridge) {
|
||||
this.bridge.setClearColor(r, g, b, a);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set gizmo visibility.
|
||||
* 设置Gizmo可见性。
|
||||
*/
|
||||
setShowGizmos(show: boolean): void {
|
||||
if (this.renderSystem) {
|
||||
this.renderSystem.setShowGizmos(show);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get gizmo visibility.
|
||||
* 获取Gizmo可见性。
|
||||
*/
|
||||
getShowGizmos(): boolean {
|
||||
return this.renderSystem?.getShowGizmos() ?? true;
|
||||
}
|
||||
|
||||
// ===== Scene Snapshot API =====
|
||||
// ===== 场景快照 API =====
|
||||
|
||||
/**
|
||||
* Save a snapshot of the current scene state.
|
||||
* 保存当前场景状态的快照。
|
||||
*/
|
||||
saveSceneSnapshot(): boolean {
|
||||
if (!this.scene) {
|
||||
console.warn('Cannot save snapshot: no scene available');
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
// Use SceneSerializer from core library
|
||||
this.sceneSnapshot = SceneSerializer.serialize(this.scene, {
|
||||
format: 'json',
|
||||
pretty: false,
|
||||
includeMetadata: false
|
||||
}) as string;
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Failed to save scene snapshot:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore scene state from saved snapshot.
|
||||
* 从保存的快照恢复场景状态。
|
||||
*/
|
||||
restoreSceneSnapshot(): boolean {
|
||||
if (!this.scene || !this.sceneSnapshot) {
|
||||
console.warn('Cannot restore snapshot: no scene or snapshot available');
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
// Use SceneSerializer from core library
|
||||
SceneSerializer.deserialize(this.scene, this.sceneSnapshot, {
|
||||
strategy: 'replace',
|
||||
preserveIds: true
|
||||
});
|
||||
|
||||
// Sync EntityStore with restored scene entities
|
||||
const entityStore = Core.services.tryResolve(EntityStoreService);
|
||||
const messageHub = Core.services.tryResolve(MessageHub);
|
||||
if (entityStore && messageHub) {
|
||||
// Remember selected entity ID before clearing
|
||||
const selectedEntity = entityStore.getSelectedEntity();
|
||||
const selectedId = selectedEntity?.id;
|
||||
|
||||
// Clear old entities from store
|
||||
entityStore.clear();
|
||||
|
||||
// Add restored entities to store
|
||||
for (const entity of this.scene.entities.buffer) {
|
||||
entityStore.addEntity(entity);
|
||||
}
|
||||
|
||||
// Re-select the same entity (now with new reference)
|
||||
if (selectedId !== undefined) {
|
||||
const newEntity = entityStore.getEntity(selectedId);
|
||||
if (newEntity) {
|
||||
entityStore.selectEntity(newEntity);
|
||||
}
|
||||
}
|
||||
|
||||
// Notify UI to refresh
|
||||
messageHub.publish('scene:restored', {});
|
||||
}
|
||||
|
||||
this.sceneSnapshot = null;
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Failed to restore scene snapshot:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a snapshot exists.
|
||||
* 检查是否存在快照。
|
||||
*/
|
||||
hasSnapshot(): boolean {
|
||||
return this.sceneSnapshot !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set selected entity IDs for gizmo display.
|
||||
* 设置选中的实体ID用于Gizmo显示。
|
||||
*/
|
||||
setSelectedEntityIds(ids: number[]): void {
|
||||
if (this.renderSystem) {
|
||||
this.renderSystem.setSelectedEntityIds(ids);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set transform tool mode.
|
||||
* 设置变换工具模式。
|
||||
*/
|
||||
setTransformMode(mode: 'select' | 'move' | 'rotate' | 'scale'): void {
|
||||
if (this.renderSystem) {
|
||||
this.renderSystem.setTransformMode(mode);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get transform tool mode.
|
||||
* 获取变换工具模式。
|
||||
*/
|
||||
getTransformMode(): 'select' | 'move' | 'rotate' | 'scale' {
|
||||
return this.renderSystem?.getTransformMode() ?? 'select';
|
||||
}
|
||||
|
||||
// ===== Multi-viewport API =====
|
||||
// ===== 多视口 API =====
|
||||
|
||||
/**
|
||||
* Register a new viewport.
|
||||
* 注册新视口。
|
||||
*/
|
||||
registerViewport(id: string, canvasId: string): void {
|
||||
if (this.bridge) {
|
||||
this.bridge.registerViewport(id, canvasId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister a viewport.
|
||||
* 注销视口。
|
||||
*/
|
||||
unregisterViewport(id: string): void {
|
||||
if (this.bridge) {
|
||||
this.bridge.unregisterViewport(id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the active viewport.
|
||||
* 设置活动视口。
|
||||
*/
|
||||
setActiveViewport(id: string): boolean {
|
||||
if (this.bridge) {
|
||||
return this.bridge.setActiveViewport(id);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set camera for a specific viewport.
|
||||
* 为特定视口设置相机。
|
||||
*/
|
||||
setViewportCamera(viewportId: string, config: CameraConfig): void {
|
||||
if (this.bridge) {
|
||||
this.bridge.setViewportCamera(viewportId, config);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get camera for a specific viewport.
|
||||
* 获取特定视口的相机。
|
||||
*/
|
||||
getViewportCamera(viewportId: string): CameraConfig | null {
|
||||
if (this.bridge) {
|
||||
return this.bridge.getViewportCamera(viewportId);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set viewport configuration.
|
||||
* 设置视口配置。
|
||||
*/
|
||||
setViewportConfig(viewportId: string, showGrid: boolean, showGizmos: boolean): void {
|
||||
if (this.bridge) {
|
||||
this.bridge.setViewportConfig(viewportId, showGrid, showGizmos);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resize a specific viewport.
|
||||
* 调整特定视口大小。
|
||||
*/
|
||||
resizeViewport(viewportId: string, width: number, height: number): void {
|
||||
if (this.bridge) {
|
||||
this.bridge.resizeViewport(viewportId, width, height);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render to a specific viewport.
|
||||
* 渲染到特定视口。
|
||||
*/
|
||||
renderToViewport(viewportId: string): void {
|
||||
if (this.bridge) {
|
||||
this.bridge.renderToViewport(viewportId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all registered viewport IDs.
|
||||
* 获取所有已注册的视口ID。
|
||||
*/
|
||||
getViewportIds(): string[] {
|
||||
if (this.bridge) {
|
||||
return this.bridge.getViewportIds();
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispose engine resources.
|
||||
* 释放引擎资源。
|
||||
@@ -237,6 +813,19 @@ export class EngineService {
|
||||
dispose(): void {
|
||||
this.stop();
|
||||
|
||||
// Stop render loop | 停止渲染循环
|
||||
if (this.animationFrameId !== null) {
|
||||
cancelAnimationFrame(this.animationFrameId);
|
||||
this.animationFrameId = null;
|
||||
}
|
||||
|
||||
// Dispose asset system | 释放资产系统
|
||||
if (this.assetManager) {
|
||||
this.assetManager.dispose();
|
||||
this.assetManager = null;
|
||||
}
|
||||
this.engineIntegration = null;
|
||||
|
||||
// Scene doesn't have a destroy method, just clear reference
|
||||
// 场景没有destroy方法,只需清除引用
|
||||
this.scene = null;
|
||||
|
||||
@@ -667,7 +667,7 @@ export class GitHubService {
|
||||
}
|
||||
|
||||
// 转换为最终结果,并对版本排序
|
||||
const plugins: PublishedPlugin[] = Array.from(pluginVersionsMap.values()).map(plugin => {
|
||||
const plugins: PublishedPlugin[] = Array.from(pluginVersionsMap.values()).map((plugin) => {
|
||||
// 按版本号降序排序(最新版本在前)
|
||||
const sortedVersions = plugin.versions.sort((a, b) => {
|
||||
const parseVersion = (v: string) => {
|
||||
@@ -714,9 +714,9 @@ export class GitHubService {
|
||||
detailsUrl: run.html_url,
|
||||
output: run.output
|
||||
? {
|
||||
title: run.output.title || '',
|
||||
summary: run.output.summary || ''
|
||||
}
|
||||
title: run.output.title || '',
|
||||
summary: run.output.summary || ''
|
||||
}
|
||||
: undefined
|
||||
}));
|
||||
} catch (error) {
|
||||
@@ -782,8 +782,8 @@ export class GitHubService {
|
||||
|
||||
const files = await this.request<any[]>(`GET /repos/${owner}/${repo}/pulls/${prNumber}/files`);
|
||||
const conflictFiles = files
|
||||
.filter(file => file.status === 'modified' || file.status === 'added' || file.status === 'deleted')
|
||||
.map(file => file.filename);
|
||||
.filter((file) => file.status === 'modified' || file.status === 'added' || file.status === 'deleted')
|
||||
.map((file) => file.filename);
|
||||
|
||||
return {
|
||||
hasConflicts: true,
|
||||
@@ -932,7 +932,7 @@ export class GitHubService {
|
||||
this.scheduleRetryLoadUser();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[GitHubService] Failed to load token:', error);
|
||||
this.notifyUserLoadStateChange(false);
|
||||
|
||||
@@ -22,6 +22,22 @@ export class NotificationService implements INotification {
|
||||
}
|
||||
}
|
||||
|
||||
warning(title: string, message: string, duration: number = 5000): void {
|
||||
this.show(`${title}: ${message}`, 'warning', duration);
|
||||
}
|
||||
|
||||
error(title: string, message: string, duration: number = 5000): void {
|
||||
this.show(`${title}: ${message}`, 'error', duration);
|
||||
}
|
||||
|
||||
info(title: string, message: string, duration: number = 3000): void {
|
||||
this.show(`${title}: ${message}`, 'info', duration);
|
||||
}
|
||||
|
||||
success(title: string, message: string, duration: number = 3000): void {
|
||||
this.show(`${title}: ${message}`, 'success', duration);
|
||||
}
|
||||
|
||||
hide(id: string): void {
|
||||
if (this.hideCallback) {
|
||||
this.hideCallback(id);
|
||||
@@ -29,4 +45,9 @@ export class NotificationService implements INotification {
|
||||
console.warn('[NotificationService] hideCallback not set');
|
||||
}
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.showCallback = undefined;
|
||||
this.hideCallback = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,7 +155,7 @@ export class PluginMarketService {
|
||||
|
||||
try {
|
||||
// 获取指定版本信息
|
||||
const versionInfo = plugin.versions.find(v => v.version === targetVersion);
|
||||
const versionInfo = plugin.versions.find((v) => v.version === targetVersion);
|
||||
if (!versionInfo) {
|
||||
throw new Error(`Version ${targetVersion} not found for plugin ${plugin.name}`);
|
||||
}
|
||||
@@ -279,7 +279,7 @@ export class PluginMarketService {
|
||||
const stored = localStorage.getItem(this.STORAGE_KEY);
|
||||
if (stored) {
|
||||
const plugins: InstalledPluginInfo[] = JSON.parse(stored);
|
||||
this.installedPlugins = new Map(plugins.map(p => [p.id, p]));
|
||||
this.installedPlugins = new Map(plugins.map((p) => [p.id, p]));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[PluginMarketService] Failed to load installed plugins:', error);
|
||||
|
||||
@@ -221,7 +221,7 @@ export class PluginPublishService {
|
||||
manifest: Record<string, unknown>,
|
||||
publishInfo: PluginPublishInfo
|
||||
): Promise<void> {
|
||||
this.notifyProgress('uploading-files', `Checking for existing manifest...`, 65);
|
||||
this.notifyProgress('uploading-files', 'Checking for existing manifest...', 65);
|
||||
|
||||
const pluginId = this.generatePluginId(publishInfo.pluginMetadata.name);
|
||||
const existingManifest = await this.getExistingManifest(pluginId, publishInfo.category);
|
||||
@@ -229,7 +229,7 @@ export class PluginPublishService {
|
||||
let finalManifest = manifest;
|
||||
|
||||
if (existingManifest) {
|
||||
this.notifyProgress('uploading-files', `Merging with existing manifest...`, 68);
|
||||
this.notifyProgress('uploading-files', 'Merging with existing manifest...', 68);
|
||||
finalManifest = this.mergeManifestVersions(existingManifest, manifest, publishInfo.version);
|
||||
}
|
||||
|
||||
|
||||
@@ -86,7 +86,7 @@ export class PluginSourceParser {
|
||||
const packageJson = JSON.parse(packageJsonContent) as PluginPackageJson;
|
||||
|
||||
// 验证 ZIP 中必须包含 dist 目录
|
||||
const distFiles = Object.keys(zip.files).filter(f => f.startsWith('dist/'));
|
||||
const distFiles = Object.keys(zip.files).filter((f) => f.startsWith('dist/'));
|
||||
if (distFiles.length === 0) {
|
||||
throw new Error('dist/ directory not found in ZIP file. Please ensure the plugin is properly built.');
|
||||
}
|
||||
|
||||
219
packages/editor-app/src/services/RuntimeResolver.ts
Normal file
219
packages/editor-app/src/services/RuntimeResolver.ts
Normal file
@@ -0,0 +1,219 @@
|
||||
/**
|
||||
* Runtime Module Resolver
|
||||
* 运行时模块解析器
|
||||
*
|
||||
* Resolves runtime module paths based on environment and configuration
|
||||
* 根据环境和配置解析运行时模块路径
|
||||
*/
|
||||
|
||||
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 !== ''
|
||||
);
|
||||
return segments.join('/');
|
||||
};
|
||||
|
||||
// Check if we're in development mode
|
||||
const isDevelopment = (): boolean => {
|
||||
try {
|
||||
// Vite environment variable
|
||||
return (import.meta as any).env?.DEV === true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export interface RuntimeModule {
|
||||
type: 'javascript' | 'wasm' | 'binary';
|
||||
files: string[];
|
||||
sourcePath: string;
|
||||
}
|
||||
|
||||
export interface RuntimeConfig {
|
||||
runtime: {
|
||||
version: string;
|
||||
modules: Record<string, any>;
|
||||
};
|
||||
}
|
||||
|
||||
export class RuntimeResolver {
|
||||
private static instance: RuntimeResolver;
|
||||
private config: RuntimeConfig | null = null;
|
||||
private baseDir: string = '';
|
||||
|
||||
private constructor() {}
|
||||
|
||||
static getInstance(): RuntimeResolver {
|
||||
if (!RuntimeResolver.instance) {
|
||||
RuntimeResolver.instance = new RuntimeResolver();
|
||||
}
|
||||
return RuntimeResolver.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the runtime resolver
|
||||
* 初始化运行时解析器
|
||||
*/
|
||||
async initialize(): Promise<void> {
|
||||
// Load runtime configuration
|
||||
const response = await fetch('/runtime.config.json');
|
||||
this.config = await response.json();
|
||||
|
||||
// Determine base directory based on environment
|
||||
if (isDevelopment()) {
|
||||
// In development, use the project root
|
||||
// We need to go up from src-tauri to get the actual project root
|
||||
const currentDir = await TauriAPI.getCurrentDir();
|
||||
// currentDir might be src-tauri, so we need to find the actual workspace root
|
||||
this.baseDir = await this.findWorkspaceRoot(currentDir);
|
||||
} else {
|
||||
// In production, use the resource directory
|
||||
this.baseDir = await TauriAPI.getAppResourceDir();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find workspace root by looking for package.json or specific markers
|
||||
* 通过查找 package.json 或特定标记来找到工作区根目录
|
||||
*/
|
||||
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
|
||||
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
|
||||
return parts.join('\\');
|
||||
}
|
||||
|
||||
// Check for workspace markers
|
||||
const workspaceMarkers = [
|
||||
`${currentPath}\\pnpm-workspace.yaml`,
|
||||
`${currentPath}\\packages\\editor-app`,
|
||||
`${currentPath}\\packages\\platform-web`
|
||||
];
|
||||
|
||||
for (const marker of workspaceMarkers) {
|
||||
if (await TauriAPI.pathExists(marker)) {
|
||||
return currentPath;
|
||||
}
|
||||
}
|
||||
|
||||
// Go up one level
|
||||
const parts = currentPath.split(/[/\\]/);
|
||||
parts.pop();
|
||||
currentPath = parts.join('\\');
|
||||
}
|
||||
|
||||
// Fallback to current directory
|
||||
return startPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get runtime module files
|
||||
* 获取运行时模块文件
|
||||
*/
|
||||
async getModuleFiles(moduleName: string): Promise<RuntimeModule> {
|
||||
if (!this.config) {
|
||||
await this.initialize();
|
||||
}
|
||||
|
||||
const moduleConfig = this.config!.runtime.modules[moduleName];
|
||||
if (!moduleConfig) {
|
||||
throw new Error(`Runtime module ${moduleName} not found in configuration`);
|
||||
}
|
||||
|
||||
const isDev = isDevelopment();
|
||||
const files: string[] = [];
|
||||
let sourcePath: string;
|
||||
|
||||
if (isDev) {
|
||||
// Development mode - use relative paths from workspace root
|
||||
const devPath = moduleConfig.development.path;
|
||||
sourcePath = `${this.baseDir}\\packages\\${sanitizePath(devPath)}`;
|
||||
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare runtime files for browser preview
|
||||
* 为浏览器预览准备运行时文件
|
||||
*/
|
||||
async prepareRuntimeFiles(targetDir: string): Promise<void> {
|
||||
// Ensure target directory exists
|
||||
const dirExists = await TauriAPI.pathExists(targetDir);
|
||||
if (!dirExists) {
|
||||
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}`;
|
||||
|
||||
if (await TauriAPI.pathExists(srcFile)) {
|
||||
await TauriAPI.copyFile(srcFile, dstFile);
|
||||
} else {
|
||||
throw new Error(`Runtime file not found: ${srcFile}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 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}`;
|
||||
|
||||
if (await TauriAPI.pathExists(srcFile)) {
|
||||
await TauriAPI.copyFile(srcFile, dstFile);
|
||||
} else {
|
||||
throw new Error(`Engine file not found: ${srcFile}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get workspace root directory
|
||||
* 获取工作区根目录
|
||||
*/
|
||||
getBaseDir(): string {
|
||||
return this.baseDir;
|
||||
}
|
||||
}
|
||||
@@ -30,11 +30,11 @@ export class TauriFileSystemService implements IFileSystem {
|
||||
}
|
||||
|
||||
async listDirectory(path: string): Promise<FileEntry[]> {
|
||||
const entries = await invoke<Array<{ name: string; isDir: boolean }>>('list_directory', { path });
|
||||
return entries.map(entry => ({
|
||||
const entries = await invoke<Array<{ name: string; path: string; is_dir: boolean }>>('list_directory', { path });
|
||||
return entries.map((entry) => ({
|
||||
name: entry.name,
|
||||
isDirectory: entry.isDir,
|
||||
path: `${path}/${entry.name}`
|
||||
isDirectory: entry.is_dir,
|
||||
path: entry.path
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user