Feature/editor optimization (#251)
* refactor: 编辑器/运行时架构拆分与构建系统升级 * feat(core): 层级系统重构与UI变换矩阵修复 * refactor: 移除 ecs-components 聚合包并修复跨包组件查找问题 * fix(physics): 修复跨包组件类引用问题 * feat: 统一运行时架构与浏览器运行支持 * feat(asset): 实现浏览器运行时资产加载系统 * fix: 修复文档、CodeQL安全问题和CI类型检查错误 * fix: 修复文档、CodeQL安全问题和CI类型检查错误 * fix: 修复文档、CodeQL安全问题、CI类型检查和测试错误 * test: 补齐核心模块测试用例,修复CI构建配置 * fix: 修复测试用例中的类型错误和断言问题 * fix: 修复 turbo build:npm 任务的依赖顺序问题 * fix: 修复 CI 构建错误并优化构建性能
This commit is contained in:
@@ -8,8 +8,9 @@ import '../styles/Viewport.css';
|
||||
import { useEngine } from '../hooks/useEngine';
|
||||
import { EngineService } from '../services/EngineService';
|
||||
import { Core, Entity, SceneSerializer } from '@esengine/ecs-framework';
|
||||
import { MessageHub } from '@esengine/editor-core';
|
||||
import { TransformComponent, CameraComponent } from '@esengine/ecs-components';
|
||||
import { MessageHub, ProjectService, AssetRegistryService } from '@esengine/editor-core';
|
||||
import { TransformComponent } from '@esengine/engine-core';
|
||||
import { CameraComponent } from '@esengine/camera';
|
||||
import { UITransformComponent } from '@esengine/ui';
|
||||
import { TauriAPI } from '../api/tauri';
|
||||
import { open } from '@tauri-apps/plugin-shell';
|
||||
@@ -59,7 +60,8 @@ function generateRuntimeHtml(): string {
|
||||
const runtime = ECSRuntime.create({
|
||||
canvasId: 'runtime-canvas',
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight
|
||||
height: window.innerHeight,
|
||||
projectConfigUrl: '/ecs-editor.config.json'
|
||||
});
|
||||
|
||||
await runtime.initialize(esEngine);
|
||||
@@ -354,11 +356,13 @@ export function Viewport({ locale = 'en', messageHub }: ViewportProps) {
|
||||
|
||||
if (messageHubRef.current) {
|
||||
const propertyName = mode === 'move' ? 'position' : mode === 'rotate' ? 'rotation' : 'scale';
|
||||
const value = propertyName === 'position' ? transform.position :
|
||||
propertyName === 'rotation' ? transform.rotation : transform.scale;
|
||||
messageHubRef.current.publish('component:property:changed', {
|
||||
entity,
|
||||
component: transform,
|
||||
propertyName,
|
||||
value: transform[propertyName]
|
||||
value
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -373,16 +377,29 @@ export function Viewport({ locale = 'en', messageHub }: ViewportProps) {
|
||||
const rotationSpeed = 0.01;
|
||||
uiTransform.rotation += deltaX * rotationSpeed;
|
||||
} else if (mode === 'scale') {
|
||||
const width = uiTransform.width * uiTransform.scaleX;
|
||||
const height = uiTransform.height * uiTransform.scaleY;
|
||||
const centerX = uiTransform.x + width * uiTransform.pivotX;
|
||||
const centerY = uiTransform.y + height * uiTransform.pivotY;
|
||||
const startDist = Math.sqrt((worldStart.x - centerX) ** 2 + (worldStart.y - centerY) ** 2);
|
||||
const endDist = Math.sqrt((worldEnd.x - centerX) ** 2 + (worldEnd.y - centerY) ** 2);
|
||||
const oldWidth = uiTransform.width * uiTransform.scaleX;
|
||||
const oldHeight = uiTransform.height * uiTransform.scaleY;
|
||||
|
||||
// pivot点的世界坐标(缩放前)
|
||||
const pivotWorldX = uiTransform.x + oldWidth * uiTransform.pivotX;
|
||||
const pivotWorldY = uiTransform.y + oldHeight * uiTransform.pivotY;
|
||||
|
||||
const startDist = Math.sqrt((worldStart.x - pivotWorldX) ** 2 + (worldStart.y - pivotWorldY) ** 2);
|
||||
const endDist = Math.sqrt((worldEnd.x - pivotWorldX) ** 2 + (worldEnd.y - pivotWorldY) ** 2);
|
||||
|
||||
if (startDist > 0) {
|
||||
const scaleFactor = endDist / startDist;
|
||||
uiTransform.scaleX *= scaleFactor;
|
||||
uiTransform.scaleY *= scaleFactor;
|
||||
const newScaleX = uiTransform.scaleX * scaleFactor;
|
||||
const newScaleY = uiTransform.scaleY * scaleFactor;
|
||||
|
||||
const newWidth = uiTransform.width * newScaleX;
|
||||
const newHeight = uiTransform.height * newScaleY;
|
||||
|
||||
// 调整位置使pivot点保持不动
|
||||
uiTransform.x = pivotWorldX - newWidth * uiTransform.pivotX;
|
||||
uiTransform.y = pivotWorldY - newHeight * uiTransform.pivotY;
|
||||
uiTransform.scaleX = newScaleX;
|
||||
uiTransform.scaleY = newScaleY;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -689,46 +706,118 @@ export function Viewport({ locale = 'en', messageHub }: ViewportProps) {
|
||||
// Write scene data and HTML (always update)
|
||||
await TauriAPI.writeFileContent(`${runtimeDir}/scene.json`, sceneData);
|
||||
|
||||
// Copy texture assets referenced in the scene
|
||||
// 复制场景中引用的纹理资产
|
||||
const sceneObj = JSON.parse(sceneData);
|
||||
const texturePathSet = new Set<string>();
|
||||
|
||||
// Find all texture paths in sprite components
|
||||
if (sceneObj.entities) {
|
||||
for (const entity of sceneObj.entities) {
|
||||
if (entity.components) {
|
||||
for (const comp of entity.components) {
|
||||
if (comp.type === 'Sprite' && comp.data?.texture) {
|
||||
texturePathSet.add(comp.data.texture);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Copy project config file (for plugin settings)
|
||||
// 复制项目配置文件(用于插件设置)
|
||||
const projectService = Core.services.tryResolve(ProjectService);
|
||||
const projectPath = projectService?.getCurrentProject()?.path;
|
||||
if (projectPath) {
|
||||
const configPath = `${projectPath}\\ecs-editor.config.json`;
|
||||
const configExists = await TauriAPI.pathExists(configPath);
|
||||
if (configExists) {
|
||||
await TauriAPI.copyFile(configPath, `${runtimeDir}\\ecs-editor.config.json`);
|
||||
console.log('[Viewport] Copied project config to runtime dir');
|
||||
}
|
||||
}
|
||||
|
||||
// Create assets directory and copy textures
|
||||
// Create assets directory
|
||||
// 创建资产目录
|
||||
const assetsDir = `${runtimeDir}\\assets`;
|
||||
const assetsDirExists = await TauriAPI.pathExists(assetsDir);
|
||||
if (!assetsDirExists) {
|
||||
await TauriAPI.createDirectory(assetsDir);
|
||||
}
|
||||
|
||||
for (const texturePath of texturePathSet) {
|
||||
if (texturePath && (texturePath.includes(':\\') || texturePath.startsWith('/'))) {
|
||||
try {
|
||||
const filename = texturePath.split(/[/\\]/).pop() || '';
|
||||
const destPath = `${assetsDir}\\${filename}`;
|
||||
const exists = await TauriAPI.pathExists(texturePath);
|
||||
if (exists) {
|
||||
await TauriAPI.copyFile(texturePath, destPath);
|
||||
// Collect all asset paths from scene
|
||||
// 从场景中收集所有资产路径
|
||||
const sceneObj = JSON.parse(sceneData);
|
||||
const assetPaths = new Set<string>();
|
||||
|
||||
// Scan all components for asset references
|
||||
if (sceneObj.entities) {
|
||||
for (const entity of sceneObj.entities) {
|
||||
if (entity.components) {
|
||||
for (const comp of entity.components) {
|
||||
// Sprite textures
|
||||
if (comp.type === 'Sprite' && comp.data?.texture) {
|
||||
assetPaths.add(comp.data.texture);
|
||||
}
|
||||
// Behavior tree assets
|
||||
if (comp.type === 'BehaviorTreeRuntime' && comp.data?.treeAssetId) {
|
||||
assetPaths.add(comp.data.treeAssetId);
|
||||
}
|
||||
// Tilemap assets
|
||||
if (comp.type === 'Tilemap' && comp.data?.tmxPath) {
|
||||
assetPaths.add(comp.data.tmxPath);
|
||||
}
|
||||
// Audio assets
|
||||
if (comp.type === 'AudioSource' && comp.data?.clip) {
|
||||
assetPaths.add(comp.data.clip);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to copy texture ${texturePath}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build asset catalog and copy files
|
||||
// 构建资产目录并复制文件
|
||||
const catalogEntries: Record<string, { guid: string; path: string; type: string; size: number; hash: string }> = {};
|
||||
|
||||
for (const assetPath of assetPaths) {
|
||||
if (!assetPath || (!assetPath.includes(':\\') && !assetPath.startsWith('/'))) continue;
|
||||
|
||||
try {
|
||||
const exists = await TauriAPI.pathExists(assetPath);
|
||||
if (!exists) {
|
||||
console.warn(`[Viewport] Asset not found: ${assetPath}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get filename and determine relative path
|
||||
const filename = assetPath.split(/[/\\]/).pop() || '';
|
||||
const destPath = `${assetsDir}\\${filename}`;
|
||||
const relativePath = `assets/${filename}`;
|
||||
|
||||
// Copy file
|
||||
await TauriAPI.copyFile(assetPath, destPath);
|
||||
|
||||
// Determine asset type from extension
|
||||
const ext = filename.substring(filename.lastIndexOf('.')).toLowerCase();
|
||||
const typeMap: Record<string, string> = {
|
||||
'.png': 'texture', '.jpg': 'texture', '.jpeg': 'texture', '.webp': 'texture',
|
||||
'.btree': 'btree',
|
||||
'.tmx': 'tilemap', '.tsx': 'tileset',
|
||||
'.mp3': 'audio', '.ogg': 'audio', '.wav': 'audio',
|
||||
'.json': 'json'
|
||||
};
|
||||
const assetType = typeMap[ext] || 'binary';
|
||||
|
||||
// Generate simple GUID based on path
|
||||
const guid = assetPath.replace(/[^a-zA-Z0-9]/g, '-').substring(0, 36);
|
||||
|
||||
catalogEntries[guid] = {
|
||||
guid,
|
||||
path: relativePath,
|
||||
type: assetType,
|
||||
size: 0,
|
||||
hash: ''
|
||||
};
|
||||
|
||||
console.log(`[Viewport] Copied asset: ${filename}`);
|
||||
} catch (error) {
|
||||
console.error(`[Viewport] Failed to copy asset ${assetPath}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
// Write asset catalog
|
||||
// 写入资产目录
|
||||
const assetCatalog = {
|
||||
version: '1.0.0',
|
||||
createdAt: Date.now(),
|
||||
entries: catalogEntries
|
||||
};
|
||||
await TauriAPI.writeFileContent(`${runtimeDir}/asset-catalog.json`, JSON.stringify(assetCatalog, null, 2));
|
||||
console.log(`[Viewport] Asset catalog created with ${Object.keys(catalogEntries).length} entries`);
|
||||
|
||||
const runtimeHtml = generateRuntimeHtml();
|
||||
await TauriAPI.writeFileContent(`${runtimeDir}/index.html`, runtimeHtml);
|
||||
|
||||
@@ -781,6 +870,19 @@ export function Viewport({ locale = 'en', messageHub }: ViewportProps) {
|
||||
await runtimeResolver.initialize();
|
||||
await runtimeResolver.prepareRuntimeFiles(runtimeDir);
|
||||
|
||||
// Copy project config file (for plugin settings)
|
||||
const projectService = Core.services.tryResolve(ProjectService);
|
||||
if (projectService) {
|
||||
const currentProject = projectService.getCurrentProject();
|
||||
if (currentProject?.path) {
|
||||
const configPath = `${currentProject.path}\\ecs-editor.config.json`;
|
||||
const configExists = await TauriAPI.pathExists(configPath);
|
||||
if (configExists) {
|
||||
await TauriAPI.copyFile(configPath, `${runtimeDir}\\ecs-editor.config.json`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Write scene data and HTML
|
||||
const sceneDataStr = typeof sceneData === 'string' ? sceneData : new TextDecoder().decode(sceneData);
|
||||
await TauriAPI.writeFileContent(`${runtimeDir}/scene.json`, sceneDataStr);
|
||||
|
||||
Reference in New Issue
Block a user