Compare commits
10 Commits
@esengine/
...
fix/event-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
60ba67de58 | ||
|
|
241ee577fe | ||
|
|
ea7990461b | ||
|
|
cadf147b74 | ||
|
|
c744d8d9fc | ||
|
|
566e1977fd | ||
|
|
17f6259f43 | ||
|
|
5d3483fc65 | ||
|
|
d07a5d81fc | ||
|
|
6a4e6fbc04 |
@@ -321,11 +321,19 @@ export class Entity {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 通知Scene中的QuerySystem实体组件发生变动
|
* 通知Scene中的QuerySystem实体组件发生变动
|
||||||
|
*
|
||||||
|
* Notify the QuerySystem in Scene that entity components have changed
|
||||||
|
*
|
||||||
|
* @param changedComponentType 变化的组件类型(可选,用于优化通知) | Changed component type (optional, for optimized notification)
|
||||||
*/
|
*/
|
||||||
private notifyQuerySystems(): void {
|
private notifyQuerySystems(changedComponentType?: ComponentType): void {
|
||||||
if (this.scene && this.scene.querySystem) {
|
if (this.scene && this.scene.querySystem) {
|
||||||
this.scene.querySystem.updateEntity(this);
|
this.scene.querySystem.updateEntity(this);
|
||||||
this.scene.clearSystemEntityCaches();
|
this.scene.clearSystemEntityCaches();
|
||||||
|
// 事件驱动:立即通知关心该组件的系统 | Event-driven: notify systems that care about this component
|
||||||
|
if (this.scene.notifyEntityComponentChanged) {
|
||||||
|
this.scene.notifyEntityComponentChanged(this, changedComponentType);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -381,7 +389,7 @@ export class Entity {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
this.notifyQuerySystems();
|
this.notifyQuerySystems(componentType);
|
||||||
|
|
||||||
return component;
|
return component;
|
||||||
}
|
}
|
||||||
@@ -514,7 +522,7 @@ export class Entity {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
this.notifyQuerySystems();
|
this.notifyQuerySystems(componentType);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Entity } from './Entity';
|
|||||||
import { EntityList } from './Utils/EntityList';
|
import { EntityList } from './Utils/EntityList';
|
||||||
import { IdentifierPool } from './Utils/IdentifierPool';
|
import { IdentifierPool } from './Utils/IdentifierPool';
|
||||||
import { EntitySystem } from './Systems/EntitySystem';
|
import { EntitySystem } from './Systems/EntitySystem';
|
||||||
import { ComponentStorageManager } from './Core/ComponentStorage';
|
import { ComponentStorageManager, ComponentType } from './Core/ComponentStorage';
|
||||||
import { QuerySystem } from './Core/QuerySystem';
|
import { QuerySystem } from './Core/QuerySystem';
|
||||||
import { TypeSafeEventSystem } from './Core/EventSystem';
|
import { TypeSafeEventSystem } from './Core/EventSystem';
|
||||||
import type { ReferenceTracker } from './Core/ReferenceTracker';
|
import type { ReferenceTracker } from './Core/ReferenceTracker';
|
||||||
@@ -120,9 +120,26 @@ export interface IScene {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 清除所有EntitySystem的实体缓存
|
* 清除所有EntitySystem的实体缓存
|
||||||
|
* Clear all EntitySystem entity caches
|
||||||
*/
|
*/
|
||||||
clearSystemEntityCaches(): void;
|
clearSystemEntityCaches(): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通知相关系统实体的组件发生了变化
|
||||||
|
*
|
||||||
|
* 当组件被添加或移除时调用,立即通知相关系统检查该实体是否匹配,
|
||||||
|
* 并触发 onAdded/onRemoved 回调。通过组件ID索引优化,只通知关心该组件的系统。
|
||||||
|
*
|
||||||
|
* Notify relevant systems that an entity's components have changed.
|
||||||
|
* Called when a component is added or removed, immediately notifying
|
||||||
|
* relevant systems to check if the entity matches and trigger onAdded/onRemoved callbacks.
|
||||||
|
* Optimized via component ID indexing to only notify systems that care about the changed component.
|
||||||
|
*
|
||||||
|
* @param entity 组件发生变化的实体 | The entity whose components changed
|
||||||
|
* @param changedComponentType 变化的组件类型(可选) | The changed component type (optional)
|
||||||
|
*/
|
||||||
|
notifyEntityComponentChanged(entity: Entity, changedComponentType?: ComponentType): void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 添加实体
|
* 添加实体
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -151,6 +151,30 @@ export class Scene implements IScene {
|
|||||||
*/
|
*/
|
||||||
private _systemAddCounter: number = 0;
|
private _systemAddCounter: number = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 组件ID到系统的索引映射
|
||||||
|
*
|
||||||
|
* 用于快速查找关心特定组件的系统,避免遍历所有系统。
|
||||||
|
* 使用组件ID(数字)而非ComponentType作为key,避免类引用问题。
|
||||||
|
*
|
||||||
|
* Component ID to systems index map.
|
||||||
|
* Used for fast lookup of systems that care about specific components.
|
||||||
|
* Uses component ID (number) instead of ComponentType as key to avoid class reference issues.
|
||||||
|
*/
|
||||||
|
private _componentIdToSystems: Map<number, Set<EntitySystem>> = new Map();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 需要接收所有组件变化通知的系统集合
|
||||||
|
*
|
||||||
|
* 包括使用 none 条件、tag/name 查询、或空匹配器的系统。
|
||||||
|
* 这些系统无法通过组件ID索引优化,需要在每次组件变化时都检查。
|
||||||
|
*
|
||||||
|
* Systems that need to receive all component change notifications.
|
||||||
|
* Includes systems using none conditions, tag/name queries, or empty matchers.
|
||||||
|
* These systems cannot be optimized via component ID indexing.
|
||||||
|
*/
|
||||||
|
private _globalNotifySystems: Set<EntitySystem> = new Set();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取场景中所有已注册的EntitySystem
|
* 获取场景中所有已注册的EntitySystem
|
||||||
*
|
*
|
||||||
@@ -344,6 +368,10 @@ export class Scene implements IScene {
|
|||||||
// 清空系统缓存
|
// 清空系统缓存
|
||||||
this._cachedSystems = null;
|
this._cachedSystems = null;
|
||||||
this._systemsOrderDirty = true;
|
this._systemsOrderDirty = true;
|
||||||
|
|
||||||
|
// 清空组件索引 | Clear component indices
|
||||||
|
this._componentIdToSystems.clear();
|
||||||
|
this._globalNotifySystems.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -453,6 +481,146 @@ export class Scene implements IScene {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通知相关系统实体的组件发生了变化
|
||||||
|
*
|
||||||
|
* 这是事件驱动设计的核心:当组件被添加或移除时,立即通知相关系统检查该实体是否匹配,
|
||||||
|
* 并触发 onAdded/onRemoved 回调。通过组件ID索引优化,只通知关心该组件的系统。
|
||||||
|
*
|
||||||
|
* This is the core of event-driven design: when a component is added or removed,
|
||||||
|
* immediately notify relevant systems to check if the entity matches and trigger
|
||||||
|
* onAdded/onRemoved callbacks. Optimized via component ID indexing to only notify
|
||||||
|
* systems that care about the changed component.
|
||||||
|
*
|
||||||
|
* @param entity 组件发生变化的实体 | The entity whose components changed
|
||||||
|
* @param changedComponentType 变化的组件类型(可选) | The changed component type (optional)
|
||||||
|
*/
|
||||||
|
public notifyEntityComponentChanged(entity: Entity, changedComponentType?: ComponentType): void {
|
||||||
|
// 已通知的系统集合,避免重复通知 | Set of notified systems to avoid duplicates
|
||||||
|
const notifiedSystems = new Set<EntitySystem>();
|
||||||
|
|
||||||
|
// 如果提供了组件类型,使用索引优化 | If component type provided, use index optimization
|
||||||
|
if (changedComponentType && ComponentRegistry.isRegistered(changedComponentType)) {
|
||||||
|
const componentId = ComponentRegistry.getBitIndex(changedComponentType);
|
||||||
|
const interestedSystems = this._componentIdToSystems.get(componentId);
|
||||||
|
|
||||||
|
if (interestedSystems) {
|
||||||
|
for (const system of interestedSystems) {
|
||||||
|
system.handleEntityComponentChanged(entity);
|
||||||
|
notifiedSystems.add(system);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 通知全局监听系统(none条件、tag/name查询等) | Notify global listener systems
|
||||||
|
for (const system of this._globalNotifySystems) {
|
||||||
|
if (!notifiedSystems.has(system)) {
|
||||||
|
system.handleEntityComponentChanged(entity);
|
||||||
|
notifiedSystems.add(system);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没有提供组件类型,回退到遍历所有系统 | Fallback to all systems if no component type
|
||||||
|
if (!changedComponentType) {
|
||||||
|
for (const system of this.systems) {
|
||||||
|
if (!notifiedSystems.has(system)) {
|
||||||
|
system.handleEntityComponentChanged(entity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将系统添加到组件索引
|
||||||
|
*
|
||||||
|
* 根据系统的 Matcher 条件,将系统注册到相应的组件ID索引中。
|
||||||
|
*
|
||||||
|
* Index a system by its interested component types.
|
||||||
|
* Registers the system to component ID indices based on its Matcher conditions.
|
||||||
|
*
|
||||||
|
* @param system 要索引的系统 | The system to index
|
||||||
|
*/
|
||||||
|
private indexSystemByComponents(system: EntitySystem): void {
|
||||||
|
const matcher = system.matcher;
|
||||||
|
if (!matcher) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// nothing 匹配器不需要索引 | Nothing matcher doesn't need indexing
|
||||||
|
if (matcher.isNothing()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const condition = matcher.getCondition();
|
||||||
|
|
||||||
|
// 有 none/tag/name 条件的系统加入全局通知 | Systems with none/tag/name go to global
|
||||||
|
if (condition.none.length > 0 || condition.tag !== undefined || condition.name !== undefined) {
|
||||||
|
this._globalNotifySystems.add(system);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 空匹配器(匹配所有实体)加入全局通知 | Empty matcher (matches all) goes to global
|
||||||
|
if (matcher.isEmpty()) {
|
||||||
|
this._globalNotifySystems.add(system);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 索引 all 条件中的组件 | Index components in all condition
|
||||||
|
for (const componentType of condition.all) {
|
||||||
|
this.addSystemToComponentIndex(componentType, system);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 索引 any 条件中的组件 | Index components in any condition
|
||||||
|
for (const componentType of condition.any) {
|
||||||
|
this.addSystemToComponentIndex(componentType, system);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 索引单组件查询 | Index single component query
|
||||||
|
if (condition.component) {
|
||||||
|
this.addSystemToComponentIndex(condition.component, system);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将系统添加到指定组件的索引
|
||||||
|
*
|
||||||
|
* Add system to the index for a specific component type.
|
||||||
|
*
|
||||||
|
* @param componentType 组件类型 | Component type
|
||||||
|
* @param system 系统 | System
|
||||||
|
*/
|
||||||
|
private addSystemToComponentIndex(componentType: ComponentType, system: EntitySystem): void {
|
||||||
|
if (!ComponentRegistry.isRegistered(componentType)) {
|
||||||
|
ComponentRegistry.register(componentType);
|
||||||
|
}
|
||||||
|
|
||||||
|
const componentId = ComponentRegistry.getBitIndex(componentType);
|
||||||
|
let systems = this._componentIdToSystems.get(componentId);
|
||||||
|
|
||||||
|
if (!systems) {
|
||||||
|
systems = new Set();
|
||||||
|
this._componentIdToSystems.set(componentId, systems);
|
||||||
|
}
|
||||||
|
|
||||||
|
systems.add(system);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从组件索引中移除系统
|
||||||
|
*
|
||||||
|
* Remove a system from all component indices.
|
||||||
|
*
|
||||||
|
* @param system 要移除的系统 | The system to remove
|
||||||
|
*/
|
||||||
|
private removeSystemFromIndex(system: EntitySystem): void {
|
||||||
|
// 从全局通知列表移除 | Remove from global notify list
|
||||||
|
this._globalNotifySystems.delete(system);
|
||||||
|
|
||||||
|
// 从所有组件索引中移除 | Remove from all component indices
|
||||||
|
for (const systems of this._componentIdToSystems.values()) {
|
||||||
|
systems.delete(system);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 在场景的实体列表中添加一个实体
|
* 在场景的实体列表中添加一个实体
|
||||||
* @param entity 要添加的实体
|
* @param entity 要添加的实体
|
||||||
@@ -738,6 +906,9 @@ export class Scene implements IScene {
|
|||||||
// 标记系统列表已变化
|
// 标记系统列表已变化
|
||||||
this.markSystemsOrderDirty();
|
this.markSystemsOrderDirty();
|
||||||
|
|
||||||
|
// 建立组件类型到系统的索引 | Build component type to system index
|
||||||
|
this.indexSystemByComponents(system);
|
||||||
|
|
||||||
injectProperties(system, this._services);
|
injectProperties(system, this._services);
|
||||||
|
|
||||||
// 调试模式下自动包装系统方法以收集性能数据(ProfilerSDK 启用时表示调试模式)
|
// 调试模式下自动包装系统方法以收集性能数据(ProfilerSDK 启用时表示调试模式)
|
||||||
@@ -822,6 +993,9 @@ export class Scene implements IScene {
|
|||||||
// 标记系统列表已变化
|
// 标记系统列表已变化
|
||||||
this.markSystemsOrderDirty();
|
this.markSystemsOrderDirty();
|
||||||
|
|
||||||
|
// 从组件类型索引中移除 | Remove from component type index
|
||||||
|
this.removeSystemFromIndex(processor);
|
||||||
|
|
||||||
// 重置System状态
|
// 重置System状态
|
||||||
processor.reset();
|
processor.reset();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -235,7 +235,7 @@ export abstract class EntitySystem implements ISystemBase, IService {
|
|||||||
* 在系统创建时调用。框架内部使用,用户不应直接调用。
|
* 在系统创建时调用。框架内部使用,用户不应直接调用。
|
||||||
*/
|
*/
|
||||||
public initialize(): void {
|
public initialize(): void {
|
||||||
// 防止重复初始化
|
// 防止重复初始化 | Prevent re-initialization
|
||||||
if (this._initialized) {
|
if (this._initialized) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -243,13 +243,20 @@ export abstract class EntitySystem implements ISystemBase, IService {
|
|||||||
this._initialized = true;
|
this._initialized = true;
|
||||||
|
|
||||||
// 框架内部初始化:触发一次实体查询,以便正确跟踪现有实体
|
// 框架内部初始化:触发一次实体查询,以便正确跟踪现有实体
|
||||||
|
// Framework initialization: query entities once to track existing entities
|
||||||
if (this.scene) {
|
if (this.scene) {
|
||||||
// 清理缓存确保初始化时重新查询
|
// 清理缓存确保初始化时重新查询 | Clear cache to ensure fresh query
|
||||||
this._entityCache.invalidate();
|
this._entityCache.invalidate();
|
||||||
this.queryEntities();
|
const entities = this.queryEntities();
|
||||||
|
|
||||||
|
// 初始化时对已存在的匹配实体触发 onAdded
|
||||||
|
// Trigger onAdded for existing matching entities during initialization
|
||||||
|
for (const entity of entities) {
|
||||||
|
this.onAdded(entity);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 调用用户可重写的初始化方法
|
// 调用用户可重写的初始化方法 | Call user-overridable initialization method
|
||||||
this.onInitialize();
|
this.onInitialize();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -718,32 +725,151 @@ export abstract class EntitySystem implements ISystemBase, IService {
|
|||||||
return `${this._systemName}[${entityCount} entities]${perfInfo}`;
|
return `${this._systemName}[${entityCount} entities]${perfInfo}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查实体是否匹配当前系统的查询条件
|
||||||
|
* Check if an entity matches this system's query condition
|
||||||
|
*
|
||||||
|
* @param entity 要检查的实体 / The entity to check
|
||||||
|
* @returns 是否匹配 / Whether the entity matches
|
||||||
|
*/
|
||||||
|
public matchesEntity(entity: Entity): boolean {
|
||||||
|
if (!this._matcher) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// nothing 匹配器不匹配任何实体
|
||||||
|
if (this._matcher.isNothing()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 空匹配器匹配所有实体
|
||||||
|
if (this._matcher.isEmpty()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const condition = this._matcher.getCondition();
|
||||||
|
|
||||||
|
// 检查 all 条件
|
||||||
|
for (const componentType of condition.all) {
|
||||||
|
if (!entity.hasComponent(componentType)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查 any 条件
|
||||||
|
if (condition.any.length > 0) {
|
||||||
|
let hasAny = false;
|
||||||
|
for (const componentType of condition.any) {
|
||||||
|
if (entity.hasComponent(componentType)) {
|
||||||
|
hasAny = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!hasAny) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查 none 条件
|
||||||
|
for (const componentType of condition.none) {
|
||||||
|
if (entity.hasComponent(componentType)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查 tag 条件
|
||||||
|
if (condition.tag !== undefined && entity.tag !== condition.tag) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查 name 条件
|
||||||
|
if (condition.name !== undefined && entity.name !== condition.name) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查单组件条件
|
||||||
|
if (condition.component !== undefined && !entity.hasComponent(condition.component)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查实体是否正在被此系统跟踪
|
||||||
|
* Check if an entity is being tracked by this system
|
||||||
|
*
|
||||||
|
* @param entity 要检查的实体 / The entity to check
|
||||||
|
* @returns 是否正在跟踪 / Whether the entity is being tracked
|
||||||
|
*/
|
||||||
|
public isTracking(entity: Entity): boolean {
|
||||||
|
return this._entityCache.isTracked(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当实体的组件发生变化时由 Scene 调用
|
||||||
|
*
|
||||||
|
* 立即检查实体是否匹配并触发 onAdded/onRemoved 回调。
|
||||||
|
* 这是事件驱动设计的核心:组件变化时立即通知相关系统。
|
||||||
|
*
|
||||||
|
* Called by Scene when an entity's components change.
|
||||||
|
* Immediately checks if the entity matches and triggers onAdded/onRemoved callbacks.
|
||||||
|
* This is the core of event-driven design: notify relevant systems immediately when components change.
|
||||||
|
*
|
||||||
|
* @param entity 组件发生变化的实体 / The entity whose components changed
|
||||||
|
* @internal 由 Scene.notifyEntityComponentChanged 调用 / Called by Scene.notifyEntityComponentChanged
|
||||||
|
*/
|
||||||
|
public handleEntityComponentChanged(entity: Entity): void {
|
||||||
|
if (!this._matcher || !this._enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const wasTracked = this._entityCache.isTracked(entity);
|
||||||
|
const nowMatches = this.matchesEntity(entity);
|
||||||
|
|
||||||
|
if (!wasTracked && nowMatches) {
|
||||||
|
// 新匹配:添加跟踪并触发 onAdded | New match: add tracking and trigger onAdded
|
||||||
|
this._entityCache.addTracked(entity);
|
||||||
|
this._entityCache.invalidate();
|
||||||
|
this.onAdded(entity);
|
||||||
|
} else if (wasTracked && !nowMatches) {
|
||||||
|
// 不再匹配:移除跟踪并触发 onRemoved | No longer matches: remove tracking and trigger onRemoved
|
||||||
|
this._entityCache.removeTracked(entity);
|
||||||
|
this._entityCache.invalidate();
|
||||||
|
this.onRemoved(entity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 更新实体跟踪,检查新增和移除的实体
|
* 更新实体跟踪,检查新增和移除的实体
|
||||||
|
*
|
||||||
|
* 由于采用了事件驱动设计,运行时的 onAdded/onRemoved 已在 handleEntityComponentChanged 中
|
||||||
|
* 立即触发。此方法不再触发回调,只同步跟踪状态。
|
||||||
|
*
|
||||||
|
* With event-driven design, runtime onAdded/onRemoved are triggered immediately in
|
||||||
|
* handleEntityComponentChanged. This method no longer triggers callbacks, only syncs tracking state.
|
||||||
*/
|
*/
|
||||||
private updateEntityTracking(currentEntities: readonly Entity[]): void {
|
private updateEntityTracking(currentEntities: readonly Entity[]): void {
|
||||||
const currentSet = new Set(currentEntities);
|
const currentSet = new Set(currentEntities);
|
||||||
let hasChanged = false;
|
let hasChanged = false;
|
||||||
|
|
||||||
// 检查新增的实体
|
// 检查新增的实体 | Check for newly added entities
|
||||||
for (const entity of currentEntities) {
|
for (const entity of currentEntities) {
|
||||||
if (!this._entityCache.isTracked(entity)) {
|
if (!this._entityCache.isTracked(entity)) {
|
||||||
this._entityCache.addTracked(entity);
|
this._entityCache.addTracked(entity);
|
||||||
this.onAdded(entity);
|
|
||||||
hasChanged = true;
|
hasChanged = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查移除的实体
|
// 检查移除的实体 | Check for removed entities
|
||||||
for (const entity of this._entityCache.getTracked()) {
|
for (const entity of this._entityCache.getTracked()) {
|
||||||
if (!currentSet.has(entity)) {
|
if (!currentSet.has(entity)) {
|
||||||
this._entityCache.removeTracked(entity);
|
this._entityCache.removeTracked(entity);
|
||||||
this.onRemoved(entity);
|
|
||||||
hasChanged = true;
|
hasChanged = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果实体发生了变化,使缓存失效
|
// 如果实体发生了变化,使缓存失效 | If entities changed, invalidate cache
|
||||||
if (hasChanged) {
|
if (hasChanged) {
|
||||||
this._entityCache.invalidate();
|
this._entityCache.invalidate();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -439,6 +439,215 @@ describe('EntitySystem', () => {
|
|||||||
|
|
||||||
scene.removeSystem(trackingSystem);
|
scene.removeSystem(trackingSystem);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('在系统 process 中添加组件时应立即触发其他系统的 onAdded', () => {
|
||||||
|
// 使用独立的场景,避免 beforeEach 创建的实体干扰
|
||||||
|
// Use independent scene to avoid interference from beforeEach entities
|
||||||
|
const testScene = new Scene();
|
||||||
|
|
||||||
|
// 组件定义
|
||||||
|
class TagComponent extends TestComponent {}
|
||||||
|
|
||||||
|
// SystemA: 匹配 TestComponent + TagComponent
|
||||||
|
class SystemA extends EntitySystem {
|
||||||
|
public onAddedEntities: Entity[] = [];
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super(Matcher.all(TestComponent, TagComponent));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override onAdded(entity: Entity): void {
|
||||||
|
this.onAddedEntities.push(entity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TriggerSystem: 在 process 中添加 TagComponent
|
||||||
|
class TriggerSystem extends EntitySystem {
|
||||||
|
constructor() {
|
||||||
|
super(Matcher.all(TestComponent));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override process(entities: readonly Entity[]): void {
|
||||||
|
for (const entity of entities) {
|
||||||
|
if (!entity.hasComponent(TagComponent)) {
|
||||||
|
entity.addComponent(new TagComponent());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const systemA = new SystemA();
|
||||||
|
const triggerSystem = new TriggerSystem();
|
||||||
|
|
||||||
|
// 注意:SystemA 先注册,TriggerSystem 后注册
|
||||||
|
// 事件驱动设计确保即使 SystemA 已执行完毕,也能收到 onAdded 通知
|
||||||
|
testScene.addSystem(systemA);
|
||||||
|
testScene.addSystem(triggerSystem);
|
||||||
|
|
||||||
|
// 创建实体(已有 TestComponent)
|
||||||
|
const testEntity = testScene.createEntity('test');
|
||||||
|
testEntity.addComponent(new TestComponent());
|
||||||
|
|
||||||
|
// 执行一帧:TriggerSystem 会添加 TagComponent,SystemA 应立即收到 onAdded
|
||||||
|
testScene.update();
|
||||||
|
|
||||||
|
expect(systemA.onAddedEntities.length).toBe(1);
|
||||||
|
expect(systemA.onAddedEntities[0]).toBe(testEntity);
|
||||||
|
|
||||||
|
testScene.removeSystem(systemA);
|
||||||
|
testScene.removeSystem(triggerSystem);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('同一帧内添加后移除组件,onAdded 和 onRemoved 都应触发', () => {
|
||||||
|
// 使用独立的场景,避免 beforeEach 创建的实体干扰
|
||||||
|
// Use independent scene to avoid interference from beforeEach entities
|
||||||
|
const testScene = new Scene();
|
||||||
|
|
||||||
|
class TagComponent extends TestComponent {}
|
||||||
|
|
||||||
|
class TrackingSystemWithTag extends EntitySystem {
|
||||||
|
public onAddedEntities: Entity[] = [];
|
||||||
|
public onRemovedEntities: Entity[] = [];
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super(Matcher.all(TestComponent, TagComponent));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override onAdded(entity: Entity): void {
|
||||||
|
this.onAddedEntities.push(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override onRemoved(entity: Entity): void {
|
||||||
|
this.onRemovedEntities.push(entity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddSystem: 在 process 中添加 TagComponent
|
||||||
|
class AddSystem extends EntitySystem {
|
||||||
|
constructor() {
|
||||||
|
super(Matcher.all(TestComponent));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override process(entities: readonly Entity[]): void {
|
||||||
|
for (const entity of entities) {
|
||||||
|
if (!entity.hasComponent(TagComponent)) {
|
||||||
|
entity.addComponent(new TagComponent());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveSystem: 在 lateProcess 中移除 TagComponent
|
||||||
|
class RemoveSystem extends EntitySystem {
|
||||||
|
constructor() {
|
||||||
|
super(Matcher.all(TagComponent));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override lateProcess(entities: readonly Entity[]): void {
|
||||||
|
for (const entity of entities) {
|
||||||
|
const tag = entity.getComponent(TagComponent);
|
||||||
|
if (tag) {
|
||||||
|
entity.removeComponent(tag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const trackingSystem = new TrackingSystemWithTag();
|
||||||
|
const addSystem = new AddSystem();
|
||||||
|
const removeSystem = new RemoveSystem();
|
||||||
|
|
||||||
|
testScene.addSystem(trackingSystem);
|
||||||
|
testScene.addSystem(addSystem);
|
||||||
|
testScene.addSystem(removeSystem);
|
||||||
|
|
||||||
|
const testEntity = testScene.createEntity('test');
|
||||||
|
testEntity.addComponent(new TestComponent());
|
||||||
|
|
||||||
|
// 执行一帧
|
||||||
|
testScene.update();
|
||||||
|
|
||||||
|
// AddSystem 添加了 TagComponent,RemoveSystem 在 lateProcess 中移除
|
||||||
|
expect(testEntity.hasComponent(TagComponent)).toBe(false);
|
||||||
|
|
||||||
|
// 事件驱动:onAdded 应该在组件添加时立即触发
|
||||||
|
expect(trackingSystem.onAddedEntities.length).toBe(1);
|
||||||
|
// onRemoved 应该在组件移除时立即触发
|
||||||
|
expect(trackingSystem.onRemovedEntities.length).toBe(1);
|
||||||
|
|
||||||
|
testScene.removeSystem(trackingSystem);
|
||||||
|
testScene.removeSystem(addSystem);
|
||||||
|
testScene.removeSystem(removeSystem);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('多个系统监听同一组件变化时都应收到 onAdded 通知', () => {
|
||||||
|
// 使用独立的场景,避免 beforeEach 创建的实体干扰
|
||||||
|
// Use independent scene to avoid interference from beforeEach entities
|
||||||
|
const testScene = new Scene();
|
||||||
|
|
||||||
|
// 使用独立的组件类,避免继承带来的问题
|
||||||
|
// Use independent component class to avoid inheritance issues
|
||||||
|
class TagComponent2 extends Component {}
|
||||||
|
|
||||||
|
class SystemA extends EntitySystem {
|
||||||
|
public onAddedEntities: Entity[] = [];
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super(Matcher.all(TestComponent, TagComponent2));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override onAdded(entity: Entity): void {
|
||||||
|
this.onAddedEntities.push(entity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SystemB extends EntitySystem {
|
||||||
|
public onAddedEntities: Entity[] = [];
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super(Matcher.all(TestComponent, TagComponent2));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override onAdded(entity: Entity): void {
|
||||||
|
this.onAddedEntities.push(entity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TriggerSystem extends EntitySystem {
|
||||||
|
constructor() {
|
||||||
|
super(Matcher.all(TestComponent));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override process(entities: readonly Entity[]): void {
|
||||||
|
for (const entity of entities) {
|
||||||
|
if (!entity.hasComponent(TagComponent2)) {
|
||||||
|
entity.addComponent(new TagComponent2());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const systemA = new SystemA();
|
||||||
|
const systemB = new SystemB();
|
||||||
|
const triggerSystem = new TriggerSystem();
|
||||||
|
|
||||||
|
testScene.addSystem(systemA);
|
||||||
|
testScene.addSystem(systemB);
|
||||||
|
testScene.addSystem(triggerSystem);
|
||||||
|
|
||||||
|
const testEntity = testScene.createEntity('test');
|
||||||
|
testEntity.addComponent(new TestComponent());
|
||||||
|
|
||||||
|
testScene.update();
|
||||||
|
|
||||||
|
// 两个系统都应收到 onAdded 通知
|
||||||
|
expect(systemA.onAddedEntities.length).toBe(1);
|
||||||
|
expect(systemB.onAddedEntities.length).toBe(1);
|
||||||
|
|
||||||
|
testScene.removeSystem(systemA);
|
||||||
|
testScene.removeSystem(systemB);
|
||||||
|
testScene.removeSystem(triggerSystem);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('reset 方法', () => {
|
describe('reset 方法', () => {
|
||||||
|
|||||||
@@ -37,6 +37,25 @@ const filesToBundle = [
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Type definition files for IDE intellisense
|
||||||
|
// 用于 IDE 智能感知的类型定义文件
|
||||||
|
const typesDir = path.join(bundleDir, 'types');
|
||||||
|
if (!fs.existsSync(typesDir)) {
|
||||||
|
fs.mkdirSync(typesDir, { recursive: true });
|
||||||
|
console.log(`Created types directory: ${typesDir}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const typeFilesToBundle = [
|
||||||
|
{
|
||||||
|
src: path.join(rootPath, 'packages/core/dist/index.d.ts'),
|
||||||
|
dst: path.join(typesDir, 'ecs-framework.d.ts')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: path.join(rootPath, 'packages/engine-core/dist/index.d.ts'),
|
||||||
|
dst: path.join(typesDir, 'engine-core.d.ts')
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
// Copy files
|
// Copy files
|
||||||
let success = true;
|
let success = true;
|
||||||
for (const { src, dst } of filesToBundle) {
|
for (const { src, dst } of filesToBundle) {
|
||||||
@@ -59,6 +78,24 @@ for (const { src, dst } of filesToBundle) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Copy type definition files (optional - don't fail if not found)
|
||||||
|
// 复制类型定义文件(可选 - 找不到不报错)
|
||||||
|
for (const { src, dst } of typeFilesToBundle) {
|
||||||
|
try {
|
||||||
|
if (!fs.existsSync(src)) {
|
||||||
|
console.warn(`Type definition not found: ${src}`);
|
||||||
|
console.log(' Build packages first: pnpm --filter @esengine/core build');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.copyFileSync(src, dst);
|
||||||
|
const stats = fs.statSync(dst);
|
||||||
|
console.log(`✓ Bundled type definition ${path.basename(dst)} (${(stats.size / 1024).toFixed(2)} KB)`);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`Failed to bundle type definition ${path.basename(src)}: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Update tauri.conf.json to include runtime directory
|
// Update tauri.conf.json to include runtime directory
|
||||||
if (success) {
|
if (success) {
|
||||||
const tauriConfigPath = path.join(editorPath, 'src-tauri', 'tauri.conf.json');
|
const tauriConfigPath = path.join(editorPath, 'src-tauri', 'tauri.conf.json');
|
||||||
|
|||||||
@@ -75,10 +75,35 @@ pub fn open_file_with_default_app(file_path: String) -> Result<(), String> {
|
|||||||
/// Show file in system file explorer
|
/// Show file in system file explorer
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn show_in_folder(file_path: String) -> Result<(), String> {
|
pub fn show_in_folder(file_path: String) -> Result<(), String> {
|
||||||
|
println!("[show_in_folder] Received path: {}", file_path);
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
{
|
{
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
// Normalize path separators for Windows
|
||||||
|
// 规范化路径分隔符
|
||||||
|
let normalized_path = file_path.replace('/', "\\");
|
||||||
|
println!("[show_in_folder] Normalized path: {}", normalized_path);
|
||||||
|
|
||||||
|
// Verify the path exists before trying to show it
|
||||||
|
// 验证路径存在
|
||||||
|
let path = Path::new(&normalized_path);
|
||||||
|
let exists = path.exists();
|
||||||
|
println!("[show_in_folder] Path exists: {}", exists);
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
return Err(format!("Path does not exist: {}", normalized_path));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Windows explorer requires /select, to be concatenated with the path
|
||||||
|
// without spaces. Use a single argument to avoid shell parsing issues.
|
||||||
|
// Windows 资源管理器要求 /select, 与路径连接在一起,中间没有空格
|
||||||
|
let select_arg = format!("/select,{}", normalized_path);
|
||||||
|
println!("[show_in_folder] Explorer arg: {}", select_arg);
|
||||||
|
|
||||||
Command::new("explorer")
|
Command::new("explorer")
|
||||||
.args(["/select,", &file_path])
|
.arg(&select_arg)
|
||||||
.spawn()
|
.spawn()
|
||||||
.map_err(|e| format!("Failed to show in folder: {}", e))?;
|
.map_err(|e| format!("Failed to show in folder: {}", e))?;
|
||||||
}
|
}
|
||||||
@@ -117,6 +142,55 @@ pub fn get_temp_dir() -> Result<String, String> {
|
|||||||
.ok_or_else(|| "Failed to get temp directory".to_string())
|
.ok_or_else(|| "Failed to get temp directory".to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Open project folder with specified editor
|
||||||
|
/// 使用指定编辑器打开项目文件夹
|
||||||
|
///
|
||||||
|
/// @param project_path - Project folder path | 项目文件夹路径
|
||||||
|
/// @param editor_command - Editor command (e.g., "code", "cursor") | 编辑器命令
|
||||||
|
/// @param file_path - Optional file to open (will be opened in the editor) | 可选的要打开的文件
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn open_with_editor(
|
||||||
|
project_path: String,
|
||||||
|
editor_command: String,
|
||||||
|
file_path: Option<String>,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
// Normalize paths
|
||||||
|
let normalized_project = project_path.replace('/', "\\");
|
||||||
|
let normalized_file = file_path.map(|f| f.replace('/', "\\"));
|
||||||
|
|
||||||
|
// Verify project path exists
|
||||||
|
let project = Path::new(&normalized_project);
|
||||||
|
if !project.exists() {
|
||||||
|
return Err(format!("Project path does not exist: {}", normalized_project));
|
||||||
|
}
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"[open_with_editor] editor: {}, project: {}, file: {:?}",
|
||||||
|
editor_command, normalized_project, normalized_file
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut cmd = Command::new(&editor_command);
|
||||||
|
|
||||||
|
// Add project folder as first argument
|
||||||
|
cmd.arg(&normalized_project);
|
||||||
|
|
||||||
|
// If a specific file is provided, add it as a second argument
|
||||||
|
// Most editors support: editor <folder> <file>
|
||||||
|
if let Some(ref file) = normalized_file {
|
||||||
|
let file_path = Path::new(file);
|
||||||
|
if file_path.exists() {
|
||||||
|
cmd.arg(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.spawn()
|
||||||
|
.map_err(|e| format!("Failed to open with editor '{}': {}", editor_command, e))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Get application resource directory
|
/// Get application resource directory
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn get_app_resource_dir(app: AppHandle) -> Result<String, String> {
|
pub fn get_app_resource_dir(app: AppHandle) -> Result<String, String> {
|
||||||
@@ -138,6 +212,97 @@ pub fn get_current_dir() -> Result<String, String> {
|
|||||||
.map_err(|e| format!("Failed to get current directory: {}", e))
|
.map_err(|e| format!("Failed to get current directory: {}", e))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Copy type definitions to project for IDE intellisense
|
||||||
|
/// 复制类型定义文件到项目以支持 IDE 智能感知
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn copy_type_definitions(app: AppHandle, project_path: String) -> Result<(), String> {
|
||||||
|
use std::fs;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
let project = Path::new(&project_path);
|
||||||
|
if !project.exists() {
|
||||||
|
return Err(format!("Project path does not exist: {}", project_path));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create types directory in project
|
||||||
|
// 在项目中创建 types 目录
|
||||||
|
let types_dir = project.join("types");
|
||||||
|
if !types_dir.exists() {
|
||||||
|
fs::create_dir_all(&types_dir)
|
||||||
|
.map_err(|e| format!("Failed to create types directory: {}", e))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get resource directory (where bundled files are)
|
||||||
|
// 获取资源目录(打包文件所在位置)
|
||||||
|
let resource_dir = app.path()
|
||||||
|
.resource_dir()
|
||||||
|
.map_err(|e| format!("Failed to get resource directory: {}", e))?;
|
||||||
|
|
||||||
|
// Type definition files to copy
|
||||||
|
// 要复制的类型定义文件
|
||||||
|
// Format: (resource_path, workspace_path, dest_name)
|
||||||
|
// 格式:(资源路径,工作区路径,目标文件名)
|
||||||
|
// Note: resource_path is relative to Tauri resource dir (runtime/ is mapped to .)
|
||||||
|
// 注意:resource_path 相对于 Tauri 资源目录(runtime/ 映射到 .)
|
||||||
|
let type_files = [
|
||||||
|
("types/ecs-framework.d.ts", "packages/core/dist/index.d.ts", "ecs-framework.d.ts"),
|
||||||
|
("types/engine-core.d.ts", "packages/engine-core/dist/index.d.ts", "engine-core.d.ts"),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Try to find workspace root (for development mode)
|
||||||
|
// 尝试查找工作区根目录(用于开发模式)
|
||||||
|
let workspace_root = std::env::current_dir()
|
||||||
|
.ok()
|
||||||
|
.and_then(|cwd| {
|
||||||
|
// Look for pnpm-workspace.yaml or package.json in parent directories
|
||||||
|
// 在父目录中查找 pnpm-workspace.yaml 或 package.json
|
||||||
|
let mut dir = cwd.as_path();
|
||||||
|
loop {
|
||||||
|
if dir.join("pnpm-workspace.yaml").exists() {
|
||||||
|
return Some(dir.to_path_buf());
|
||||||
|
}
|
||||||
|
match dir.parent() {
|
||||||
|
Some(parent) => dir = parent,
|
||||||
|
None => return None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut copied_count = 0;
|
||||||
|
for (resource_relative, workspace_relative, dest_name) in type_files {
|
||||||
|
let dest_path = types_dir.join(dest_name);
|
||||||
|
|
||||||
|
// Try resource directory first (production mode)
|
||||||
|
// 首先尝试资源目录(生产模式)
|
||||||
|
let src_path = resource_dir.join(resource_relative);
|
||||||
|
if src_path.exists() {
|
||||||
|
fs::copy(&src_path, &dest_path)
|
||||||
|
.map_err(|e| format!("Failed to copy {}: {}", resource_relative, e))?;
|
||||||
|
println!("[copy_type_definitions] Copied {} to {}", src_path.display(), dest_path.display());
|
||||||
|
copied_count += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try workspace directory (development mode)
|
||||||
|
// 尝试工作区目录(开发模式)
|
||||||
|
if let Some(ref ws_root) = workspace_root {
|
||||||
|
let ws_src_path = ws_root.join(workspace_relative);
|
||||||
|
if ws_src_path.exists() {
|
||||||
|
fs::copy(&ws_src_path, &dest_path)
|
||||||
|
.map_err(|e| format!("Failed to copy {}: {}", workspace_relative, e))?;
|
||||||
|
println!("[copy_type_definitions] Copied {} to {} (dev mode)", ws_src_path.display(), dest_path.display());
|
||||||
|
copied_count += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("[copy_type_definitions] {} not found, skipping", dest_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("[copy_type_definitions] Copied {} type definition files to {}", copied_count, types_dir.display());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Start a local HTTP server for runtime preview
|
/// Start a local HTTP server for runtime preview
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn start_local_server(root_path: String, port: u16) -> Result<String, String> {
|
pub fn start_local_server(root_path: String, port: u16) -> Result<String, String> {
|
||||||
|
|||||||
@@ -81,6 +81,8 @@ fn main() {
|
|||||||
commands::open_file_with_default_app,
|
commands::open_file_with_default_app,
|
||||||
commands::show_in_folder,
|
commands::show_in_folder,
|
||||||
commands::get_temp_dir,
|
commands::get_temp_dir,
|
||||||
|
commands::open_with_editor,
|
||||||
|
commands::copy_type_definitions,
|
||||||
commands::get_app_resource_dir,
|
commands::get_app_resource_dir,
|
||||||
commands::get_current_dir,
|
commands::get_current_dir,
|
||||||
commands::start_local_server,
|
commands::start_local_server,
|
||||||
|
|||||||
@@ -381,6 +381,14 @@ function App() {
|
|||||||
// 设置 Tauri project:// 协议的基础路径(用于加载插件等项目文件)
|
// 设置 Tauri project:// 协议的基础路径(用于加载插件等项目文件)
|
||||||
await TauriAPI.setProjectBasePath(projectPath);
|
await TauriAPI.setProjectBasePath(projectPath);
|
||||||
|
|
||||||
|
// 复制类型定义到项目,用于 IDE 智能感知
|
||||||
|
// Copy type definitions to project for IDE intellisense
|
||||||
|
try {
|
||||||
|
await TauriAPI.copyTypeDefinitions(projectPath);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[App] Failed to copy type definitions:', e);
|
||||||
|
}
|
||||||
|
|
||||||
const settings = SettingsService.getInstance();
|
const settings = SettingsService.getInstance();
|
||||||
settings.addRecentProject(projectPath);
|
settings.addRecentProject(projectPath);
|
||||||
|
|
||||||
@@ -465,7 +473,9 @@ function App() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleCreateProjectFromWizard = async (projectName: string, projectPath: string, _templateId: string) => {
|
const handleCreateProjectFromWizard = async (projectName: string, projectPath: string, _templateId: string) => {
|
||||||
const fullProjectPath = `${projectPath}\\${projectName}`;
|
// 使用与 projectPath 相同的路径分隔符 | Use same separator as projectPath
|
||||||
|
const sep = projectPath.includes('/') ? '/' : '\\';
|
||||||
|
const fullProjectPath = `${projectPath}${sep}${projectName}`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
@@ -631,6 +641,13 @@ function App() {
|
|||||||
await pluginLoader.unloadProjectPlugins(pluginManager);
|
await pluginLoader.unloadProjectPlugins(pluginManager);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 清理场景(会清理所有实体和系统)
|
||||||
|
// Clear scene (clears all entities and systems)
|
||||||
|
const scene = Core.scene;
|
||||||
|
if (scene) {
|
||||||
|
scene.end();
|
||||||
|
}
|
||||||
|
|
||||||
// 清理模块系统
|
// 清理模块系统
|
||||||
const engineService = EngineService.getInstance();
|
const engineService = EngineService.getInstance();
|
||||||
engineService.clearModuleSystems();
|
engineService.clearModuleSystems();
|
||||||
@@ -817,6 +834,28 @@ function App() {
|
|||||||
onOpenProject={handleOpenProject}
|
onOpenProject={handleOpenProject}
|
||||||
onCreateProject={handleCreateProject}
|
onCreateProject={handleCreateProject}
|
||||||
onOpenRecentProject={handleOpenRecentProject}
|
onOpenRecentProject={handleOpenRecentProject}
|
||||||
|
onRemoveRecentProject={(projectPath) => {
|
||||||
|
settings.removeRecentProject(projectPath);
|
||||||
|
// 强制重新渲染 | Force re-render
|
||||||
|
setStatus(t('header.status.ready'));
|
||||||
|
}}
|
||||||
|
onDeleteProject={async (projectPath) => {
|
||||||
|
try {
|
||||||
|
await TauriAPI.deleteFolder(projectPath);
|
||||||
|
// 删除成功后从列表中移除并触发重新渲染
|
||||||
|
// Remove from list and trigger re-render after successful deletion
|
||||||
|
settings.removeRecentProject(projectPath);
|
||||||
|
setStatus(t('header.status.ready'));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to delete project:', error);
|
||||||
|
setErrorDialog({
|
||||||
|
title: locale === 'zh' ? '删除项目失败' : 'Failed to Delete Project',
|
||||||
|
message: locale === 'zh'
|
||||||
|
? `无法删除项目:\n${error instanceof Error ? error.message : String(error)}`
|
||||||
|
: `Failed to delete project:\n${error instanceof Error ? error.message : String(error)}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
onLocaleChange={handleLocaleChange}
|
onLocaleChange={handleLocaleChange}
|
||||||
recentProjects={recentProjects}
|
recentProjects={recentProjects}
|
||||||
locale={locale}
|
locale={locale}
|
||||||
|
|||||||
@@ -168,6 +168,26 @@ export class TauriAPI {
|
|||||||
await invoke('show_in_folder', { filePath: path });
|
await invoke('show_in_folder', { filePath: path });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用指定编辑器打开项目
|
||||||
|
* Open project with specified editor
|
||||||
|
*
|
||||||
|
* @param projectPath 项目文件夹路径 | Project folder path
|
||||||
|
* @param editorCommand 编辑器命令(如 "code", "cursor")| Editor command
|
||||||
|
* @param filePath 可选的要打开的文件路径 | Optional file path to open
|
||||||
|
*/
|
||||||
|
static async openWithEditor(
|
||||||
|
projectPath: string,
|
||||||
|
editorCommand: string,
|
||||||
|
filePath?: string
|
||||||
|
): Promise<void> {
|
||||||
|
await invoke('open_with_editor', {
|
||||||
|
projectPath,
|
||||||
|
editorCommand,
|
||||||
|
filePath: filePath || null
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 打开行为树文件选择对话框
|
* 打开行为树文件选择对话框
|
||||||
* @returns 用户选择的文件路径,取消则返回 null
|
* @returns 用户选择的文件路径,取消则返回 null
|
||||||
@@ -311,6 +331,16 @@ export class TauriAPI {
|
|||||||
return await invoke<string>('generate_qrcode', { text });
|
return await invoke<string>('generate_qrcode', { text });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 复制类型定义文件到项目
|
||||||
|
* Copy type definition files to project for IDE intellisense
|
||||||
|
*
|
||||||
|
* @param projectPath 项目路径 | Project path
|
||||||
|
*/
|
||||||
|
static async copyTypeDefinitions(projectPath: string): Promise<void> {
|
||||||
|
return await invoke<void>('copy_type_definitions', { projectPath });
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 将本地文件路径转换为 Tauri 可访问的 asset URL
|
* 将本地文件路径转换为 Tauri 可访问的 asset URL
|
||||||
* @param filePath 本地文件路径
|
* @param filePath 本地文件路径
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ import {
|
|||||||
import { Core } from '@esengine/ecs-framework';
|
import { Core } from '@esengine/ecs-framework';
|
||||||
import { MessageHub, FileActionRegistry, type FileCreationTemplate } from '@esengine/editor-core';
|
import { MessageHub, FileActionRegistry, type FileCreationTemplate } from '@esengine/editor-core';
|
||||||
import { TauriAPI, DirectoryEntry } from '../api/tauri';
|
import { TauriAPI, DirectoryEntry } from '../api/tauri';
|
||||||
|
import { SettingsService } from '../services/SettingsService';
|
||||||
import { ContextMenu, ContextMenuItem } from './ContextMenu';
|
import { ContextMenu, ContextMenuItem } from './ContextMenu';
|
||||||
import { PromptDialog } from './PromptDialog';
|
import { PromptDialog } from './PromptDialog';
|
||||||
import '../styles/ContentBrowser.css';
|
import '../styles/ContentBrowser.css';
|
||||||
@@ -210,8 +211,124 @@ export function ContentBrowser({
|
|||||||
'Shader': { en: 'Shader', zh: '着色器' },
|
'Shader': { en: 'Shader', zh: '着色器' },
|
||||||
'Tilemap': { en: 'Tilemap', zh: '瓦片地图' },
|
'Tilemap': { en: 'Tilemap', zh: '瓦片地图' },
|
||||||
'Tileset': { en: 'Tileset', zh: '瓦片集' },
|
'Tileset': { en: 'Tileset', zh: '瓦片集' },
|
||||||
|
'Component': { en: 'Component', zh: '组件' },
|
||||||
|
'System': { en: 'System', zh: '系统' },
|
||||||
|
'TypeScript': { en: 'TypeScript', zh: 'TypeScript' },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 注册内置的 TypeScript 文件创建模板
|
||||||
|
// Register built-in TypeScript file creation templates
|
||||||
|
useEffect(() => {
|
||||||
|
if (!fileActionRegistry) return;
|
||||||
|
|
||||||
|
const builtinTemplates: FileCreationTemplate[] = [
|
||||||
|
{
|
||||||
|
id: 'ts-component',
|
||||||
|
label: 'Component',
|
||||||
|
extension: '.ts',
|
||||||
|
icon: 'FileCode',
|
||||||
|
category: 'Script',
|
||||||
|
getContent: (fileName: string) => {
|
||||||
|
const className = fileName.replace(/\.ts$/, '');
|
||||||
|
return `import { Component, ECSComponent, Property, Serialize, Serializable } from '@esengine/ecs-framework';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ${className}
|
||||||
|
*/
|
||||||
|
@ECSComponent('${className}')
|
||||||
|
@Serializable({ version: 1, typeId: '${className}' })
|
||||||
|
export class ${className} extends Component {
|
||||||
|
// 在这里添加组件属性
|
||||||
|
// Add component properties here
|
||||||
|
|
||||||
|
@Serialize()
|
||||||
|
@Property({ type: 'number', label: 'Example Property' })
|
||||||
|
public exampleProperty: number = 0;
|
||||||
|
|
||||||
|
onInitialize(): void {
|
||||||
|
// 组件初始化时调用
|
||||||
|
// Called when component is initialized
|
||||||
|
}
|
||||||
|
|
||||||
|
onDestroy(): void {
|
||||||
|
// 组件销毁时调用
|
||||||
|
// Called when component is destroyed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ts-system',
|
||||||
|
label: 'System',
|
||||||
|
extension: '.ts',
|
||||||
|
icon: 'FileCode',
|
||||||
|
category: 'Script',
|
||||||
|
getContent: (fileName: string) => {
|
||||||
|
const className = fileName.replace(/\.ts$/, '');
|
||||||
|
return `import { EntitySystem, Matcher, type Entity } from '@esengine/ecs-framework';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ${className}
|
||||||
|
*/
|
||||||
|
export class ${className} extends EntitySystem {
|
||||||
|
// 定义系统处理的组件类型
|
||||||
|
// Define component types this system processes
|
||||||
|
protected getMatcher(): Matcher {
|
||||||
|
// 返回匹配器,指定需要哪些组件
|
||||||
|
// Return matcher specifying required components
|
||||||
|
// return Matcher.all(SomeComponent);
|
||||||
|
return Matcher.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected updateEntity(entity: Entity, deltaTime: number): void {
|
||||||
|
// 处理每个实体
|
||||||
|
// Process each entity
|
||||||
|
}
|
||||||
|
|
||||||
|
// 可选:系统初始化
|
||||||
|
// Optional: System initialization
|
||||||
|
// onInitialize(): void {
|
||||||
|
// super.onInitialize();
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ts-script',
|
||||||
|
label: 'TypeScript',
|
||||||
|
extension: '.ts',
|
||||||
|
icon: 'FileCode',
|
||||||
|
category: 'Script',
|
||||||
|
getContent: (fileName: string) => {
|
||||||
|
const name = fileName.replace(/\.ts$/, '');
|
||||||
|
return `/**
|
||||||
|
* ${name}
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function ${name.charAt(0).toLowerCase() + name.slice(1)}(): void {
|
||||||
|
// 在这里编写代码
|
||||||
|
// Write your code here
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// 注册模板
|
||||||
|
for (const template of builtinTemplates) {
|
||||||
|
fileActionRegistry.registerCreationTemplate(template);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理函数
|
||||||
|
return () => {
|
||||||
|
for (const template of builtinTemplates) {
|
||||||
|
fileActionRegistry.unregisterCreationTemplate(template);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [fileActionRegistry]);
|
||||||
|
|
||||||
const getTemplateLabel = (label: string): string => {
|
const getTemplateLabel = (label: string): string => {
|
||||||
const mapping = templateLabels[label];
|
const mapping = templateLabels[label];
|
||||||
if (mapping) {
|
if (mapping) {
|
||||||
@@ -439,6 +556,24 @@ export function ContentBrowser({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 脚本文件使用配置的编辑器打开
|
||||||
|
// Open script files with configured editor
|
||||||
|
if (ext === 'ts' || ext === 'tsx' || ext === 'js' || ext === 'jsx') {
|
||||||
|
const settings = SettingsService.getInstance();
|
||||||
|
const editorCommand = settings.getScriptEditorCommand();
|
||||||
|
|
||||||
|
if (editorCommand && projectPath) {
|
||||||
|
try {
|
||||||
|
await TauriAPI.openWithEditor(projectPath, editorCommand, asset.path);
|
||||||
|
return;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to open with editor:', error);
|
||||||
|
// 如果失败,回退到系统默认应用
|
||||||
|
// Fall back to system default app if failed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (fileActionRegistry) {
|
if (fileActionRegistry) {
|
||||||
const handled = await fileActionRegistry.handleDoubleClick(asset.path);
|
const handled = await fileActionRegistry.handleDoubleClick(asset.path);
|
||||||
if (handled) return;
|
if (handled) return;
|
||||||
@@ -450,7 +585,7 @@ export function ContentBrowser({
|
|||||||
console.error('Failed to open file:', error);
|
console.error('Failed to open file:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [loadAssets, onOpenScene, fileActionRegistry]);
|
}, [loadAssets, onOpenScene, fileActionRegistry, projectPath]);
|
||||||
|
|
||||||
// Handle context menu
|
// Handle context menu
|
||||||
const handleContextMenu = useCallback((e: React.MouseEvent, asset?: AssetItem) => {
|
const handleContextMenu = useCallback((e: React.MouseEvent, asset?: AssetItem) => {
|
||||||
@@ -799,9 +934,10 @@ export function ContentBrowser({
|
|||||||
icon: <ExternalLink size={16} />,
|
icon: <ExternalLink size={16} />,
|
||||||
onClick: async () => {
|
onClick: async () => {
|
||||||
try {
|
try {
|
||||||
|
console.log('[ContentBrowser] showInFolder path:', asset.path);
|
||||||
await TauriAPI.showInFolder(asset.path);
|
await TauriAPI.showInFolder(asset.path);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to show in folder:', error);
|
console.error('Failed to show in folder:', error, 'Path:', asset.path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -1126,10 +1262,16 @@ export function ContentBrowser({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Create File Dialog */}
|
{/* Create File Dialog */}
|
||||||
{createFileDialog && (
|
{createFileDialog && (() => {
|
||||||
|
// 规范化扩展名(确保有点号前缀)
|
||||||
|
// Normalize extension (ensure dot prefix)
|
||||||
|
const ext = createFileDialog.template.extension.startsWith('.')
|
||||||
|
? createFileDialog.template.extension
|
||||||
|
: `.${createFileDialog.template.extension}`;
|
||||||
|
return (
|
||||||
<PromptDialog
|
<PromptDialog
|
||||||
title={`New ${createFileDialog.template.label}`}
|
title={locale === 'zh' ? `新建 ${getTemplateLabel(createFileDialog.template.label)}` : `New ${createFileDialog.template.label}`}
|
||||||
message={`Enter file name (.${createFileDialog.template.extension} will be added):`}
|
message={locale === 'zh' ? `输入文件名(将添加 ${ext}):` : `Enter file name (${ext} will be added):`}
|
||||||
placeholder="filename"
|
placeholder="filename"
|
||||||
confirmText={locale === 'zh' ? '创建' : 'Create'}
|
confirmText={locale === 'zh' ? '创建' : 'Create'}
|
||||||
cancelText={locale === 'zh' ? '取消' : 'Cancel'}
|
cancelText={locale === 'zh' ? '取消' : 'Cancel'}
|
||||||
@@ -1138,8 +1280,8 @@ export function ContentBrowser({
|
|||||||
setCreateFileDialog(null);
|
setCreateFileDialog(null);
|
||||||
|
|
||||||
let fileName = value;
|
let fileName = value;
|
||||||
if (!fileName.endsWith(`.${template.extension}`)) {
|
if (!fileName.endsWith(ext)) {
|
||||||
fileName = `${fileName}.${template.extension}`;
|
fileName = `${fileName}${ext}`;
|
||||||
}
|
}
|
||||||
const filePath = `${parentPath}/${fileName}`;
|
const filePath = `${parentPath}/${fileName}`;
|
||||||
|
|
||||||
@@ -1155,7 +1297,8 @@ export function ContentBrowser({
|
|||||||
}}
|
}}
|
||||||
onCancel={() => setCreateFileDialog(null)}
|
onCancel={() => setCreateFileDialog(null)}
|
||||||
/>
|
/>
|
||||||
)}
|
);
|
||||||
|
})()}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -833,9 +833,10 @@ export const FileTree = forwardRef<FileTreeHandle, FileTreeProps>(({ rootPath, o
|
|||||||
icon: <FolderOpen size={16} />,
|
icon: <FolderOpen size={16} />,
|
||||||
onClick: async () => {
|
onClick: async () => {
|
||||||
try {
|
try {
|
||||||
|
console.log('[FileTree] showInFolder path:', node.path);
|
||||||
await TauriAPI.showInFolder(node.path);
|
await TauriAPI.showInFolder(node.path);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to show in folder:', error);
|
console.error('Failed to show in folder:', error, 'Path:', node.path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useState, useEffect, useRef } from 'react';
|
import { useState, useEffect, useRef } from 'react';
|
||||||
import { getVersion } from '@tauri-apps/api/app';
|
import { getVersion } from '@tauri-apps/api/app';
|
||||||
import { Globe, ChevronDown, Download, X, Loader2 } from 'lucide-react';
|
import { Globe, ChevronDown, Download, X, Loader2, Trash2 } from 'lucide-react';
|
||||||
import { checkForUpdatesOnStartup, installUpdate, type UpdateCheckResult } from '../utils/updater';
|
import { checkForUpdatesOnStartup, installUpdate, type UpdateCheckResult } from '../utils/updater';
|
||||||
import { StartupLogo } from './StartupLogo';
|
import { StartupLogo } from './StartupLogo';
|
||||||
import '../styles/StartupPage.css';
|
import '../styles/StartupPage.css';
|
||||||
@@ -11,6 +11,8 @@ interface StartupPageProps {
|
|||||||
onOpenProject: () => void;
|
onOpenProject: () => void;
|
||||||
onCreateProject: () => void;
|
onCreateProject: () => void;
|
||||||
onOpenRecentProject?: (projectPath: string) => void;
|
onOpenRecentProject?: (projectPath: string) => void;
|
||||||
|
onRemoveRecentProject?: (projectPath: string) => void;
|
||||||
|
onDeleteProject?: (projectPath: string) => Promise<void>;
|
||||||
onLocaleChange?: (locale: Locale) => void;
|
onLocaleChange?: (locale: Locale) => void;
|
||||||
recentProjects?: string[];
|
recentProjects?: string[];
|
||||||
locale: string;
|
locale: string;
|
||||||
@@ -21,11 +23,13 @@ const LANGUAGES = [
|
|||||||
{ code: 'zh', name: '中文' }
|
{ code: 'zh', name: '中文' }
|
||||||
];
|
];
|
||||||
|
|
||||||
export function StartupPage({ onOpenProject, onCreateProject, onOpenRecentProject, onLocaleChange, recentProjects = [], locale }: StartupPageProps) {
|
export function StartupPage({ onOpenProject, onCreateProject, onOpenRecentProject, onRemoveRecentProject, onDeleteProject, onLocaleChange, recentProjects = [], locale }: StartupPageProps) {
|
||||||
const [showLogo, setShowLogo] = useState(true);
|
const [showLogo, setShowLogo] = useState(true);
|
||||||
const [hoveredProject, setHoveredProject] = useState<string | null>(null);
|
const [hoveredProject, setHoveredProject] = useState<string | null>(null);
|
||||||
const [appVersion, setAppVersion] = useState<string>('');
|
const [appVersion, setAppVersion] = useState<string>('');
|
||||||
const [showLangMenu, setShowLangMenu] = useState(false);
|
const [showLangMenu, setShowLangMenu] = useState(false);
|
||||||
|
const [contextMenu, setContextMenu] = useState<{ x: number; y: number; project: string } | null>(null);
|
||||||
|
const [deleteConfirm, setDeleteConfirm] = useState<string | null>(null);
|
||||||
const [updateInfo, setUpdateInfo] = useState<UpdateCheckResult | null>(null);
|
const [updateInfo, setUpdateInfo] = useState<UpdateCheckResult | null>(null);
|
||||||
const [showUpdateBanner, setShowUpdateBanner] = useState(false);
|
const [showUpdateBanner, setShowUpdateBanner] = useState(false);
|
||||||
const [isInstalling, setIsInstalling] = useState(false);
|
const [isInstalling, setIsInstalling] = useState(false);
|
||||||
@@ -66,7 +70,13 @@ export function StartupPage({ onOpenProject, onCreateProject, onOpenRecentProjec
|
|||||||
updateAvailable: 'New version available',
|
updateAvailable: 'New version available',
|
||||||
updateNow: 'Update Now',
|
updateNow: 'Update Now',
|
||||||
installing: 'Installing...',
|
installing: 'Installing...',
|
||||||
later: 'Later'
|
later: 'Later',
|
||||||
|
removeFromList: 'Remove from List',
|
||||||
|
deleteProject: 'Delete Project',
|
||||||
|
deleteConfirmTitle: 'Delete Project',
|
||||||
|
deleteConfirmMessage: 'Are you sure you want to permanently delete this project? This action cannot be undone.',
|
||||||
|
cancel: 'Cancel',
|
||||||
|
delete: 'Delete'
|
||||||
},
|
},
|
||||||
zh: {
|
zh: {
|
||||||
title: 'ESEngine 编辑器',
|
title: 'ESEngine 编辑器',
|
||||||
@@ -78,7 +88,13 @@ export function StartupPage({ onOpenProject, onCreateProject, onOpenRecentProjec
|
|||||||
updateAvailable: '发现新版本',
|
updateAvailable: '发现新版本',
|
||||||
updateNow: '立即更新',
|
updateNow: '立即更新',
|
||||||
installing: '正在安装...',
|
installing: '正在安装...',
|
||||||
later: '稍后'
|
later: '稍后',
|
||||||
|
removeFromList: '从列表中移除',
|
||||||
|
deleteProject: '删除项目',
|
||||||
|
deleteConfirmTitle: '删除项目',
|
||||||
|
deleteConfirmMessage: '确定要永久删除此项目吗?此操作无法撤销。',
|
||||||
|
cancel: '取消',
|
||||||
|
delete: '删除'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -136,6 +152,10 @@ export function StartupPage({ onOpenProject, onCreateProject, onOpenRecentProjec
|
|||||||
onMouseEnter={() => setHoveredProject(project)}
|
onMouseEnter={() => setHoveredProject(project)}
|
||||||
onMouseLeave={() => setHoveredProject(null)}
|
onMouseLeave={() => setHoveredProject(null)}
|
||||||
onClick={() => onOpenRecentProject?.(project)}
|
onClick={() => onOpenRecentProject?.(project)}
|
||||||
|
onContextMenu={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setContextMenu({ x: e.clientX, y: e.clientY, project });
|
||||||
|
}}
|
||||||
style={{ cursor: onOpenRecentProject ? 'pointer' : 'default' }}
|
style={{ cursor: onOpenRecentProject ? 'pointer' : 'default' }}
|
||||||
>
|
>
|
||||||
<svg className="recent-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
<svg className="recent-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||||
@@ -145,6 +165,18 @@ export function StartupPage({ onOpenProject, onCreateProject, onOpenRecentProjec
|
|||||||
<div className="recent-name">{project.split(/[\\/]/).pop()}</div>
|
<div className="recent-name">{project.split(/[\\/]/).pop()}</div>
|
||||||
<div className="recent-path">{project}</div>
|
<div className="recent-path">{project}</div>
|
||||||
</div>
|
</div>
|
||||||
|
{onRemoveRecentProject && (
|
||||||
|
<button
|
||||||
|
className="recent-remove-btn"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onRemoveRecentProject(project);
|
||||||
|
}}
|
||||||
|
title={t.removeFromList}
|
||||||
|
>
|
||||||
|
<Trash2 size={14} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
@@ -217,6 +249,78 @@ export function StartupPage({ onOpenProject, onCreateProject, onOpenRecentProjec
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 右键菜单 | Context Menu */}
|
||||||
|
{contextMenu && (
|
||||||
|
<div
|
||||||
|
className="startup-context-menu-overlay"
|
||||||
|
onClick={() => setContextMenu(null)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="startup-context-menu"
|
||||||
|
style={{ left: contextMenu.x, top: contextMenu.y }}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
className="startup-context-menu-item"
|
||||||
|
onClick={() => {
|
||||||
|
onRemoveRecentProject?.(contextMenu.project);
|
||||||
|
setContextMenu(null);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<X size={14} />
|
||||||
|
<span>{t.removeFromList}</span>
|
||||||
|
</button>
|
||||||
|
{onDeleteProject && (
|
||||||
|
<button
|
||||||
|
className="startup-context-menu-item danger"
|
||||||
|
onClick={() => {
|
||||||
|
setDeleteConfirm(contextMenu.project);
|
||||||
|
setContextMenu(null);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Trash2 size={14} />
|
||||||
|
<span>{t.deleteProject}</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 删除确认对话框 | Delete Confirmation Dialog */}
|
||||||
|
{deleteConfirm && (
|
||||||
|
<div className="startup-dialog-overlay">
|
||||||
|
<div className="startup-dialog">
|
||||||
|
<div className="startup-dialog-header">
|
||||||
|
<Trash2 size={20} className="dialog-icon-danger" />
|
||||||
|
<h3>{t.deleteConfirmTitle}</h3>
|
||||||
|
</div>
|
||||||
|
<div className="startup-dialog-body">
|
||||||
|
<p>{t.deleteConfirmMessage}</p>
|
||||||
|
<p className="startup-dialog-path">{deleteConfirm}</p>
|
||||||
|
</div>
|
||||||
|
<div className="startup-dialog-footer">
|
||||||
|
<button
|
||||||
|
className="startup-dialog-btn"
|
||||||
|
onClick={() => setDeleteConfirm(null)}
|
||||||
|
>
|
||||||
|
{t.cancel}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="startup-dialog-btn danger"
|
||||||
|
onClick={async () => {
|
||||||
|
if (deleteConfirm && onDeleteProject) {
|
||||||
|
await onDeleteProject(deleteConfirm);
|
||||||
|
}
|
||||||
|
setDeleteConfirm(null);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t.delete}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,15 @@ import { EditorEngineSync } from '../services/EditorEngineSync';
|
|||||||
let engineInitialized = false;
|
let engineInitialized = false;
|
||||||
let engineInitializing = false;
|
let engineInitializing = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重置引擎初始化状态(在项目关闭时调用)
|
||||||
|
* Reset engine initialization state (called when project is closed)
|
||||||
|
*/
|
||||||
|
export function resetEngineState(): void {
|
||||||
|
engineInitialized = false;
|
||||||
|
engineInitializing = false;
|
||||||
|
}
|
||||||
|
|
||||||
export interface EngineState {
|
export interface EngineState {
|
||||||
initialized: boolean;
|
initialized: boolean;
|
||||||
running: boolean;
|
running: boolean;
|
||||||
|
|||||||
@@ -56,6 +56,32 @@ class EditorAppearanceEditorModule implements IEditorModuleLoader {
|
|||||||
step: 1
|
step: 1
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'scriptEditor',
|
||||||
|
title: '脚本编辑器',
|
||||||
|
description: '配置用于打开脚本文件的外部编辑器',
|
||||||
|
settings: [
|
||||||
|
{
|
||||||
|
key: 'editor.scriptEditor',
|
||||||
|
label: '脚本编辑器',
|
||||||
|
type: 'select',
|
||||||
|
defaultValue: 'system',
|
||||||
|
description: '双击脚本文件时使用的编辑器',
|
||||||
|
options: SettingsService.SCRIPT_EDITORS.map(editor => ({
|
||||||
|
value: editor.id,
|
||||||
|
label: editor.name
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'editor.customScriptEditorCommand',
|
||||||
|
label: '自定义编辑器命令',
|
||||||
|
type: 'string',
|
||||||
|
defaultValue: '',
|
||||||
|
description: '当选择"自定义"时,填写编辑器的命令行命令(如 notepad++)',
|
||||||
|
placeholder: '例如:notepad++'
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import {
|
|||||||
type GameRuntimeConfig
|
type GameRuntimeConfig
|
||||||
} from '@esengine/runtime-core';
|
} from '@esengine/runtime-core';
|
||||||
import { getMaterialManager } from '@esengine/material-system';
|
import { getMaterialManager } from '@esengine/material-system';
|
||||||
|
import { resetEngineState } from '../hooks/useEngine';
|
||||||
import { convertFileSrc } from '@tauri-apps/api/core';
|
import { convertFileSrc } from '@tauri-apps/api/core';
|
||||||
import { IdGenerator } from '../utils/idGenerator';
|
import { IdGenerator } from '../utils/idGenerator';
|
||||||
import { TauriAssetReader } from './TauriAssetReader';
|
import { TauriAssetReader } from './TauriAssetReader';
|
||||||
@@ -245,7 +246,14 @@ export class EngineService {
|
|||||||
ctx.uiInputSystem.unbind?.();
|
ctx.uiInputSystem.unbind?.();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 清理 viewport | Clear viewport
|
||||||
|
this.unregisterViewport('editor-viewport');
|
||||||
|
|
||||||
|
// 重置 useEngine 的模块级状态 | Reset useEngine module-level state
|
||||||
|
resetEngineState();
|
||||||
|
|
||||||
this._modulesInitialized = false;
|
this._modulesInitialized = false;
|
||||||
|
this._initialized = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -70,9 +70,11 @@ export class SettingsService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public addRecentProject(projectPath: string): void {
|
public addRecentProject(projectPath: string): void {
|
||||||
|
// 规范化路径,防止双重转义 | Normalize path to prevent double escaping
|
||||||
|
const normalizedPath = projectPath.replace(/\\\\/g, '\\');
|
||||||
const recentProjects = this.getRecentProjects();
|
const recentProjects = this.getRecentProjects();
|
||||||
const filtered = recentProjects.filter((p) => p !== projectPath);
|
const filtered = recentProjects.filter((p) => p !== normalizedPath);
|
||||||
const updated = [projectPath, ...filtered].slice(0, 10);
|
const updated = [normalizedPath, ...filtered].slice(0, 10);
|
||||||
this.set('recentProjects', updated);
|
this.set('recentProjects', updated);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,4 +87,64 @@ export class SettingsService {
|
|||||||
public clearRecentProjects(): void {
|
public clearRecentProjects(): void {
|
||||||
this.set('recentProjects', []);
|
this.set('recentProjects', []);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== Script Editor Settings ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 支持的脚本编辑器类型
|
||||||
|
* Supported script editor types
|
||||||
|
*/
|
||||||
|
public static readonly SCRIPT_EDITORS = [
|
||||||
|
{ id: 'system', name: 'System Default', nameZh: '系统默认', command: '' },
|
||||||
|
{ id: 'vscode', name: 'Visual Studio Code', nameZh: 'Visual Studio Code', command: 'code' },
|
||||||
|
{ id: 'cursor', name: 'Cursor', nameZh: 'Cursor', command: 'cursor' },
|
||||||
|
{ id: 'webstorm', name: 'WebStorm', nameZh: 'WebStorm', command: 'webstorm' },
|
||||||
|
{ id: 'sublime', name: 'Sublime Text', nameZh: 'Sublime Text', command: 'subl' },
|
||||||
|
{ id: 'custom', name: 'Custom', nameZh: '自定义', command: '' }
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取脚本编辑器设置
|
||||||
|
* Get script editor setting
|
||||||
|
*/
|
||||||
|
public getScriptEditor(): string {
|
||||||
|
return this.get<string>('editor.scriptEditor', 'system');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置脚本编辑器
|
||||||
|
* Set script editor
|
||||||
|
*/
|
||||||
|
public setScriptEditor(editorId: string): void {
|
||||||
|
this.set('editor.scriptEditor', editorId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取自定义脚本编辑器命令
|
||||||
|
* Get custom script editor command
|
||||||
|
*/
|
||||||
|
public getCustomScriptEditorCommand(): string {
|
||||||
|
return this.get<string>('editor.customScriptEditorCommand', '');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置自定义脚本编辑器命令
|
||||||
|
* Set custom script editor command
|
||||||
|
*/
|
||||||
|
public setCustomScriptEditorCommand(command: string): void {
|
||||||
|
this.set('editor.customScriptEditorCommand', command);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前脚本编辑器的命令
|
||||||
|
* Get current script editor command
|
||||||
|
*/
|
||||||
|
public getScriptEditorCommand(): string {
|
||||||
|
const editorId = this.getScriptEditor();
|
||||||
|
if (editorId === 'custom') {
|
||||||
|
return this.getCustomScriptEditorCommand();
|
||||||
|
}
|
||||||
|
const editor = SettingsService.SCRIPT_EDITORS.find(e => e.id === editorId);
|
||||||
|
return editor?.command || '';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -174,6 +174,32 @@
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.recent-remove-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
padding: 0;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #6e6e6e;
|
||||||
|
cursor: pointer;
|
||||||
|
opacity: 0;
|
||||||
|
transition: all 0.15s;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recent-item:hover .recent-remove-btn {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recent-remove-btn:hover {
|
||||||
|
background: rgba(255, 80, 80, 0.15);
|
||||||
|
color: #f87171;
|
||||||
|
}
|
||||||
|
|
||||||
.startup-footer {
|
.startup-footer {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -346,3 +372,157 @@
|
|||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 右键菜单样式 | Context Menu Styles */
|
||||||
|
.startup-context-menu-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.startup-context-menu {
|
||||||
|
position: fixed;
|
||||||
|
min-width: 180px;
|
||||||
|
background: #252529;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 6px;
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
|
||||||
|
padding: 4px 0;
|
||||||
|
z-index: 1001;
|
||||||
|
}
|
||||||
|
|
||||||
|
.startup-context-menu-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px 14px;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: #cccccc;
|
||||||
|
font-size: 13px;
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.startup-context-menu-item:hover {
|
||||||
|
background: #3b82f6;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.startup-context-menu-item.danger {
|
||||||
|
color: #f87171;
|
||||||
|
}
|
||||||
|
|
||||||
|
.startup-context-menu-item.danger:hover {
|
||||||
|
background: #dc2626;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 对话框样式 | Dialog Styles */
|
||||||
|
.startup-dialog-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.6);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 1002;
|
||||||
|
}
|
||||||
|
|
||||||
|
.startup-dialog {
|
||||||
|
width: 400px;
|
||||||
|
background: #2d2d30;
|
||||||
|
border: 1px solid #3e3e42;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.5);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.startup-dialog-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 16px 20px;
|
||||||
|
background: #252526;
|
||||||
|
border-bottom: 1px solid #3e3e42;
|
||||||
|
}
|
||||||
|
|
||||||
|
.startup-dialog-header h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-icon-danger {
|
||||||
|
color: #f87171;
|
||||||
|
}
|
||||||
|
|
||||||
|
.startup-dialog-body {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.startup-dialog-body p {
|
||||||
|
margin: 0 0 12px 0;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #cccccc;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.startup-dialog-body p:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.startup-dialog-path {
|
||||||
|
padding: 10px 12px;
|
||||||
|
background: #1e1e1e;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #858585;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.startup-dialog-footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 16px 20px;
|
||||||
|
background: #252526;
|
||||||
|
border-top: 1px solid #3e3e42;
|
||||||
|
}
|
||||||
|
|
||||||
|
.startup-dialog-btn {
|
||||||
|
padding: 8px 16px;
|
||||||
|
border: 1px solid #3e3e42;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #2d2d30;
|
||||||
|
color: #cccccc;
|
||||||
|
font-size: 13px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.startup-dialog-btn:hover {
|
||||||
|
background: #37373d;
|
||||||
|
border-color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.startup-dialog-btn.danger {
|
||||||
|
background: #dc2626;
|
||||||
|
border-color: #dc2626;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.startup-dialog-btn.danger:hover {
|
||||||
|
background: #b91c1c;
|
||||||
|
border-color: #b91c1c;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1159,6 +1159,11 @@ export class PluginManager implements IService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 重置初始化状态,允许下次重新初始化运行时
|
||||||
|
// Reset initialized flag to allow re-initialization
|
||||||
|
this.initialized = false;
|
||||||
|
logger.debug('Scene systems cleared, runtime can be re-initialized');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -94,11 +94,15 @@ export class ProjectService implements IService {
|
|||||||
scriptsPath: 'scripts',
|
scriptsPath: 'scripts',
|
||||||
buildOutput: '.esengine/compiled',
|
buildOutput: '.esengine/compiled',
|
||||||
scenesPath: 'scenes',
|
scenesPath: 'scenes',
|
||||||
defaultScene: 'main.ecs'
|
defaultScene: 'main.ecs',
|
||||||
|
plugins: { enabledPlugins: [] },
|
||||||
|
modules: { disabledModules: [] }
|
||||||
};
|
};
|
||||||
|
|
||||||
await this.fileAPI.writeFileContent(configPath, JSON.stringify(config, null, 2));
|
await this.fileAPI.writeFileContent(configPath, JSON.stringify(config, null, 2));
|
||||||
|
|
||||||
|
// Create scenes folder and default scene
|
||||||
|
// 创建场景文件夹和默认场景
|
||||||
const scenesPath = `${projectPath}${sep}${config.scenesPath}`;
|
const scenesPath = `${projectPath}${sep}${config.scenesPath}`;
|
||||||
await this.fileAPI.createDirectory(scenesPath);
|
await this.fileAPI.createDirectory(scenesPath);
|
||||||
|
|
||||||
@@ -111,6 +115,55 @@ export class ProjectService implements IService {
|
|||||||
}) as string;
|
}) as string;
|
||||||
await this.fileAPI.writeFileContent(defaultScenePath, sceneData);
|
await this.fileAPI.writeFileContent(defaultScenePath, sceneData);
|
||||||
|
|
||||||
|
// Create scripts folder for user scripts
|
||||||
|
// 创建用户脚本文件夹
|
||||||
|
const scriptsPath = `${projectPath}${sep}${config.scriptsPath}`;
|
||||||
|
await this.fileAPI.createDirectory(scriptsPath);
|
||||||
|
|
||||||
|
// Create scripts/editor folder for editor extension scripts
|
||||||
|
// 创建编辑器扩展脚本文件夹
|
||||||
|
const editorScriptsPath = `${scriptsPath}${sep}editor`;
|
||||||
|
await this.fileAPI.createDirectory(editorScriptsPath);
|
||||||
|
|
||||||
|
// Create assets folder for project assets (textures, audio, etc.)
|
||||||
|
// 创建资源文件夹(纹理、音频等)
|
||||||
|
const assetsPath = `${projectPath}${sep}assets`;
|
||||||
|
await this.fileAPI.createDirectory(assetsPath);
|
||||||
|
|
||||||
|
// Create types folder for type definitions
|
||||||
|
// 创建类型定义文件夹
|
||||||
|
const typesPath = `${projectPath}${sep}types`;
|
||||||
|
await this.fileAPI.createDirectory(typesPath);
|
||||||
|
|
||||||
|
// Create tsconfig.json for TypeScript support
|
||||||
|
// 创建 tsconfig.json 用于 TypeScript 支持
|
||||||
|
const tsConfig = {
|
||||||
|
compilerOptions: {
|
||||||
|
target: 'ES2020',
|
||||||
|
module: 'ESNext',
|
||||||
|
moduleResolution: 'bundler',
|
||||||
|
lib: ['ES2020', 'DOM'],
|
||||||
|
strict: true,
|
||||||
|
esModuleInterop: true,
|
||||||
|
skipLibCheck: true,
|
||||||
|
forceConsistentCasingInFileNames: true,
|
||||||
|
experimentalDecorators: true,
|
||||||
|
emitDecoratorMetadata: true,
|
||||||
|
noEmit: true,
|
||||||
|
// Reference local type definitions
|
||||||
|
// 引用本地类型定义文件
|
||||||
|
typeRoots: ['./types'],
|
||||||
|
paths: {
|
||||||
|
'@esengine/ecs-framework': ['./types/ecs-framework.d.ts'],
|
||||||
|
'@esengine/engine-core': ['./types/engine-core.d.ts']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
include: ['scripts/**/*.ts'],
|
||||||
|
exclude: ['.esengine']
|
||||||
|
};
|
||||||
|
const tsConfigPath = `${projectPath}${sep}tsconfig.json`;
|
||||||
|
await this.fileAPI.writeFileContent(tsConfigPath, JSON.stringify(tsConfig, null, 2));
|
||||||
|
|
||||||
await this.messageHub.publish('project:created', {
|
await this.messageHub.publish('project:created', {
|
||||||
path: projectPath
|
path: projectPath
|
||||||
});
|
});
|
||||||
@@ -258,8 +311,10 @@ export class ProjectService implements IService {
|
|||||||
scenesPath: config.scenesPath || 'scenes',
|
scenesPath: config.scenesPath || 'scenes',
|
||||||
defaultScene: config.defaultScene || 'main.ecs',
|
defaultScene: config.defaultScene || 'main.ecs',
|
||||||
uiDesignResolution: config.uiDesignResolution,
|
uiDesignResolution: config.uiDesignResolution,
|
||||||
plugins: config.plugins,
|
// Provide default empty plugins config for legacy projects
|
||||||
modules: config.modules
|
// 为旧项目提供默认的空插件配置
|
||||||
|
plugins: config.plugins || { enabledPlugins: [] },
|
||||||
|
modules: config.modules || { disabledModules: [] }
|
||||||
};
|
};
|
||||||
logger.debug('Loaded config result:', result);
|
logger.debug('Loaded config result:', result);
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
Reference in New Issue
Block a user