feat(engine): 添加编辑器模式标志控制编辑器UI显示 (#274)

* feat(engine): 添加编辑器模式标志控制编辑器UI显示

- 在 Rust 引擎中添加 isEditor 标志,控制网格、gizmos、坐标轴指示器的显示
- 运行时模式下自动隐藏所有编辑器专用 UI
- 编辑器预览和浏览器运行时通过 setEditorMode(false) 禁用编辑器 UI
- 添加 Scene.isEditorMode 延迟组件生命周期回调,直到 begin() 调用
- 修复用户组件注册到 Core ComponentRegistry 以支持序列化
- 修复 Run in Browser 时用户组件加载问题

* fix: 复制引擎模块的类型定义文件到 dist/engine

* fix: 修复用户项目 tsconfig paths 类型定义路径

- 从 module.json 读取实际包名而不是使用目录名
- 修复 .d.ts 文件复制逻辑,支持 .mjs 扩展名
This commit is contained in:
YHH
2025-12-04 22:43:26 +08:00
committed by GitHub
parent 0d9bab910e
commit d7454e3ca4
16 changed files with 393 additions and 40 deletions

View File

@@ -475,12 +475,26 @@ fn update_tsconfig_file(
// Check for index.d.ts
// 检查是否存在 index.d.ts
let dts_path = module_path.join("index.d.ts");
if dts_path.exists() {
let module_name = format!("@esengine/{}", module_id);
let dts_path_str = format!("{}/{}/index.d.ts", engine_path_normalized, module_id);
paths.insert(module_name, serde_json::json!([dts_path_str]));
module_count += 1;
if !dts_path.exists() {
continue;
}
// Read module.json to get the actual package name
// 读取 module.json 获取实际的包名
let module_json_path = module_path.join("module.json");
let module_name = if module_json_path.exists() {
fs::read_to_string(&module_json_path)
.ok()
.and_then(|content| serde_json::from_str::<serde_json::Value>(&content).ok())
.and_then(|json| json.get("name").and_then(|n| n.as_str()).map(|s| s.to_string()))
.unwrap_or_else(|| format!("@esengine/{}", module_id))
} else {
format!("@esengine/{}", module_id)
};
let dts_path_str = format!("{}/{}/index.d.ts", engine_path_normalized, module_id);
paths.insert(module_name, serde_json::json!([dts_path_str]));
module_count += 1;
}
}

View File

@@ -248,14 +248,20 @@ export class ${className} extends Component {
@Property({ type: 'number', label: 'Example Property' })
public exampleProperty: number = 0;
onInitialize(): void {
// 组件初始化时调用
// Called when component is initialized
/**
* 组件添加到实体时调用
* Called when component is added to entity
*/
onAddedToEntity(): void {
console.log('${className} added to entity');
}
onDestroy(): void {
// 组件销毁时调用
// Called when component is destroyed
/**
* 组件从实体移除时调用
* Called when component is removed from entity
*/
onRemovedFromEntity(): void {
console.log('${className} removed from entity');
}
}
`;

View File

@@ -25,8 +25,12 @@ import type { ModuleManifest } from '../services/RuntimeResolver';
*
* This matches the structure of published builds for consistency
* 这与发布构建的结构一致
*
* @param importMap - Import map for module resolution
* @param modules - Module manifests for plugin loading
* @param hasUserRuntime - Whether user-runtime.js exists and should be loaded
*/
function generateRuntimeHtml(importMap: Record<string, string>, modules: ModuleManifest[]): string {
function generateRuntimeHtml(importMap: Record<string, string>, modules: ModuleManifest[], hasUserRuntime: boolean = false): string {
const importMapScript = `<script type="importmap">
${JSON.stringify({ imports: importMap }, null, 2).split('\n').join('\n ')}
</script>`;
@@ -45,6 +49,44 @@ function generateRuntimeHtml(importMap: Record<string, string>, modules: ModuleM
}`
).join('\n');
// Generate user runtime loading code
// 生成用户运行时加载代码
const userRuntimeCode = hasUserRuntime ? `
updateLoading('Loading user scripts...');
try {
// Import ECS framework and set up global for user-runtime.js shim
// 导入 ECS 框架并为 user-runtime.js 设置全局变量
const ecsFramework = await import('@esengine/ecs-framework');
window.__ESENGINE__ = window.__ESENGINE__ || {};
window.__ESENGINE__.ecsFramework = ecsFramework;
// Load user-runtime.js which contains compiled user components
// 加载 user-runtime.js其中包含编译的用户组件
const userRuntimeScript = document.createElement('script');
userRuntimeScript.src = './user-runtime.js?_=' + Date.now();
await new Promise((resolve, reject) => {
userRuntimeScript.onload = resolve;
userRuntimeScript.onerror = reject;
document.head.appendChild(userRuntimeScript);
});
// Register user components to ComponentRegistry
// 将用户组件注册到 ComponentRegistry
if (window.__USER_RUNTIME_EXPORTS__) {
const { ComponentRegistry, Component } = ecsFramework;
const exports = window.__USER_RUNTIME_EXPORTS__;
for (const [name, exported] of Object.entries(exports)) {
if (typeof exported === 'function' && exported.prototype instanceof Component) {
ComponentRegistry.register(exported);
console.log('[Preview] Registered user component:', name);
}
}
}
} catch (e) {
console.warn('[Preview] Failed to load user scripts:', e.message);
}
` : '';
return `<!DOCTYPE html>
<html lang="en">
<head>
@@ -136,7 +178,7 @@ ${importMapScript}
${pluginImportCode}
await runtime.initialize(wasmModule);
${userRuntimeCode}
updateLoading('Loading scene...');
await runtime.loadScene('./scene.json?_=' + Date.now());
@@ -681,9 +723,9 @@ export function Viewport({ locale = 'en', messageHub }: ViewportProps) {
// Save editor camera state
editorCameraRef.current = { x: camera2DOffset.x, y: camera2DOffset.y, zoom: camera2DZoom };
setPlayState('playing');
// Hide grid and gizmos in play mode
EngineService.getInstance().setShowGrid(false);
EngineService.getInstance().setShowGizmos(false);
// Disable editor mode (hides grid, gizmos, axis indicator)
// 禁用编辑器模式隐藏网格、gizmos、坐标轴指示器
EngineService.getInstance().setEditorMode(false);
// Switch to player camera
syncPlayerCamera();
engine.start();
@@ -708,9 +750,9 @@ export function Viewport({ locale = 'en', messageHub }: ViewportProps) {
// Restore editor camera state
setCamera2DOffset({ x: editorCameraRef.current.x, y: editorCameraRef.current.y });
setCamera2DZoom(editorCameraRef.current.zoom);
// Restore grid and gizmos
EngineService.getInstance().setShowGrid(showGrid);
EngineService.getInstance().setShowGizmos(showGizmos);
// Restore editor mode (restores grid, gizmos, axis indicator based on settings)
// 恢复编辑器模式根据设置恢复网格、gizmos、坐标轴指示器
EngineService.getInstance().setEditorMode(true);
// Restore editor default background color
EngineService.getInstance().setClearColor(0.1, 0.1, 0.12, 1.0);
};
@@ -888,8 +930,21 @@ export function Viewport({ locale = 'en', messageHub }: ViewportProps) {
await TauriAPI.writeFileContent(`${runtimeDir}/asset-catalog.json`, JSON.stringify(assetCatalog, null, 2));
console.log(`[Viewport] Asset catalog created with ${Object.keys(catalogEntries).length} entries`);
// Copy user-runtime.js if it exists
// 如果存在用户运行时,复制 user-runtime.js
let hasUserRuntime = false;
if (projectPath) {
const userRuntimePath = `${projectPath}\\.esengine\\compiled\\user-runtime.js`;
const userRuntimeExists = await TauriAPI.pathExists(userRuntimePath);
if (userRuntimeExists) {
await TauriAPI.copyFile(userRuntimePath, `${runtimeDir}\\user-runtime.js`);
console.log('[Viewport] Copied user-runtime.js');
hasUserRuntime = true;
}
}
// Generate HTML with import maps (matching published build structure)
const runtimeHtml = generateRuntimeHtml(importMap, modules);
const runtimeHtml = generateRuntimeHtml(importMap, modules, hasUserRuntime);
await TauriAPI.writeFileContent(`${runtimeDir}/index.html`, runtimeHtml);
// Start local server and open browser
@@ -954,10 +1009,26 @@ export function Viewport({ locale = 'en', messageHub }: ViewportProps) {
}
}
// Write scene data and HTML with import maps
// Write scene data
const sceneDataStr = typeof sceneData === 'string' ? sceneData : new TextDecoder().decode(sceneData);
await TauriAPI.writeFileContent(`${runtimeDir}/scene.json`, sceneDataStr);
await TauriAPI.writeFileContent(`${runtimeDir}/index.html`, generateRuntimeHtml(importMap, modules));
// Copy user-runtime.js if it exists
// 如果存在用户运行时,复制 user-runtime.js
let hasUserRuntime = false;
const currentProject = projectService?.getCurrentProject();
if (currentProject?.path) {
const userRuntimePath = `${currentProject.path}\\.esengine\\compiled\\user-runtime.js`;
const userRuntimeExists = await TauriAPI.pathExists(userRuntimePath);
if (userRuntimeExists) {
await TauriAPI.copyFile(userRuntimePath, `${runtimeDir}\\user-runtime.js`);
console.log('[Viewport] Copied user-runtime.js for device preview');
hasUserRuntime = true;
}
}
// Write HTML with import maps
await TauriAPI.writeFileContent(`${runtimeDir}/index.html`, generateRuntimeHtml(importMap, modules, hasUserRuntime));
// Copy textures referenced in scene
const assetsDir = `${runtimeDir}\\assets`;

View File

@@ -612,6 +612,26 @@ export class EngineService {
return this._runtime?.renderSystem?.getShowGizmos() ?? true;
}
/**
* Set editor mode.
* 设置编辑器模式。
*
* When false (runtime mode), editor-only UI like grid, gizmos,
* and axis indicator are automatically hidden.
* 当为 false运行时模式编辑器专用 UI 会自动隐藏。
*/
setEditorMode(isEditor: boolean): void {
this._runtime?.setEditorMode(isEditor);
}
/**
* Get editor mode.
* 获取编辑器模式。
*/
isEditorMode(): boolean {
return this._runtime?.isEditorMode() ?? true;
}
/**
* Set UI canvas size for boundary display.
*/

View File

@@ -213,6 +213,15 @@ function copyEngineModulesPlugin(): Plugin {
if (fs.existsSync(sourceMapPath)) {
fs.copyFileSync(sourceMapPath, path.join(moduleOutputDir, 'index.js.map'));
}
// Copy type definitions if exists
// 复制类型定义文件(如果存在)
// Handle both .js and .mjs extensions
// 处理 .js 和 .mjs 两种扩展名
const distDir = path.dirname(module.distPath);
const dtsPath = path.join(distDir, 'index.d.ts');
if (fs.existsSync(dtsPath)) {
fs.copyFileSync(dtsPath, path.join(moduleOutputDir, 'index.d.ts'));
}
hasRuntime = true;
// Copy additional included files (e.g., chunks)