Compare commits
5 Commits
editor-v1.
...
feat/githu
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
566e1977fd | ||
|
|
17f6259f43 | ||
|
|
5d3483fc65 | ||
|
|
d07a5d81fc | ||
|
|
6a4e6fbc04 |
@@ -180,7 +180,6 @@ function createNav(t, prefix = '') {
|
||||
{ text: t.nav.lawnMowerDemo, link: 'https://github.com/esengine/lawn-mower-demo' }
|
||||
]
|
||||
},
|
||||
{ text: t.nav.changelog, link: `${prefix}/changelog` },
|
||||
{
|
||||
text: `v${corePackageJson.version}`,
|
||||
link: 'https://github.com/esengine/ecs-framework/releases'
|
||||
|
||||
@@ -6,8 +6,7 @@
|
||||
"api": "API",
|
||||
"examples": "Examples",
|
||||
"workerDemo": "Worker System Demo",
|
||||
"lawnMowerDemo": "Lawn Mower Demo",
|
||||
"changelog": "Changelog"
|
||||
"lawnMowerDemo": "Lawn Mower Demo"
|
||||
},
|
||||
"sidebar": {
|
||||
"gettingStarted": "Getting Started",
|
||||
|
||||
@@ -6,8 +6,7 @@
|
||||
"api": "API",
|
||||
"examples": "示例",
|
||||
"workerDemo": "Worker系统演示",
|
||||
"lawnMowerDemo": "割草机演示",
|
||||
"changelog": "更新日志"
|
||||
"lawnMowerDemo": "割草机演示"
|
||||
},
|
||||
"sidebar": {
|
||||
"gettingStarted": "开始使用",
|
||||
|
||||
@@ -1,138 +0,0 @@
|
||||
# Changelog
|
||||
|
||||
本文档记录 `@esengine/ecs-framework` 核心库的版本更新历史。
|
||||
|
||||
---
|
||||
|
||||
## v2.2.21 (2025-12-05)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **迭代安全修复**: 修复 `process`/`lateProcess` 迭代时组件变化导致跳过实体的问题 (#272)
|
||||
- 在系统处理过程中添加/移除组件不再导致实体被意外跳过
|
||||
|
||||
### Performance
|
||||
|
||||
- **HierarchySystem 性能优化**: 优化层级系统避免每帧遍历所有实体 (#279)
|
||||
- 使用脏实体集合代替每帧遍历所有实体
|
||||
- 静态场景下 `process()` 从 O(n) 优化为 O(1)
|
||||
- 1000 实体静态场景: 81.79μs → 0.07μs (快 1168 倍)
|
||||
- 10000 实体静态场景: 939.43μs → 0.56μs (快 1677 倍)
|
||||
- 服务端模拟 (100房间 x 100实体): 2.7ms → 1.4ms 每 tick
|
||||
|
||||
---
|
||||
|
||||
## v2.2.20 (2025-12-04)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **系统 onAdded 回调修复**: 修复系统 `onAdded` 回调受注册顺序影响的问题 (#270)
|
||||
- 系统初始化时会对已存在的匹配实体触发 `onAdded` 回调
|
||||
- 新增 `matchesEntity(entity)` 方法,用于检查实体是否匹配系统的查询条件
|
||||
- Scene 新增 `notifySystemsEntityAdded/Removed` 方法,确保所有系统都能收到实体变更通知
|
||||
|
||||
---
|
||||
|
||||
## v2.2.19 (2025-12-03)
|
||||
|
||||
### Features
|
||||
|
||||
- **系统稳定排序**: 添加系统稳定排序支持 (#257)
|
||||
- 新增 `addOrder` 属性,用于 `updateOrder` 相同时的稳定排序
|
||||
- 确保相同优先级的系统按添加顺序执行
|
||||
|
||||
- **模块配置**: 添加 `module.json` 配置文件 (#256)
|
||||
- 定义模块 ID、名称、版本等元信息
|
||||
- 支持模块依赖声明和导出配置
|
||||
|
||||
---
|
||||
|
||||
## v2.2.18 (2025-11-30)
|
||||
|
||||
### Features
|
||||
|
||||
- **高级性能分析器**: 实现全新的性能分析 SDK (#248)
|
||||
- `ProfilerSDK`: 统一的性能分析接口
|
||||
- 手动采样标记 (`beginSample`/`endSample`)
|
||||
- 自动作用域测量 (`measure`/`measureAsync`)
|
||||
- 调用层级追踪和调用图生成
|
||||
- 计数器和仪表支持
|
||||
- `AdvancedProfilerCollector`: 高级性能数据收集器
|
||||
- 帧时间统计和历史记录
|
||||
- 内存快照和 GC 检测
|
||||
- 长任务检测 (Long Task API)
|
||||
- 性能报告生成
|
||||
- `DebugManager`: 调试管理器
|
||||
- 统一的调试工具入口
|
||||
- 性能分析器集成
|
||||
|
||||
- **属性装饰器增强**: 扩展 `@serialize` 装饰器功能 (#247)
|
||||
- 支持更多序列化选项配置
|
||||
|
||||
### Improvements
|
||||
|
||||
- **EntitySystem 测试覆盖**: 添加完整的系统测试用例 (#240)
|
||||
- 覆盖实体查询、缓存、生命周期等场景
|
||||
|
||||
- **Matcher 增强**: 优化匹配器功能 (#240)
|
||||
- 改进匹配逻辑和性能
|
||||
|
||||
---
|
||||
|
||||
## v2.2.17 (2025-11-28)
|
||||
|
||||
### Features
|
||||
|
||||
- **ComponentRegistry 增强**: 添加组件注册表新功能 (#244)
|
||||
- 支持通过名称注册和查询组件类型
|
||||
- 添加组件掩码缓存优化性能
|
||||
|
||||
- **序列化装饰器改进**: 增强 `@serialize` 装饰器 (#244)
|
||||
- 支持更灵活的序列化配置
|
||||
- 改进嵌套对象序列化
|
||||
|
||||
- **EntitySystem 生命周期**: 新增系统生命周期方法 (#244)
|
||||
- `onSceneStart()`: 场景开始时调用
|
||||
- `onSceneStop()`: 场景停止时调用
|
||||
|
||||
---
|
||||
|
||||
## v2.2.16 (2025-11-27)
|
||||
|
||||
### Features
|
||||
|
||||
- **组件生命周期**: 添加组件生命周期回调支持 (#237)
|
||||
- `onDeserialized()`: 组件从场景文件加载或快照恢复后调用,用于恢复运行时数据
|
||||
|
||||
- **ServiceContainer 增强**: 改进服务容器功能 (#237)
|
||||
- 支持 `Symbol.for()` 模式的服务标识
|
||||
- 新增 `@InjectProperty` 属性注入装饰器
|
||||
- 改进服务解析和生命周期管理
|
||||
|
||||
- **SceneSerializer 增强**: 场景序列化器新功能 (#237)
|
||||
- 支持更多组件类型的序列化
|
||||
- 改进反序列化错误处理
|
||||
|
||||
- **属性装饰器扩展**: 扩展 `@serialize` 装饰器 (#238)
|
||||
- 支持 `@range`、`@slider` 等编辑器提示
|
||||
- 支持 `@dropdown`、`@color` 等 UI 类型
|
||||
- 支持 `@asset` 资源引用类型
|
||||
|
||||
### Improvements
|
||||
|
||||
- **Matcher 测试**: 添加 Matcher 匹配器测试用例 (#240)
|
||||
- **EntitySystem 测试**: 添加实体系统完整测试覆盖 (#240)
|
||||
|
||||
---
|
||||
|
||||
## 版本说明
|
||||
|
||||
- **主版本号**: 重大不兼容更新
|
||||
- **次版本号**: 新功能添加(向后兼容)
|
||||
- **修订版本号**: Bug 修复和小改进
|
||||
|
||||
## 相关链接
|
||||
|
||||
- [GitHub Releases](https://github.com/esengine/ecs-framework/releases)
|
||||
- [NPM Package](https://www.npmjs.com/package/@esengine/ecs-framework)
|
||||
- [文档首页](./index.md)
|
||||
@@ -1,138 +0,0 @@
|
||||
# Changelog
|
||||
|
||||
This document records the version update history of the `@esengine/ecs-framework` core library.
|
||||
|
||||
---
|
||||
|
||||
## v2.2.21 (2025-12-05)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **Iteration safety fix**: Fix issue where component changes during `process`/`lateProcess` iteration caused entities to be skipped (#272)
|
||||
- Adding/removing components during system processing no longer causes entities to be unexpectedly skipped
|
||||
|
||||
### Performance
|
||||
|
||||
- **HierarchySystem optimization**: Optimize hierarchy system to avoid iterating all entities every frame (#279)
|
||||
- Use dirty entity set instead of iterating all entities
|
||||
- Static scene `process()` optimized from O(n) to O(1)
|
||||
- 1000 entities static scene: 81.79μs → 0.07μs (1168x faster)
|
||||
- 10000 entities static scene: 939.43μs → 0.56μs (1677x faster)
|
||||
- Server simulation (100 rooms x 100 entities): 2.7ms → 1.4ms per tick
|
||||
|
||||
---
|
||||
|
||||
## v2.2.20 (2025-12-04)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **System onAdded callback fix**: Fix issue where system `onAdded` callback was affected by registration order (#270)
|
||||
- System initialization now triggers `onAdded` callback for existing matching entities
|
||||
- Added `matchesEntity(entity)` method to check if an entity matches the system's query condition
|
||||
- Scene added `notifySystemsEntityAdded/Removed` methods to ensure all systems receive entity change notifications
|
||||
|
||||
---
|
||||
|
||||
## v2.2.19 (2025-12-03)
|
||||
|
||||
### Features
|
||||
|
||||
- **System stable sorting**: Add stable sorting support for systems (#257)
|
||||
- Added `addOrder` property for stable sorting when `updateOrder` is the same
|
||||
- Ensures systems with same priority execute in add order
|
||||
|
||||
- **Module configuration**: Add `module.json` configuration file (#256)
|
||||
- Define module ID, name, version and other metadata
|
||||
- Support module dependency declaration and export configuration
|
||||
|
||||
---
|
||||
|
||||
## v2.2.18 (2025-11-30)
|
||||
|
||||
### Features
|
||||
|
||||
- **Advanced Performance Profiler**: Implement new performance analysis SDK (#248)
|
||||
- `ProfilerSDK`: Unified performance analysis interface
|
||||
- Manual sampling markers (`beginSample`/`endSample`)
|
||||
- Automatic scope measurement (`measure`/`measureAsync`)
|
||||
- Call hierarchy tracking and call graph generation
|
||||
- Counter and gauge support
|
||||
- `AdvancedProfilerCollector`: Advanced performance data collector
|
||||
- Frame time statistics and history
|
||||
- Memory snapshots and GC detection
|
||||
- Long task detection (Long Task API)
|
||||
- Performance report generation
|
||||
- `DebugManager`: Debug manager
|
||||
- Unified debug tool entry point
|
||||
- Profiler integration
|
||||
|
||||
- **Property decorator enhancement**: Extend `@serialize` decorator functionality (#247)
|
||||
- Support more serialization option configurations
|
||||
|
||||
### Improvements
|
||||
|
||||
- **EntitySystem test coverage**: Add complete system test cases (#240)
|
||||
- Cover entity query, cache, lifecycle scenarios
|
||||
|
||||
- **Matcher enhancement**: Optimize matcher functionality (#240)
|
||||
- Improved matching logic and performance
|
||||
|
||||
---
|
||||
|
||||
## v2.2.17 (2025-11-28)
|
||||
|
||||
### Features
|
||||
|
||||
- **ComponentRegistry enhancement**: Add new component registry features (#244)
|
||||
- Support registering and querying component types by name
|
||||
- Add component mask caching for performance optimization
|
||||
|
||||
- **Serialization decorator improvements**: Enhance `@serialize` decorator (#244)
|
||||
- Support more flexible serialization configuration
|
||||
- Improved nested object serialization
|
||||
|
||||
- **EntitySystem lifecycle**: Add new system lifecycle methods (#244)
|
||||
- `onSceneStart()`: Called when scene starts
|
||||
- `onSceneStop()`: Called when scene stops
|
||||
|
||||
---
|
||||
|
||||
## v2.2.16 (2025-11-27)
|
||||
|
||||
### Features
|
||||
|
||||
- **Component lifecycle**: Add component lifecycle callback support (#237)
|
||||
- `onDeserialized()`: Called after component is loaded from scene file or snapshot restore, used to restore runtime data
|
||||
|
||||
- **ServiceContainer enhancement**: Improve service container functionality (#237)
|
||||
- Support `Symbol.for()` pattern for service identifiers
|
||||
- Added `@InjectProperty` property injection decorator
|
||||
- Improved service resolution and lifecycle management
|
||||
|
||||
- **SceneSerializer enhancement**: New scene serializer features (#237)
|
||||
- Support serialization of more component types
|
||||
- Improved deserialization error handling
|
||||
|
||||
- **Property decorator extension**: Extend `@serialize` decorator (#238)
|
||||
- Support `@range`, `@slider` and other editor hints
|
||||
- Support `@dropdown`, `@color` and other UI types
|
||||
- Support `@asset` resource reference type
|
||||
|
||||
### Improvements
|
||||
|
||||
- **Matcher tests**: Add Matcher test cases (#240)
|
||||
- **EntitySystem tests**: Add complete entity system test coverage (#240)
|
||||
|
||||
---
|
||||
|
||||
## Version Notes
|
||||
|
||||
- **Major version**: Breaking changes
|
||||
- **Minor version**: New features (backward compatible)
|
||||
- **Patch version**: Bug fixes and improvements
|
||||
|
||||
## Related Links
|
||||
|
||||
- [GitHub Releases](https://github.com/esengine/ecs-framework/releases)
|
||||
- [NPM Package](https://www.npmjs.com/package/@esengine/ecs-framework)
|
||||
- [Documentation Home](./index.md)
|
||||
@@ -1,820 +0,0 @@
|
||||
# System Architecture
|
||||
|
||||
In ECS architecture, Systems are where business logic is processed. Systems are responsible for performing operations on entities that have specific component combinations, serving as the logic processing units of ECS architecture.
|
||||
|
||||
## Basic Concepts
|
||||
|
||||
Systems are concrete classes that inherit from the `EntitySystem` abstract base class, used for:
|
||||
- Defining entity processing logic (such as movement, collision detection, rendering, etc.)
|
||||
- Filtering entities based on component combinations
|
||||
- Providing lifecycle management and performance monitoring
|
||||
- Managing entity add/remove events
|
||||
|
||||
## System Types
|
||||
|
||||
The framework provides several different system base classes:
|
||||
|
||||
### EntitySystem - Base System
|
||||
|
||||
The most basic system class, all other systems inherit from it:
|
||||
|
||||
```typescript
|
||||
import { EntitySystem, ECSSystem, Matcher } from '@esengine/ecs-framework';
|
||||
|
||||
@ECSSystem('Movement')
|
||||
class MovementSystem extends EntitySystem {
|
||||
constructor() {
|
||||
// Use Matcher to define entity conditions to process
|
||||
super(Matcher.all(Position, Velocity));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const position = entity.getComponent(Position);
|
||||
const velocity = entity.getComponent(Velocity);
|
||||
|
||||
if (position && velocity) {
|
||||
position.x += velocity.dx * Time.deltaTime;
|
||||
position.y += velocity.dy * Time.deltaTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### ProcessingSystem - Processing System
|
||||
|
||||
Suitable for systems that don't need to process entities individually:
|
||||
|
||||
```typescript
|
||||
@ECSSystem('Physics')
|
||||
class PhysicsSystem extends ProcessingSystem {
|
||||
constructor() {
|
||||
super(); // No Matcher needed
|
||||
}
|
||||
|
||||
public processSystem(): void {
|
||||
// Execute physics world step
|
||||
this.physicsWorld.step(Time.deltaTime);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### PassiveSystem - Passive System
|
||||
|
||||
Passive systems don't actively process, mainly used for listening to entity add and remove events:
|
||||
|
||||
```typescript
|
||||
@ECSSystem('EntityTracker')
|
||||
class EntityTrackerSystem extends PassiveSystem {
|
||||
constructor() {
|
||||
super(Matcher.all(Health));
|
||||
}
|
||||
|
||||
protected onAdded(entity: Entity): void {
|
||||
console.log(`Health entity added: ${entity.name}`);
|
||||
}
|
||||
|
||||
protected onRemoved(entity: Entity): void {
|
||||
console.log(`Health entity removed: ${entity.name}`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### IntervalSystem - Interval System
|
||||
|
||||
Systems that execute at fixed time intervals:
|
||||
|
||||
```typescript
|
||||
@ECSSystem('AutoSave')
|
||||
class AutoSaveSystem extends IntervalSystem {
|
||||
constructor() {
|
||||
// Execute every 5 seconds
|
||||
super(5.0, Matcher.all(SaveData));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
console.log('Executing auto save...');
|
||||
// Save game data
|
||||
this.saveGameData(entities);
|
||||
}
|
||||
|
||||
private saveGameData(entities: readonly Entity[]): void {
|
||||
// Save logic
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### WorkerEntitySystem - Multi-threaded System
|
||||
|
||||
A Web Worker-based multi-threaded processing system, suitable for compute-intensive tasks, capable of fully utilizing multi-core CPU performance.
|
||||
|
||||
Worker systems provide true parallel computing capabilities, support SharedArrayBuffer optimization, and have automatic fallback support. Particularly suitable for physics simulation, particle systems, AI computation, and similar scenarios.
|
||||
|
||||
**For detailed content, please refer to: [Worker System](/guide/worker-system)**
|
||||
|
||||
## Entity Matcher
|
||||
|
||||
Matcher is used to define which entities a system needs to process. It provides flexible condition combinations:
|
||||
|
||||
### Basic Match Conditions
|
||||
|
||||
```typescript
|
||||
// Must have both Position and Velocity components
|
||||
const matcher1 = Matcher.all(Position, Velocity);
|
||||
|
||||
// Must have at least one of Health or Shield components
|
||||
const matcher2 = Matcher.any(Health, Shield);
|
||||
|
||||
// Must not have Dead component
|
||||
const matcher3 = Matcher.none(Dead);
|
||||
```
|
||||
|
||||
### Compound Match Conditions
|
||||
|
||||
```typescript
|
||||
// Complex combination conditions
|
||||
const complexMatcher = Matcher.all(Position, Velocity)
|
||||
.any(Player, Enemy)
|
||||
.none(Dead, Disabled);
|
||||
|
||||
@ECSSystem('Combat')
|
||||
class CombatSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(complexMatcher);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Special Match Conditions
|
||||
|
||||
```typescript
|
||||
// Match by tag
|
||||
const tagMatcher = Matcher.byTag(1); // Match entities with tag 1
|
||||
|
||||
// Match by name
|
||||
const nameMatcher = Matcher.byName("Player"); // Match entities named "Player"
|
||||
|
||||
// Single component match
|
||||
const componentMatcher = Matcher.byComponent(Health); // Match entities with Health component
|
||||
|
||||
// Match no entities
|
||||
const nothingMatcher = Matcher.nothing(); // For systems that only need lifecycle callbacks
|
||||
```
|
||||
|
||||
### Empty Matcher vs Nothing Matcher
|
||||
|
||||
```typescript
|
||||
// empty() - Empty condition, matches all entities
|
||||
const emptyMatcher = Matcher.empty();
|
||||
|
||||
// nothing() - Matches no entities, for systems that only need lifecycle methods
|
||||
const nothingMatcher = Matcher.nothing();
|
||||
|
||||
// Use case: Systems that only need onBegin/onEnd lifecycle
|
||||
@ECSSystem('FrameTimer')
|
||||
class FrameTimerSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.nothing()); // Process no entities
|
||||
}
|
||||
|
||||
protected onBegin(): void {
|
||||
// Execute at the start of each frame, e.g., record frame start time
|
||||
console.log('Frame started');
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// Never called because there are no matching entities
|
||||
}
|
||||
|
||||
protected onEnd(): void {
|
||||
// Execute at the end of each frame
|
||||
console.log('Frame ended');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> **Tip**: For more details on Matcher and entity queries, please refer to the [Entity Query System](/guide/entity-query) documentation.
|
||||
|
||||
## System Lifecycle
|
||||
|
||||
Systems provide complete lifecycle callbacks:
|
||||
|
||||
```typescript
|
||||
@ECSSystem('Example')
|
||||
class ExampleSystem extends EntitySystem {
|
||||
protected onInitialize(): void {
|
||||
console.log('System initialized');
|
||||
// Called when system is added to scene, for initializing resources
|
||||
}
|
||||
|
||||
protected onBegin(): void {
|
||||
// Called before each frame's processing begins
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// Main processing logic
|
||||
for (const entity of entities) {
|
||||
// Process each entity
|
||||
// Safe to add/remove components here without affecting current iteration
|
||||
}
|
||||
}
|
||||
|
||||
protected lateProcess(entities: readonly Entity[]): void {
|
||||
// Post-processing after main process
|
||||
// Safe to add/remove components here without affecting current iteration
|
||||
}
|
||||
|
||||
protected onEnd(): void {
|
||||
// Called after each frame's processing ends
|
||||
}
|
||||
|
||||
protected onDestroy(): void {
|
||||
console.log('System destroyed');
|
||||
// Called when system is removed from scene, for cleaning up resources
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Entity Event Listening
|
||||
|
||||
Systems can listen for entity add and remove events:
|
||||
|
||||
```typescript
|
||||
@ECSSystem('EnemyManager')
|
||||
class EnemyManagerSystem extends EntitySystem {
|
||||
private enemyCount = 0;
|
||||
|
||||
constructor() {
|
||||
super(Matcher.all(Enemy, Health));
|
||||
}
|
||||
|
||||
protected onAdded(entity: Entity): void {
|
||||
this.enemyCount++;
|
||||
console.log(`Enemy joined battle, current enemy count: ${this.enemyCount}`);
|
||||
|
||||
// Can set initial state for new enemies here
|
||||
const health = entity.getComponent(Health);
|
||||
if (health) {
|
||||
health.current = health.max;
|
||||
}
|
||||
}
|
||||
|
||||
protected onRemoved(entity: Entity): void {
|
||||
this.enemyCount--;
|
||||
console.log(`Enemy removed, remaining enemies: ${this.enemyCount}`);
|
||||
|
||||
// Check if all enemies are defeated
|
||||
if (this.enemyCount === 0) {
|
||||
this.scene?.eventSystem.emitSync('all_enemies_defeated');
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Important: Timing of onAdded/onRemoved Calls
|
||||
|
||||
> **Note**: `onAdded` and `onRemoved` callbacks are called **synchronously**, executing immediately **before** `addComponent`/`removeComponent` returns.
|
||||
|
||||
This means:
|
||||
|
||||
```typescript
|
||||
// Wrong: Chain assignment executes after onAdded
|
||||
const comp = entity.addComponent(new ClickComponent());
|
||||
comp.element = this._element; // At this point onAdded has already executed!
|
||||
|
||||
// Correct: Pass initial values through constructor
|
||||
const comp = entity.addComponent(new ClickComponent(this._element));
|
||||
|
||||
// Or use the createComponent method
|
||||
const comp = entity.createComponent(ClickComponent, this._element);
|
||||
```
|
||||
|
||||
**Why this design?**
|
||||
|
||||
The event-driven design ensures that `onAdded`/`onRemoved` callbacks are not affected by system registration order. When a component is added, all systems listening for that component receive notification immediately, rather than waiting until the next frame.
|
||||
|
||||
**Best Practices:**
|
||||
|
||||
1. Component initial values should be passed through the **constructor**
|
||||
2. Don't rely on setting properties after `addComponent` returns
|
||||
3. If you need to access component properties in `onAdded`, ensure those properties are set at construction time
|
||||
|
||||
### Safely Modifying Components in process/lateProcess
|
||||
|
||||
When iterating entities in `process` or `lateProcess`, you can safely add or remove components without affecting the current iteration:
|
||||
|
||||
```typescript
|
||||
@ECSSystem('Damage')
|
||||
class DamageSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(Health, DamageReceiver));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const health = entity.getComponent(Health);
|
||||
const damage = entity.getComponent(DamageReceiver);
|
||||
|
||||
if (health && damage) {
|
||||
health.current -= damage.amount;
|
||||
|
||||
// Safe: removing component won't affect current iteration
|
||||
entity.removeComponent(damage);
|
||||
|
||||
if (health.current <= 0) {
|
||||
// Safe: adding component won't affect current iteration
|
||||
entity.addComponent(new Dead());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The framework creates a snapshot of the entity list before each `process`/`lateProcess` call, ensuring that component changes during iteration won't cause entities to be skipped or processed multiple times.
|
||||
|
||||
## System Properties and Methods
|
||||
|
||||
### Important Properties
|
||||
|
||||
```typescript
|
||||
@ECSSystem('Example')
|
||||
class ExampleSystem extends EntitySystem {
|
||||
showSystemInfo(): void {
|
||||
console.log(`System name: ${this.systemName}`); // System name
|
||||
console.log(`Update order: ${this.updateOrder}`); // Update order
|
||||
console.log(`Is enabled: ${this.enabled}`); // Enabled state
|
||||
console.log(`Entity count: ${this.entities.length}`); // Number of matched entities
|
||||
console.log(`Scene: ${this.scene?.name}`); // Parent scene
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Entity Access
|
||||
|
||||
```typescript
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// Method 1: Use entity list from parameter
|
||||
for (const entity of entities) {
|
||||
// Process entity
|
||||
}
|
||||
|
||||
// Method 2: Use this.entities property (same as parameter)
|
||||
for (const entity of this.entities) {
|
||||
// Process entity
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Controlling System Execution
|
||||
|
||||
```typescript
|
||||
@ECSSystem('Conditional')
|
||||
class ConditionalSystem extends EntitySystem {
|
||||
private shouldProcess = true;
|
||||
|
||||
protected onCheckProcessing(): boolean {
|
||||
// Return false to skip this processing
|
||||
return this.shouldProcess && this.entities.length > 0;
|
||||
}
|
||||
|
||||
public pause(): void {
|
||||
this.shouldProcess = false;
|
||||
}
|
||||
|
||||
public resume(): void {
|
||||
this.shouldProcess = true;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Event System Integration
|
||||
|
||||
Systems can conveniently listen for and send events:
|
||||
|
||||
```typescript
|
||||
@ECSSystem('GameLogic')
|
||||
class GameLogicSystem extends EntitySystem {
|
||||
protected onInitialize(): void {
|
||||
// Add event listeners (automatically cleaned up when system is destroyed)
|
||||
this.addEventListener('player_died', this.onPlayerDied.bind(this));
|
||||
this.addEventListener('level_complete', this.onLevelComplete.bind(this));
|
||||
}
|
||||
|
||||
private onPlayerDied(data: any): void {
|
||||
console.log('Player died, restarting game');
|
||||
// Handle player death logic
|
||||
}
|
||||
|
||||
private onLevelComplete(data: any): void {
|
||||
console.log('Level complete, loading next level');
|
||||
// Handle level completion logic
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// Send events during processing
|
||||
for (const entity of entities) {
|
||||
const health = entity.getComponent(Health);
|
||||
if (health && health.current <= 0) {
|
||||
this.scene?.eventSystem.emitSync('entity_died', { entity });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Performance Monitoring
|
||||
|
||||
Systems have built-in performance monitoring:
|
||||
|
||||
```typescript
|
||||
@ECSSystem('Performance')
|
||||
class PerformanceSystem extends EntitySystem {
|
||||
protected onEnd(): void {
|
||||
// Get performance data
|
||||
const perfData = this.getPerformanceData();
|
||||
if (perfData) {
|
||||
console.log(`Execution time: ${perfData.executionTime.toFixed(2)}ms`);
|
||||
}
|
||||
|
||||
// Get performance statistics
|
||||
const stats = this.getPerformanceStats();
|
||||
if (stats) {
|
||||
console.log(`Average execution time: ${stats.averageTime.toFixed(2)}ms`);
|
||||
}
|
||||
}
|
||||
|
||||
public resetPerformance(): void {
|
||||
this.resetPerformanceData();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## System Management
|
||||
|
||||
### Adding Systems to Scene
|
||||
|
||||
The framework provides two ways to add systems: pass an instance or pass a type (automatic dependency injection).
|
||||
|
||||
```typescript
|
||||
// Add systems in scene subclass
|
||||
class GameScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// Method 1: Pass instance
|
||||
this.addSystem(new MovementSystem());
|
||||
this.addSystem(new RenderSystem());
|
||||
|
||||
// Method 2: Pass type (automatic dependency injection)
|
||||
this.addEntityProcessor(PhysicsSystem);
|
||||
|
||||
// Set system update order
|
||||
const movementSystem = this.getSystem(MovementSystem);
|
||||
if (movementSystem) {
|
||||
movementSystem.updateOrder = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### System Dependency Injection
|
||||
|
||||
Systems implement the `IService` interface and support obtaining other services or systems through dependency injection:
|
||||
|
||||
```typescript
|
||||
import { ECSSystem, Injectable, Inject } from '@esengine/ecs-framework';
|
||||
|
||||
@Injectable()
|
||||
@ECSSystem('Physics')
|
||||
class PhysicsSystem extends EntitySystem {
|
||||
constructor(
|
||||
@Inject(CollisionService) private collision: CollisionService
|
||||
) {
|
||||
super(Matcher.all(Transform, RigidBody));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// Use injected service
|
||||
this.collision.detectCollisions(entities);
|
||||
}
|
||||
|
||||
// Implement IService interface dispose method
|
||||
public dispose(): void {
|
||||
// Clean up resources
|
||||
}
|
||||
}
|
||||
|
||||
// Just pass the type when using, framework will auto-inject dependencies
|
||||
class GameScene extends Scene {
|
||||
protected initialize(): void {
|
||||
// Automatic dependency injection
|
||||
this.addEntityProcessor(PhysicsSystem);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Notes:
|
||||
- Use `@Injectable()` decorator to mark systems that need dependency injection
|
||||
- Use `@Inject()` decorator in constructor parameters to declare dependencies
|
||||
- Systems must implement the `dispose()` method (IService interface requirement)
|
||||
- Use `addEntityProcessor(Type)` instead of `addSystem(new Type())` to enable dependency injection
|
||||
|
||||
### System Update Order
|
||||
|
||||
System execution order is determined by the `updateOrder` property. Lower values execute first:
|
||||
|
||||
```typescript
|
||||
@ECSSystem('Input')
|
||||
class InputSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(InputComponent));
|
||||
this.updateOrder = -100; // Input system executes first
|
||||
}
|
||||
}
|
||||
|
||||
@ECSSystem('Physics')
|
||||
class PhysicsSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(RigidBody));
|
||||
this.updateOrder = 0; // Default order
|
||||
}
|
||||
}
|
||||
|
||||
@ECSSystem('Render')
|
||||
class RenderSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(Sprite, Transform));
|
||||
this.updateOrder = 100; // Render system executes last
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Stable Sorting: addOrder
|
||||
|
||||
When multiple systems have the same `updateOrder`, the framework uses `addOrder` (add order) as a secondary sorting criterion to ensure stable and predictable results:
|
||||
|
||||
```typescript
|
||||
// Both systems have default updateOrder of 0
|
||||
@ECSSystem('SystemA')
|
||||
class SystemA extends EntitySystem { /* ... */ }
|
||||
|
||||
@ECSSystem('SystemB')
|
||||
class SystemB extends EntitySystem { /* ... */ }
|
||||
|
||||
// Add order determines execution order
|
||||
scene.addSystem(new SystemA()); // addOrder = 0, executes first
|
||||
scene.addSystem(new SystemB()); // addOrder = 1, executes second
|
||||
```
|
||||
|
||||
> **Note**: `addOrder` is automatically set by the framework when calling `addSystem`, no manual management needed. This ensures systems with the same `updateOrder` execute in their addition order, avoiding random behavior from unstable sorting.
|
||||
|
||||
## Complex System Examples
|
||||
|
||||
### Collision Detection System
|
||||
|
||||
```typescript
|
||||
@ECSSystem('Collision')
|
||||
class CollisionSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(Transform, Collider));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// Simple n² collision detection
|
||||
for (let i = 0; i < entities.length; i++) {
|
||||
for (let j = i + 1; j < entities.length; j++) {
|
||||
this.checkCollision(entities[i], entities[j]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private checkCollision(entityA: Entity, entityB: Entity): void {
|
||||
const transformA = entityA.getComponent(Transform);
|
||||
const transformB = entityB.getComponent(Transform);
|
||||
const colliderA = entityA.getComponent(Collider);
|
||||
const colliderB = entityB.getComponent(Collider);
|
||||
|
||||
if (this.isColliding(transformA, colliderA, transformB, colliderB)) {
|
||||
// Send collision event
|
||||
this.scene?.eventSystem.emitSync('collision', {
|
||||
entityA,
|
||||
entityB
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private isColliding(transformA: Transform, colliderA: Collider,
|
||||
transformB: Transform, colliderB: Collider): boolean {
|
||||
// Collision detection logic
|
||||
return false; // Simplified example
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### State Machine System
|
||||
|
||||
```typescript
|
||||
@ECSSystem('StateMachine')
|
||||
class StateMachineSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(StateMachine));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const stateMachine = entity.getComponent(StateMachine);
|
||||
if (stateMachine) {
|
||||
stateMachine.updateTimer(Time.deltaTime);
|
||||
this.updateState(entity, stateMachine);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private updateState(entity: Entity, stateMachine: StateMachine): void {
|
||||
switch (stateMachine.currentState) {
|
||||
case EntityState.Idle:
|
||||
this.handleIdleState(entity, stateMachine);
|
||||
break;
|
||||
case EntityState.Moving:
|
||||
this.handleMovingState(entity, stateMachine);
|
||||
break;
|
||||
case EntityState.Attacking:
|
||||
this.handleAttackingState(entity, stateMachine);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private handleIdleState(entity: Entity, stateMachine: StateMachine): void {
|
||||
// Idle state logic
|
||||
}
|
||||
|
||||
private handleMovingState(entity: Entity, stateMachine: StateMachine): void {
|
||||
// Moving state logic
|
||||
}
|
||||
|
||||
private handleAttackingState(entity: Entity, stateMachine: StateMachine): void {
|
||||
// Attacking state logic
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Single Responsibility for Systems
|
||||
|
||||
```typescript
|
||||
// Good system design - single responsibility
|
||||
@ECSSystem('Movement')
|
||||
class MovementSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(Position, Velocity));
|
||||
}
|
||||
}
|
||||
|
||||
@ECSSystem('Rendering')
|
||||
class RenderingSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(Sprite, Transform));
|
||||
}
|
||||
}
|
||||
|
||||
// Avoid - too many responsibilities
|
||||
@ECSSystem('GameSystem')
|
||||
class GameSystem extends EntitySystem {
|
||||
// One system handling movement, rendering, sound effects, and more
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Use @ECSSystem Decorator
|
||||
|
||||
`@ECSSystem` is a required decorator for system classes, providing type identification and metadata management.
|
||||
|
||||
#### Why It's Required
|
||||
|
||||
| Feature | Description |
|
||||
|---------|-------------|
|
||||
| **Type Identification** | Provides stable system names that remain correct after code obfuscation |
|
||||
| **Debug Support** | Shows readable system names in performance monitoring, logs, and debug tools |
|
||||
| **System Management** | Find and manage systems by name |
|
||||
| **Serialization Support** | Records system configuration during scene serialization |
|
||||
|
||||
#### Basic Syntax
|
||||
|
||||
```typescript
|
||||
@ECSSystem(systemName: string)
|
||||
```
|
||||
|
||||
- `systemName`: The system's name, recommend using descriptive names
|
||||
|
||||
#### Usage Example
|
||||
|
||||
```typescript
|
||||
// Correct usage
|
||||
@ECSSystem('Physics')
|
||||
class PhysicsSystem extends EntitySystem {
|
||||
// System implementation
|
||||
}
|
||||
|
||||
// Recommended: Use descriptive names
|
||||
@ECSSystem('PlayerMovement')
|
||||
class PlayerMovementSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(Player, Position, Velocity));
|
||||
}
|
||||
}
|
||||
|
||||
// Wrong - no decorator
|
||||
class BadSystem extends EntitySystem {
|
||||
// Systems defined this way may have issues in production:
|
||||
// 1. Class name changes after code minification, can't identify correctly
|
||||
// 2. Performance monitoring and debug tools show incorrect names
|
||||
}
|
||||
```
|
||||
|
||||
#### System Name Usage
|
||||
|
||||
```typescript
|
||||
@ECSSystem('Combat')
|
||||
class CombatSystem extends EntitySystem {
|
||||
protected onInitialize(): void {
|
||||
// Access system name using systemName property
|
||||
console.log(`System ${this.systemName} initialized`); // Output: System Combat initialized
|
||||
}
|
||||
}
|
||||
|
||||
// Find system by name
|
||||
const combat = scene.getSystemByName('Combat');
|
||||
|
||||
// Performance monitoring displays system name
|
||||
const perfData = combatSystem.getPerformanceData();
|
||||
console.log(`${combatSystem.systemName} execution time: ${perfData?.executionTime}ms`);
|
||||
```
|
||||
|
||||
### 3. Proper Update Order
|
||||
|
||||
```typescript
|
||||
// Set system update order by logical sequence
|
||||
@ECSSystem('Input')
|
||||
class InputSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super();
|
||||
this.updateOrder = -100; // Process input first
|
||||
}
|
||||
}
|
||||
|
||||
@ECSSystem('Logic')
|
||||
class GameLogicSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super();
|
||||
this.updateOrder = 0; // Process game logic
|
||||
}
|
||||
}
|
||||
|
||||
@ECSSystem('Render')
|
||||
class RenderSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super();
|
||||
this.updateOrder = 100; // Render last
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Avoid Direct References Between Systems
|
||||
|
||||
```typescript
|
||||
// Avoid: Direct system references
|
||||
@ECSSystem('Bad')
|
||||
class BadSystem extends EntitySystem {
|
||||
private otherSystem: SomeOtherSystem; // Avoid direct references to other systems
|
||||
}
|
||||
|
||||
// Recommended: Communicate through event system
|
||||
@ECSSystem('Good')
|
||||
class GoodSystem extends EntitySystem {
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
// Communicate with other systems through event system
|
||||
this.scene?.eventSystem.emitSync('data_updated', { entities });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Clean Up Resources Promptly
|
||||
|
||||
```typescript
|
||||
@ECSSystem('Resource')
|
||||
class ResourceSystem extends EntitySystem {
|
||||
private resources: Map<string, any> = new Map();
|
||||
|
||||
protected onDestroy(): void {
|
||||
// Clean up resources
|
||||
for (const [key, resource] of this.resources) {
|
||||
if (resource.dispose) {
|
||||
resource.dispose();
|
||||
}
|
||||
}
|
||||
this.resources.clear();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Systems are the logic processing core of ECS architecture. Properly designing and using systems makes your game code more modular, efficient, and maintainable.
|
||||
@@ -216,13 +216,11 @@ class ExampleSystem extends EntitySystem {
|
||||
// 主要的处理逻辑
|
||||
for (const entity of entities) {
|
||||
// 处理每个实体
|
||||
// ✅ 可以安全地在这里添加/移除组件,不会影响当前迭代
|
||||
}
|
||||
}
|
||||
|
||||
protected lateProcess(entities: readonly Entity[]): void {
|
||||
// 主处理之后的后期处理
|
||||
// ✅ 可以安全地在这里添加/移除组件,不会影响当前迭代
|
||||
}
|
||||
|
||||
protected onEnd(): void {
|
||||
@@ -272,68 +270,6 @@ class EnemyManagerSystem extends EntitySystem {
|
||||
}
|
||||
```
|
||||
|
||||
### 重要:onAdded/onRemoved 的调用时机
|
||||
|
||||
> ⚠️ **注意**:`onAdded` 和 `onRemoved` 回调是**同步调用**的,会在 `addComponent`/`removeComponent` 返回**之前**立即执行。
|
||||
|
||||
这意味着:
|
||||
|
||||
```typescript
|
||||
// ❌ 错误的用法:链式赋值在 onAdded 之后才执行
|
||||
const comp = entity.addComponent(new ClickComponent());
|
||||
comp.element = this._element; // 此时 onAdded 已经执行完了!
|
||||
|
||||
// ✅ 正确的用法:通过构造函数传入初始值
|
||||
const comp = entity.addComponent(new ClickComponent(this._element));
|
||||
|
||||
// ✅ 或者使用 createComponent 方法
|
||||
const comp = entity.createComponent(ClickComponent, this._element);
|
||||
```
|
||||
|
||||
**为什么这样设计?**
|
||||
|
||||
事件驱动设计确保 `onAdded`/`onRemoved` 回调不受系统注册顺序的影响。当组件被添加时,所有监听该组件的系统都会立即收到通知,而不是等到下一帧。
|
||||
|
||||
**最佳实践:**
|
||||
|
||||
1. 组件的初始值应该通过**构造函数**传入
|
||||
2. 不要依赖 `addComponent` 返回后再设置属性
|
||||
3. 如果需要在 `onAdded` 中访问组件属性,确保这些属性在构造时已经设置
|
||||
|
||||
### 在 process/lateProcess 中安全地修改组件
|
||||
|
||||
在 `process` 或 `lateProcess` 中迭代实体时,可以安全地添加或移除组件,不会影响当前的迭代过程:
|
||||
|
||||
```typescript
|
||||
@ECSSystem('Damage')
|
||||
class DamageSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(Health, DamageReceiver));
|
||||
}
|
||||
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const health = entity.getComponent(Health);
|
||||
const damage = entity.getComponent(DamageReceiver);
|
||||
|
||||
if (health && damage) {
|
||||
health.current -= damage.amount;
|
||||
|
||||
// ✅ 安全:移除组件不会影响当前迭代
|
||||
entity.removeComponent(damage);
|
||||
|
||||
if (health.current <= 0) {
|
||||
// ✅ 安全:添加组件也不会影响当前迭代
|
||||
entity.addComponent(new Dead());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
框架会在每次 `process`/`lateProcess` 调用前创建实体列表的快照,确保迭代过程中的组件变化不会导致跳过实体或重复处理。
|
||||
|
||||
## 系统属性和方法
|
||||
|
||||
### 重要属性
|
||||
@@ -521,8 +457,6 @@ class GameScene extends Scene {
|
||||
|
||||
### 系统更新顺序
|
||||
|
||||
系统的执行顺序由 `updateOrder` 属性决定,数值越小越先执行:
|
||||
|
||||
```typescript
|
||||
@ECSSystem('Input')
|
||||
class InputSystem extends EntitySystem {
|
||||
@@ -549,25 +483,6 @@ class RenderSystem extends EntitySystem {
|
||||
}
|
||||
```
|
||||
|
||||
#### 稳定排序:addOrder
|
||||
|
||||
当多个系统的 `updateOrder` 相同时,框架使用 `addOrder`(添加顺序)作为第二排序条件,确保排序结果稳定可预测:
|
||||
|
||||
```typescript
|
||||
// 这两个系统 updateOrder 都是默认值 0
|
||||
@ECSSystem('SystemA')
|
||||
class SystemA extends EntitySystem { /* ... */ }
|
||||
|
||||
@ECSSystem('SystemB')
|
||||
class SystemB extends EntitySystem { /* ... */ }
|
||||
|
||||
// 添加顺序决定了执行顺序
|
||||
scene.addSystem(new SystemA()); // addOrder = 0,先执行
|
||||
scene.addSystem(new SystemB()); // addOrder = 1,后执行
|
||||
```
|
||||
|
||||
> **注意**:`addOrder` 由框架在 `addSystem` 时自动设置,无需手动管理。这确保了相同 `updateOrder` 的系统按照添加顺序执行,避免了排序不稳定导致的随机行为。
|
||||
|
||||
## 复杂系统示例
|
||||
|
||||
### 碰撞检测系统
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@esengine/ecs-framework",
|
||||
"version": "2.2.21",
|
||||
"version": "2.2.18",
|
||||
"description": "用于Laya、Cocos Creator等JavaScript游戏引擎的高性能ECS框架",
|
||||
"main": "bin/index.js",
|
||||
"types": "bin/index.d.ts",
|
||||
|
||||
@@ -21,14 +21,6 @@ export class ComponentRegistry {
|
||||
private static maskCache = new Map<string, BitMask64Data>();
|
||||
private static nextBitIndex = 0;
|
||||
|
||||
/**
|
||||
* 热更新模式标志,默认禁用
|
||||
* Hot reload mode flag, disabled by default
|
||||
* 编辑器环境应启用此选项以支持脚本热更新
|
||||
* Editor environment should enable this to support script hot reload
|
||||
*/
|
||||
private static hotReloadEnabled = false;
|
||||
|
||||
/**
|
||||
* 注册组件类型并分配位掩码
|
||||
* @param componentType 组件类型
|
||||
@@ -42,27 +34,11 @@ export class ComponentRegistry {
|
||||
return existingIndex;
|
||||
}
|
||||
|
||||
// 检查是否有同名但不同类的组件已注册(热更新场景)
|
||||
// Check if a component with the same name but different class is registered (hot reload scenario)
|
||||
if (this.hotReloadEnabled && this.componentNameToType.has(typeName)) {
|
||||
// 检查是否有同名但不同类的组件已注册
|
||||
if (this.componentNameToType.has(typeName)) {
|
||||
const existingType = this.componentNameToType.get(typeName);
|
||||
if (existingType !== componentType) {
|
||||
// 热更新:替换旧的类为新的类,复用相同的 bitIndex
|
||||
// Hot reload: replace old class with new class, reuse the same bitIndex
|
||||
const existingIndex = this.componentTypes.get(existingType!)!;
|
||||
|
||||
// 移除旧类的映射
|
||||
// Remove old class mapping
|
||||
this.componentTypes.delete(existingType!);
|
||||
|
||||
// 用新类更新映射
|
||||
// Update mappings with new class
|
||||
this.componentTypes.set(componentType, existingIndex);
|
||||
this.bitIndexToType.set(existingIndex, componentType);
|
||||
this.componentNameToType.set(typeName, componentType);
|
||||
|
||||
console.log(`[ComponentRegistry] Hot reload: replaced component "${typeName}"`);
|
||||
return existingIndex;
|
||||
console.warn(`[ComponentRegistry] Component name conflict: "${typeName}" already registered with different class. Existing: ${existingType?.name}, New: ${componentType.name}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -233,32 +209,6 @@ export class ComponentRegistry {
|
||||
this.maskCache.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用热更新模式
|
||||
* Enable hot reload mode
|
||||
* 在编辑器环境中调用以支持脚本热更新
|
||||
* Call in editor environment to support script hot reload
|
||||
*/
|
||||
public static enableHotReload(): void {
|
||||
this.hotReloadEnabled = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 禁用热更新模式
|
||||
* Disable hot reload mode
|
||||
*/
|
||||
public static disableHotReload(): void {
|
||||
this.hotReloadEnabled = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查热更新模式是否启用
|
||||
* Check if hot reload mode is enabled
|
||||
*/
|
||||
public static isHotReloadEnabled(): boolean {
|
||||
return this.hotReloadEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置注册表(用于测试)
|
||||
*/
|
||||
@@ -269,6 +219,5 @@ export class ComponentRegistry {
|
||||
this.componentNameToId.clear();
|
||||
this.maskCache.clear();
|
||||
this.nextBitIndex = 0;
|
||||
this.hotReloadEnabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -321,19 +321,11 @@ export class Entity {
|
||||
|
||||
/**
|
||||
* 通知Scene中的QuerySystem实体组件发生变动
|
||||
*
|
||||
* Notify the QuerySystem in Scene that entity components have changed
|
||||
*
|
||||
* @param changedComponentType 变化的组件类型(可选,用于优化通知) | Changed component type (optional, for optimized notification)
|
||||
*/
|
||||
private notifyQuerySystems(changedComponentType?: ComponentType): void {
|
||||
private notifyQuerySystems(): void {
|
||||
if (this.scene && this.scene.querySystem) {
|
||||
this.scene.querySystem.updateEntity(this);
|
||||
this.scene.clearSystemEntityCaches();
|
||||
// 事件驱动:立即通知关心该组件的系统 | Event-driven: notify systems that care about this component
|
||||
if (this.scene.notifyEntityComponentChanged) {
|
||||
this.scene.notifyEntityComponentChanged(this, changedComponentType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -375,15 +367,7 @@ export class Entity {
|
||||
if (this.scene.referenceTracker) {
|
||||
this.scene.referenceTracker.registerEntityScene(this.id, this.scene);
|
||||
}
|
||||
|
||||
// 编辑器模式下延迟执行 onAddedToEntity | Defer onAddedToEntity in editor mode
|
||||
if (this.scene.isEditorMode) {
|
||||
this.scene.queueDeferredComponentCallback(() => {
|
||||
component.onAddedToEntity();
|
||||
});
|
||||
} else {
|
||||
component.onAddedToEntity();
|
||||
}
|
||||
component.onAddedToEntity();
|
||||
|
||||
if (this.scene && this.scene.eventSystem) {
|
||||
this.scene.eventSystem.emitSync('component:added', {
|
||||
@@ -397,7 +381,7 @@ export class Entity {
|
||||
});
|
||||
}
|
||||
|
||||
this.notifyQuerySystems(componentType);
|
||||
this.notifyQuerySystems();
|
||||
|
||||
return component;
|
||||
}
|
||||
@@ -530,7 +514,7 @@ export class Entity {
|
||||
});
|
||||
}
|
||||
|
||||
this.notifyQuerySystems(componentType);
|
||||
this.notifyQuerySystems();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Entity } from './Entity';
|
||||
import { EntityList } from './Utils/EntityList';
|
||||
import { IdentifierPool } from './Utils/IdentifierPool';
|
||||
import { EntitySystem } from './Systems/EntitySystem';
|
||||
import { ComponentStorageManager, ComponentType } from './Core/ComponentStorage';
|
||||
import { ComponentStorageManager } from './Core/ComponentStorage';
|
||||
import { QuerySystem } from './Core/QuerySystem';
|
||||
import { TypeSafeEventSystem } from './Core/EventSystem';
|
||||
import type { ReferenceTracker } from './Core/ReferenceTracker';
|
||||
@@ -78,18 +78,6 @@ export interface IScene {
|
||||
*/
|
||||
readonly services: ServiceContainer;
|
||||
|
||||
/**
|
||||
* 编辑器模式标志
|
||||
*
|
||||
* 当为 true 时,组件的生命周期回调(如 onAddedToEntity)会被延迟,
|
||||
* 直到调用 begin() 开始运行场景时才会触发。
|
||||
*
|
||||
* Editor mode flag.
|
||||
* When true, component lifecycle callbacks (like onAddedToEntity) are deferred
|
||||
* until begin() is called to start running the scene.
|
||||
*/
|
||||
isEditorMode: boolean;
|
||||
|
||||
/**
|
||||
* 获取系统列表
|
||||
*/
|
||||
@@ -110,15 +98,6 @@ export interface IScene {
|
||||
*/
|
||||
unload(): void;
|
||||
|
||||
/**
|
||||
* 添加延迟的组件生命周期回调
|
||||
*
|
||||
* Queue a deferred component lifecycle callback.
|
||||
*
|
||||
* @param callback 要延迟执行的回调 | The callback to defer
|
||||
*/
|
||||
queueDeferredComponentCallback(callback: () => void): void;
|
||||
|
||||
/**
|
||||
* 开始场景
|
||||
*/
|
||||
@@ -141,26 +120,9 @@ export interface IScene {
|
||||
|
||||
/**
|
||||
* 清除所有EntitySystem的实体缓存
|
||||
* Clear all EntitySystem entity caches
|
||||
*/
|
||||
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;
|
||||
|
||||
/**
|
||||
* 添加实体
|
||||
*/
|
||||
|
||||
@@ -117,30 +117,6 @@ export class Scene implements IScene {
|
||||
*/
|
||||
private _didSceneBegin: boolean = false;
|
||||
|
||||
/**
|
||||
* 编辑器模式标志
|
||||
*
|
||||
* 当为 true 时,组件的生命周期回调(如 onAddedToEntity)会被延迟,
|
||||
* 直到调用 begin() 开始运行场景时才会触发。
|
||||
*
|
||||
* Editor mode flag.
|
||||
* When true, component lifecycle callbacks (like onAddedToEntity) are deferred
|
||||
* until begin() is called to start running the scene.
|
||||
*/
|
||||
public isEditorMode: boolean = false;
|
||||
|
||||
/**
|
||||
* 延迟的组件生命周期回调队列
|
||||
*
|
||||
* 在编辑器模式下,组件的 onAddedToEntity 回调会被加入此队列,
|
||||
* 等到 begin() 调用时统一执行。
|
||||
*
|
||||
* Deferred component lifecycle callback queue.
|
||||
* In editor mode, component's onAddedToEntity callbacks are queued here,
|
||||
* and will be executed when begin() is called.
|
||||
*/
|
||||
private _deferredComponentCallbacks: Array<() => void> = [];
|
||||
|
||||
/**
|
||||
* 系统列表缓存
|
||||
*/
|
||||
@@ -175,30 +151,6 @@ export class Scene implements IScene {
|
||||
*/
|
||||
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
|
||||
*
|
||||
@@ -343,47 +295,14 @@ export class Scene implements IScene {
|
||||
*/
|
||||
public unload(): void {}
|
||||
|
||||
/**
|
||||
* 添加延迟的组件生命周期回调
|
||||
*
|
||||
* 在编辑器模式下,组件的 onAddedToEntity 回调会通过此方法加入队列。
|
||||
*
|
||||
* Queue a deferred component lifecycle callback.
|
||||
* In editor mode, component's onAddedToEntity callbacks are queued via this method.
|
||||
*
|
||||
* @param callback 要延迟执行的回调 | The callback to defer
|
||||
*/
|
||||
public queueDeferredComponentCallback(callback: () => void): void {
|
||||
this._deferredComponentCallbacks.push(callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始场景,启动实体处理器等
|
||||
*
|
||||
* 这个方法会启动场景。它将启动实体处理器等,并调用onStart方法。
|
||||
* 在编辑器模式下,此方法还会执行所有延迟的组件生命周期回调。
|
||||
*
|
||||
* This method starts the scene. It will start entity processors and call onStart.
|
||||
* In editor mode, this method also executes all deferred component lifecycle callbacks.
|
||||
*/
|
||||
public begin() {
|
||||
// 标记场景已开始运行
|
||||
// 标记场景已开始运行并调用onStart方法
|
||||
this._didSceneBegin = true;
|
||||
|
||||
// 执行所有延迟的组件生命周期回调 | Execute all deferred component lifecycle callbacks
|
||||
if (this._deferredComponentCallbacks.length > 0) {
|
||||
for (const callback of this._deferredComponentCallbacks) {
|
||||
try {
|
||||
callback();
|
||||
} catch (error) {
|
||||
this.logger.error('Error executing deferred component callback:', error);
|
||||
}
|
||||
}
|
||||
// 清空队列 | Clear the queue
|
||||
this._deferredComponentCallbacks = [];
|
||||
}
|
||||
|
||||
// 调用onStart方法
|
||||
this.onStart();
|
||||
}
|
||||
|
||||
@@ -425,10 +344,6 @@ export class Scene implements IScene {
|
||||
// 清空系统缓存
|
||||
this._cachedSystems = null;
|
||||
this._systemsOrderDirty = true;
|
||||
|
||||
// 清空组件索引 | Clear component indices
|
||||
this._componentIdToSystems.clear();
|
||||
this._globalNotifySystems.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -538,146 +453,6 @@ 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 要添加的实体
|
||||
@@ -963,9 +738,6 @@ export class Scene implements IScene {
|
||||
// 标记系统列表已变化
|
||||
this.markSystemsOrderDirty();
|
||||
|
||||
// 建立组件类型到系统的索引 | Build component type to system index
|
||||
this.indexSystemByComponents(system);
|
||||
|
||||
injectProperties(system, this._services);
|
||||
|
||||
// 调试模式下自动包装系统方法以收集性能数据(ProfilerSDK 启用时表示调试模式)
|
||||
@@ -1050,9 +822,6 @@ export class Scene implements IScene {
|
||||
// 标记系统列表已变化
|
||||
this.markSystemsOrderDirty();
|
||||
|
||||
// 从组件类型索引中移除 | Remove from component type index
|
||||
this.removeSystemFromIndex(processor);
|
||||
|
||||
// 重置System状态
|
||||
processor.reset();
|
||||
}
|
||||
|
||||
@@ -235,7 +235,7 @@ export abstract class EntitySystem implements ISystemBase, IService {
|
||||
* 在系统创建时调用。框架内部使用,用户不应直接调用。
|
||||
*/
|
||||
public initialize(): void {
|
||||
// 防止重复初始化 | Prevent re-initialization
|
||||
// 防止重复初始化
|
||||
if (this._initialized) {
|
||||
return;
|
||||
}
|
||||
@@ -243,20 +243,13 @@ export abstract class EntitySystem implements ISystemBase, IService {
|
||||
this._initialized = true;
|
||||
|
||||
// 框架内部初始化:触发一次实体查询,以便正确跟踪现有实体
|
||||
// Framework initialization: query entities once to track existing entities
|
||||
if (this.scene) {
|
||||
// 清理缓存确保初始化时重新查询 | Clear cache to ensure fresh query
|
||||
// 清理缓存确保初始化时重新查询
|
||||
this._entityCache.invalidate();
|
||||
const entities = this.queryEntities();
|
||||
|
||||
// 初始化时对已存在的匹配实体触发 onAdded
|
||||
// Trigger onAdded for existing matching entities during initialization
|
||||
for (const entity of entities) {
|
||||
this.onAdded(entity);
|
||||
}
|
||||
this.queryEntities();
|
||||
}
|
||||
|
||||
// 调用用户可重写的初始化方法 | Call user-overridable initialization method
|
||||
// 调用用户可重写的初始化方法
|
||||
this.onInitialize();
|
||||
}
|
||||
|
||||
@@ -601,13 +594,10 @@ export abstract class EntitySystem implements ISystemBase, IService {
|
||||
// 查询实体并存储到帧缓存中
|
||||
// 响应式查询会自动维护最新的实体列表,updateEntityTracking会在检测到变化时invalidate
|
||||
const queriedEntities = this.queryEntities();
|
||||
// 创建数组副本以防止迭代过程中数组被修改
|
||||
// Create a copy to prevent array modification during iteration
|
||||
const entities = [...queriedEntities];
|
||||
this._entityCache.setFrame(entities);
|
||||
entityCount = entities.length;
|
||||
this._entityCache.setFrame(queriedEntities);
|
||||
entityCount = queriedEntities.length;
|
||||
|
||||
this.process(entities);
|
||||
this.process(queriedEntities);
|
||||
} finally {
|
||||
monitor.endMonitoring(this._systemName, startTime, entityCount);
|
||||
}
|
||||
@@ -626,15 +616,8 @@ export abstract class EntitySystem implements ISystemBase, IService {
|
||||
let entityCount = 0;
|
||||
|
||||
try {
|
||||
// 重新查询实体以获取最新列表
|
||||
// 在 update 和 lateUpdate 之间可能有新组件被添加(事件驱动设计)
|
||||
// Re-query entities to get the latest list
|
||||
// New components may have been added between update and lateUpdate (event-driven design)
|
||||
const queriedEntities = this.queryEntities();
|
||||
// 创建数组副本以防止迭代过程中数组被修改
|
||||
// Create a copy to prevent array modification during iteration
|
||||
const entities = [...queriedEntities];
|
||||
this._entityCache.setFrame(entities);
|
||||
// 使用缓存的实体列表,避免重复查询
|
||||
const entities = this._entityCache.getFrame() || [];
|
||||
entityCount = entities.length;
|
||||
this.lateProcess(entities);
|
||||
this.onEnd();
|
||||
@@ -735,151 +718,32 @@ export abstract class EntitySystem implements ISystemBase, IService {
|
||||
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 {
|
||||
const currentSet = new Set(currentEntities);
|
||||
let hasChanged = false;
|
||||
|
||||
// 检查新增的实体 | Check for newly added entities
|
||||
// 检查新增的实体
|
||||
for (const entity of currentEntities) {
|
||||
if (!this._entityCache.isTracked(entity)) {
|
||||
this._entityCache.addTracked(entity);
|
||||
this.onAdded(entity);
|
||||
hasChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 检查移除的实体 | Check for removed entities
|
||||
// 检查移除的实体
|
||||
for (const entity of this._entityCache.getTracked()) {
|
||||
if (!currentSet.has(entity)) {
|
||||
this._entityCache.removeTracked(entity);
|
||||
this.onRemoved(entity);
|
||||
hasChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果实体发生了变化,使缓存失效 | If entities changed, invalidate cache
|
||||
// 如果实体发生了变化,使缓存失效
|
||||
if (hasChanged) {
|
||||
this._entityCache.invalidate();
|
||||
}
|
||||
|
||||
@@ -25,12 +25,6 @@ import { HierarchyComponent } from '../Components/HierarchyComponent';
|
||||
export class HierarchySystem extends EntitySystem {
|
||||
private static readonly MAX_DEPTH = 32;
|
||||
|
||||
/**
|
||||
* 脏实体集合 - 只有这些实体需要在 process() 中更新缓存
|
||||
* Dirty entity set - only these entities need cache update in process()
|
||||
*/
|
||||
private dirtyEntities: Set<Entity> = new Set();
|
||||
|
||||
constructor() {
|
||||
super(Matcher.empty().all(HierarchyComponent));
|
||||
}
|
||||
@@ -42,19 +36,14 @@ export class HierarchySystem extends EntitySystem {
|
||||
return -1000;
|
||||
}
|
||||
|
||||
protected override process(_entities: readonly Entity[]): void {
|
||||
// 只更新脏实体,不遍历所有实体 | Only update dirty entities, no full iteration
|
||||
if (this.dirtyEntities.size === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const entity of this.dirtyEntities) {
|
||||
// 确保实体仍然有效 | Ensure entity is still valid
|
||||
if (entity.scene) {
|
||||
protected override process(): void {
|
||||
// 更新所有脏缓存
|
||||
for (const entity of this.entities) {
|
||||
const hierarchy = entity.getComponent(HierarchyComponent);
|
||||
if (hierarchy?.bCacheDirty) {
|
||||
this.updateHierarchyCache(entity);
|
||||
}
|
||||
}
|
||||
this.dirtyEntities.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -485,21 +474,15 @@ export class HierarchySystem extends EntitySystem {
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记缓存为脏,并添加到脏实体集合
|
||||
* Mark cache as dirty and add to dirty entity set
|
||||
* 标记缓存为脏
|
||||
*/
|
||||
private markCacheDirty(entity: Entity): void {
|
||||
const hierarchy = entity.getComponent(HierarchyComponent);
|
||||
if (!hierarchy) return;
|
||||
|
||||
// 如果已经是脏的,跳过(避免重复递归)
|
||||
// Skip if already dirty (avoid redundant recursion)
|
||||
if (hierarchy.bCacheDirty) return;
|
||||
|
||||
hierarchy.bCacheDirty = true;
|
||||
this.dirtyEntities.add(entity);
|
||||
|
||||
// 递归标记所有子级 | Recursively mark all children
|
||||
// 递归标记所有子级
|
||||
for (const childId of hierarchy.childIds) {
|
||||
const child = this.scene?.findEntityById(childId);
|
||||
if (child) {
|
||||
@@ -525,29 +508,14 @@ export class HierarchySystem extends EntitySystem {
|
||||
hierarchy.bCacheDirty = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 当实体被添加到系统时,将其加入脏集合
|
||||
* When entity is added to system, add it to dirty set
|
||||
*/
|
||||
protected override onAdded(entity: Entity): void {
|
||||
const hierarchy = entity.getComponent(HierarchyComponent);
|
||||
if (hierarchy && hierarchy.bCacheDirty) {
|
||||
this.dirtyEntities.add(entity);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 当实体被移除时清理层级关系
|
||||
* When entity is removed, clean up hierarchy relationships
|
||||
*/
|
||||
protected override onRemoved(entity: Entity): void {
|
||||
// 从脏集合中移除 | Remove from dirty set
|
||||
this.dirtyEntities.delete(entity);
|
||||
|
||||
protected onEntityRemoved(entity: Entity): void {
|
||||
const hierarchy = entity.getComponent(HierarchyComponent);
|
||||
if (!hierarchy) return;
|
||||
|
||||
// 从父级移除 | Remove from parent
|
||||
// 从父级移除
|
||||
if (hierarchy.parentId !== null) {
|
||||
const parent = this.scene?.findEntityById(hierarchy.parentId);
|
||||
if (parent) {
|
||||
@@ -561,8 +529,8 @@ export class HierarchySystem extends EntitySystem {
|
||||
}
|
||||
}
|
||||
|
||||
// 处理子级:将子级移动到根级
|
||||
// Handle children: move children to root level
|
||||
// 处理子级:可选择销毁或移动到根级
|
||||
// 默认将子级移动到根级
|
||||
for (const childId of hierarchy.childIds) {
|
||||
const child = this.scene?.findEntityById(childId);
|
||||
if (child) {
|
||||
@@ -576,7 +544,6 @@ export class HierarchySystem extends EntitySystem {
|
||||
}
|
||||
|
||||
public override dispose(): void {
|
||||
// 清理脏实体集合 | Clear dirty entity set
|
||||
this.dirtyEntities.clear();
|
||||
// 清理资源
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,46 +188,6 @@ export class PlatformDetector {
|
||||
window.location !== undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测是否在 Tauri 桌面环境中运行
|
||||
* Check if running in Tauri desktop environment
|
||||
*
|
||||
* 同时支持 Tauri v1 (__TAURI__) 和 v2 (__TAURI_INTERNALS__)
|
||||
* Supports both Tauri v1 (__TAURI__) and v2 (__TAURI_INTERNALS__)
|
||||
*/
|
||||
public static isTauriEnvironment(): boolean {
|
||||
if (typeof window === 'undefined') {
|
||||
return false;
|
||||
}
|
||||
// Tauri v1 uses __TAURI__, Tauri v2 uses __TAURI_INTERNALS__
|
||||
return '__TAURI__' in window || '__TAURI_INTERNALS__' in window;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测是否在编辑器环境中运行
|
||||
* Check if running in editor environment
|
||||
*
|
||||
* 包括 Tauri 桌面应用或带 __ESENGINE_EDITOR__ 标记的环境
|
||||
* Includes Tauri desktop app or environments marked with __ESENGINE_EDITOR__
|
||||
*/
|
||||
public static isEditorEnvironment(): boolean {
|
||||
if (typeof window === 'undefined') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Tauri desktop app | Tauri 桌面应用
|
||||
if (this.isTauriEnvironment()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Editor marker | 编辑器标记
|
||||
if ('__ESENGINE_EDITOR__' in window) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取详细的环境信息(用于调试)
|
||||
*/
|
||||
|
||||
@@ -180,103 +180,6 @@ describe('ComponentRegistry Extended - 64+ 组件支持', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('热更新模式', () => {
|
||||
it('默认应该禁用热更新模式', () => {
|
||||
expect(ComponentRegistry.isHotReloadEnabled()).toBe(false);
|
||||
});
|
||||
|
||||
it('应该能够启用和禁用热更新模式', () => {
|
||||
expect(ComponentRegistry.isHotReloadEnabled()).toBe(false);
|
||||
|
||||
ComponentRegistry.enableHotReload();
|
||||
expect(ComponentRegistry.isHotReloadEnabled()).toBe(true);
|
||||
|
||||
ComponentRegistry.disableHotReload();
|
||||
expect(ComponentRegistry.isHotReloadEnabled()).toBe(false);
|
||||
});
|
||||
|
||||
it('reset 应该重置热更新模式为禁用', () => {
|
||||
ComponentRegistry.enableHotReload();
|
||||
expect(ComponentRegistry.isHotReloadEnabled()).toBe(true);
|
||||
|
||||
ComponentRegistry.reset();
|
||||
expect(ComponentRegistry.isHotReloadEnabled()).toBe(false);
|
||||
});
|
||||
|
||||
it('启用热更新时应该替换同名组件类', () => {
|
||||
ComponentRegistry.enableHotReload();
|
||||
|
||||
// 模拟热更新场景:两个不同的类但有相同的 constructor.name
|
||||
// Simulate hot reload: two different classes with same constructor.name
|
||||
const createHotReloadComponent = (version: number) => {
|
||||
// 使用 eval 创建具有相同名称的类
|
||||
// Use function constructor to create classes with same name
|
||||
const cls = (new Function('Component', `
|
||||
return class HotReloadTestComponent extends Component {
|
||||
constructor() {
|
||||
super();
|
||||
this.version = ${version};
|
||||
}
|
||||
}
|
||||
`))(Component);
|
||||
return cls;
|
||||
};
|
||||
|
||||
const TestComponentV1 = createHotReloadComponent(1);
|
||||
const TestComponentV2 = createHotReloadComponent(2);
|
||||
|
||||
// 确保两个类名相同但不是同一个类
|
||||
expect(TestComponentV1.name).toBe(TestComponentV2.name);
|
||||
expect(TestComponentV1).not.toBe(TestComponentV2);
|
||||
|
||||
const index1 = ComponentRegistry.register(TestComponentV1);
|
||||
const index2 = ComponentRegistry.register(TestComponentV2);
|
||||
|
||||
// 应该复用相同的 bitIndex
|
||||
expect(index1).toBe(index2);
|
||||
|
||||
// 新类应该替换旧类
|
||||
expect(ComponentRegistry.isRegistered(TestComponentV2)).toBe(true);
|
||||
expect(ComponentRegistry.isRegistered(TestComponentV1)).toBe(false);
|
||||
});
|
||||
|
||||
it('禁用热更新时不应该替换同名组件类', () => {
|
||||
// 确保热更新被禁用
|
||||
ComponentRegistry.disableHotReload();
|
||||
|
||||
// 创建两个同名组件
|
||||
// Create two classes with same constructor.name
|
||||
const createNoHotReloadComponent = (id: number) => {
|
||||
const cls = (new Function('Component', `
|
||||
return class NoHotReloadComponent extends Component {
|
||||
constructor() {
|
||||
super();
|
||||
this.id = ${id};
|
||||
}
|
||||
}
|
||||
`))(Component);
|
||||
return cls;
|
||||
};
|
||||
|
||||
const TestCompA = createNoHotReloadComponent(1);
|
||||
const TestCompB = createNoHotReloadComponent(2);
|
||||
|
||||
// 确保两个类名相同但不是同一个类
|
||||
expect(TestCompA.name).toBe(TestCompB.name);
|
||||
expect(TestCompA).not.toBe(TestCompB);
|
||||
|
||||
const index1 = ComponentRegistry.register(TestCompA);
|
||||
const index2 = ComponentRegistry.register(TestCompB);
|
||||
|
||||
// 应该分配不同的 bitIndex(因为热更新被禁用)
|
||||
expect(index2).toBe(index1 + 1);
|
||||
|
||||
// 两个类都应该被注册
|
||||
expect(ComponentRegistry.isRegistered(TestCompA)).toBe(true);
|
||||
expect(ComponentRegistry.isRegistered(TestCompB)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('边界情况', () => {
|
||||
it('应该正确处理第 64 个组件(边界)', () => {
|
||||
const scene = new Scene();
|
||||
|
||||
@@ -439,215 +439,6 @@ describe('EntitySystem', () => {
|
||||
|
||||
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 方法', () => {
|
||||
@@ -829,256 +620,4 @@ describe('EntitySystem', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('addComponent 后立即 getComponent', () => {
|
||||
it('addComponent 后应该能立即 getComponent 获取到组件', () => {
|
||||
// 使用独立场景 | Use independent scene
|
||||
const testScene = new Scene();
|
||||
|
||||
class ClickComponent extends Component {
|
||||
public element: string;
|
||||
constructor(element: string) {
|
||||
super();
|
||||
this.element = element;
|
||||
}
|
||||
}
|
||||
|
||||
const testEntity = testScene.createEntity('panel');
|
||||
|
||||
// 添加组件后立即获取 | Get component immediately after adding
|
||||
const comp = testEntity.addComponent(new ClickComponent('button'));
|
||||
const comp1 = testEntity.getComponent(ClickComponent);
|
||||
|
||||
expect(comp).not.toBeNull();
|
||||
expect(comp1).not.toBeNull();
|
||||
expect(comp).toBe(comp1);
|
||||
expect(comp1!.element).toBe('button');
|
||||
});
|
||||
|
||||
it('有系统监听时 addComponent 后应该能立即 getComponent', () => {
|
||||
// 使用独立场景 | Use independent scene
|
||||
const testScene = new Scene();
|
||||
|
||||
class ClickComponent extends Component {
|
||||
public element: string;
|
||||
constructor(element: string) {
|
||||
super();
|
||||
this.element = element;
|
||||
}
|
||||
}
|
||||
|
||||
// 添加一个监听该组件的系统 | Add a system that listens to this component
|
||||
class ClickSystem extends EntitySystem {
|
||||
public onAddedCount = 0;
|
||||
|
||||
constructor() {
|
||||
super(Matcher.all(ClickComponent));
|
||||
}
|
||||
|
||||
protected override onAdded(entity: Entity): void {
|
||||
this.onAddedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
const clickSystem = new ClickSystem();
|
||||
testScene.addSystem(clickSystem);
|
||||
|
||||
const testEntity = testScene.createEntity('panel');
|
||||
|
||||
// 添加组件后立即获取 | Get component immediately after adding
|
||||
const comp = testEntity.addComponent(new ClickComponent('button'));
|
||||
const comp1 = testEntity.getComponent(ClickComponent);
|
||||
|
||||
expect(comp).not.toBeNull();
|
||||
expect(comp1).not.toBeNull();
|
||||
expect(comp).toBe(comp1);
|
||||
expect(comp1!.element).toBe('button');
|
||||
|
||||
// onAdded 应该被触发 | onAdded should be triggered
|
||||
expect(clickSystem.onAddedCount).toBe(1);
|
||||
|
||||
testScene.removeSystem(clickSystem);
|
||||
});
|
||||
|
||||
it('系统在 onAdded 中移除组件时 getComponent 应返回 null', () => {
|
||||
// 使用独立场景 | Use independent scene
|
||||
const testScene = new Scene();
|
||||
|
||||
class ClickComponent extends Component {
|
||||
public element: string;
|
||||
constructor(element: string) {
|
||||
super();
|
||||
this.element = element;
|
||||
}
|
||||
}
|
||||
|
||||
// 这个系统在 onAdded 中移除组件(模拟可能的用户代码)
|
||||
// This system removes component in onAdded (simulating possible user code)
|
||||
class RemoveOnAddSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(ClickComponent));
|
||||
}
|
||||
|
||||
protected override onAdded(entity: Entity): void {
|
||||
// 在 onAdded 中移除组件 | Remove component in onAdded
|
||||
const comp = entity.getComponent(ClickComponent);
|
||||
if (comp) {
|
||||
entity.removeComponent(comp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const removeSystem = new RemoveOnAddSystem();
|
||||
testScene.addSystem(removeSystem);
|
||||
|
||||
const testEntity = testScene.createEntity('panel');
|
||||
|
||||
// 添加组件 - 会触发 onAdded,然后组件被移除
|
||||
// Add component - triggers onAdded, then component is removed
|
||||
const comp = testEntity.addComponent(new ClickComponent('button'));
|
||||
|
||||
// 此时 getComponent 应该返回 null,因为组件在 onAdded 中被移除了
|
||||
// getComponent should return null because component was removed in onAdded
|
||||
const comp1 = testEntity.getComponent(ClickComponent);
|
||||
|
||||
expect(comp).not.toBeNull(); // addComponent 返回值仍然有效
|
||||
expect(comp1).toBeNull(); // 但 getComponent 返回 null
|
||||
|
||||
testScene.removeSystem(removeSystem);
|
||||
});
|
||||
|
||||
it('模拟 lawn-mower-demo: CSystem 在 process 中添加 D 组件', () => {
|
||||
// 模拟 lawn-mower-demo 的场景 | Simulate lawn-mower-demo scenario
|
||||
const testScene = new Scene();
|
||||
|
||||
// 组件定义 | Component definitions
|
||||
class A extends Component {}
|
||||
class B extends Component {}
|
||||
class C extends Component {
|
||||
public aId: number;
|
||||
public bId: number;
|
||||
constructor(aId: number, bId: number) {
|
||||
super();
|
||||
this.aId = aId;
|
||||
this.bId = bId;
|
||||
}
|
||||
}
|
||||
class D extends Component {}
|
||||
|
||||
// ASystem: 匹配 A + D | Matches A + D
|
||||
class ASystem extends EntitySystem {
|
||||
public onAddedEntities: Entity[] = [];
|
||||
constructor() {
|
||||
super(Matcher.all(A, D));
|
||||
}
|
||||
protected override onAdded(entity: Entity): void {
|
||||
console.log('ASystem onAdded:', entity.name);
|
||||
this.onAddedEntities.push(entity);
|
||||
}
|
||||
}
|
||||
|
||||
// BSystem: 匹配 B + D | Matches B + D
|
||||
class BSystem extends EntitySystem {
|
||||
public onAddedEntities: Entity[] = [];
|
||||
constructor() {
|
||||
super(Matcher.all(B, D));
|
||||
}
|
||||
protected override onAdded(entity: Entity): void {
|
||||
console.log('BSystem onAdded:', entity.name);
|
||||
this.onAddedEntities.push(entity);
|
||||
}
|
||||
}
|
||||
|
||||
// CSystem: 在 process 中给 A 和 B 实体添加 D 组件
|
||||
// CSystem: Adds D component to A and B entities in process
|
||||
class CSystem extends EntitySystem {
|
||||
constructor() {
|
||||
super(Matcher.all(C));
|
||||
}
|
||||
protected override process(entities: readonly Entity[]): void {
|
||||
for (const entity of entities) {
|
||||
const c = entity.getComponent(C);
|
||||
if (c) {
|
||||
const a = this.scene!.findEntityById(c.aId);
|
||||
if (a && !a.hasComponent(D)) {
|
||||
console.log('CSystem: Adding D to Entity A');
|
||||
a.addComponent(new D());
|
||||
}
|
||||
const b = this.scene!.findEntityById(c.bId);
|
||||
if (b && !b.hasComponent(D)) {
|
||||
console.log('CSystem: Adding D to Entity B');
|
||||
b.addComponent(new D());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DSystem: 在 lateProcess 中移除 D 组件
|
||||
// DSystem: Removes D component in lateProcess
|
||||
class DSystem extends EntitySystem {
|
||||
public lateProcessEntities: Entity[] = [];
|
||||
constructor() {
|
||||
super(Matcher.all(D));
|
||||
}
|
||||
protected override lateProcess(entities: readonly Entity[]): void {
|
||||
console.log('DSystem lateProcess, entities count:', entities.length);
|
||||
for (const entity of entities) {
|
||||
console.log('DSystem removing D from:', entity.name);
|
||||
this.lateProcessEntities.push(entity);
|
||||
const d = entity.getComponent(D);
|
||||
if (d) {
|
||||
entity.removeComponent(d);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 按顺序添加系统(与 demo 一致)
|
||||
// Add systems in order (same as demo)
|
||||
const aSystem = new ASystem();
|
||||
const bSystem = new BSystem();
|
||||
const cSystem = new CSystem();
|
||||
const dSystem = new DSystem();
|
||||
|
||||
testScene.addSystem(aSystem);
|
||||
testScene.addSystem(bSystem);
|
||||
testScene.addSystem(cSystem);
|
||||
testScene.addSystem(dSystem);
|
||||
|
||||
// 创建实体 | Create entities
|
||||
const entity1 = testScene.createEntity('Entity1');
|
||||
entity1.addComponent(new A());
|
||||
|
||||
const entity2 = testScene.createEntity('Entity2');
|
||||
entity2.addComponent(new B());
|
||||
|
||||
const entity3 = testScene.createEntity('Entity3');
|
||||
entity3.addComponent(new C(entity1.id, entity2.id));
|
||||
|
||||
// 执行一帧 | Execute one frame
|
||||
testScene.update();
|
||||
|
||||
// 验证 ASystem 和 BSystem 都收到了 onAdded 通知
|
||||
// Verify ASystem and BSystem both received onAdded notification
|
||||
expect(aSystem.onAddedEntities.length).toBe(1);
|
||||
expect(aSystem.onAddedEntities[0]).toBe(entity1);
|
||||
|
||||
expect(bSystem.onAddedEntities.length).toBe(1);
|
||||
expect(bSystem.onAddedEntities[0]).toBe(entity2);
|
||||
|
||||
// 检查 DSystem 处理了哪些实体
|
||||
console.log('DSystem processed entities:', dSystem.lateProcessEntities.map(e => e.name));
|
||||
|
||||
// D 组件应该在 lateProcess 中被移除
|
||||
// D component should be removed in lateProcess
|
||||
expect(entity1.hasComponent(D)).toBe(false);
|
||||
expect(entity2.hasComponent(D)).toBe(false);
|
||||
|
||||
testScene.removeSystem(aSystem);
|
||||
testScene.removeSystem(bSystem);
|
||||
testScene.removeSystem(cSystem);
|
||||
testScene.removeSystem(dSystem);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
@@ -598,28 +598,6 @@ export class EngineBridge implements IEngineBridge {
|
||||
this.getEngine().setShowGizmos(show);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
if (!this.initialized) return;
|
||||
this.getEngine().setEditorMode(isEditor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get editor mode.
|
||||
* 获取编辑器模式。
|
||||
*/
|
||||
isEditorMode(): boolean {
|
||||
if (!this.initialized) return true;
|
||||
return this.getEngine().isEditorMode();
|
||||
}
|
||||
|
||||
// ===== Multi-viewport API =====
|
||||
// ===== 多视口 API =====
|
||||
|
||||
|
||||
@@ -117,11 +117,6 @@ export class GameEngine {
|
||||
* The shader ID for referencing this shader | 用于引用此着色器的ID
|
||||
*/
|
||||
compileShader(vertex_source: string, fragment_source: string): number;
|
||||
/**
|
||||
* Get editor mode.
|
||||
* 获取编辑器模式。
|
||||
*/
|
||||
isEditorMode(): boolean;
|
||||
/**
|
||||
* Render sprites as overlay (without clearing screen).
|
||||
* 渲染精灵作为叠加层(不清除屏幕)。
|
||||
@@ -161,15 +156,6 @@ export class GameEngine {
|
||||
* * `r`, `g`, `b`, `a` - Color components (0.0-1.0) | 颜色分量 (0.0-1.0)
|
||||
*/
|
||||
setClearColor(r: number, g: number, b: number, a: number): void;
|
||||
/**
|
||||
* Set editor mode.
|
||||
* 设置编辑器模式。
|
||||
*
|
||||
* When false (runtime mode), editor-only UI like grid, gizmos,
|
||||
* and axis indicator are automatically hidden.
|
||||
* 当为 false(运行时模式)时,编辑器专用 UI(如网格、gizmos、坐标轴指示器)会自动隐藏。
|
||||
*/
|
||||
setEditorMode(is_editor: boolean): void;
|
||||
/**
|
||||
* Set gizmo visibility.
|
||||
* 设置辅助工具可见性。
|
||||
@@ -388,7 +374,6 @@ export interface InitOutput {
|
||||
readonly gameengine_hasMaterial: (a: number, b: number) => number;
|
||||
readonly gameengine_hasShader: (a: number, b: number) => number;
|
||||
readonly gameengine_height: (a: number) => number;
|
||||
readonly gameengine_isEditorMode: (a: number) => number;
|
||||
readonly gameengine_isKeyDown: (a: number, b: number, c: number) => number;
|
||||
readonly gameengine_loadTexture: (a: number, b: number, c: number, d: number) => [number, number];
|
||||
readonly gameengine_loadTextureByPath: (a: number, b: number, c: number) => [number, number, number];
|
||||
@@ -404,7 +389,6 @@ export interface InitOutput {
|
||||
readonly gameengine_setActiveViewport: (a: number, b: number, c: number) => number;
|
||||
readonly gameengine_setCamera: (a: number, b: number, c: number, d: number, e: number) => void;
|
||||
readonly gameengine_setClearColor: (a: number, b: number, c: number, d: number, e: number) => void;
|
||||
readonly gameengine_setEditorMode: (a: number, b: number) => void;
|
||||
readonly gameengine_setMaterialBlendMode: (a: number, b: number, c: number) => number;
|
||||
readonly gameengine_setMaterialColor: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number) => number;
|
||||
readonly gameengine_setMaterialFloat: (a: number, b: number, c: number, d: number, e: number) => number;
|
||||
|
||||
2
packages/editor-app/.gitignore
vendored
2
packages/editor-app/.gitignore
vendored
@@ -1,4 +1,2 @@
|
||||
# Generated runtime files
|
||||
src-tauri/runtime/
|
||||
src-tauri/engine/
|
||||
src-tauri/bin/
|
||||
|
||||
@@ -22,11 +22,10 @@ if (!fs.existsSync(bundleDir)) {
|
||||
}
|
||||
|
||||
// Files to bundle
|
||||
// 需要打包的文件
|
||||
const filesToBundle = [
|
||||
{
|
||||
src: path.join(rootPath, 'packages/platform-web/dist/index.mjs'),
|
||||
dst: path.join(bundleDir, 'platform-web.mjs')
|
||||
src: path.join(rootPath, 'packages/platform-web/dist/runtime.browser.js'),
|
||||
dst: path.join(bundleDir, 'runtime.browser.js')
|
||||
},
|
||||
{
|
||||
src: path.join(rootPath, 'packages/engine/pkg/es_engine_bg.wasm'),
|
||||
@@ -38,25 +37,6 @@ 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
|
||||
let success = true;
|
||||
for (const { src, dst } of filesToBundle) {
|
||||
@@ -79,131 +59,34 @@ 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;
|
||||
// Update tauri.conf.json to include runtime directory
|
||||
if (success) {
|
||||
const tauriConfigPath = path.join(editorPath, 'src-tauri', 'tauri.conf.json');
|
||||
const config = JSON.parse(fs.readFileSync(tauriConfigPath, 'utf8'));
|
||||
|
||||
// Add runtime directory to resources
|
||||
if (!config.bundle) {
|
||||
config.bundle = {};
|
||||
}
|
||||
if (!config.bundle.resources) {
|
||||
config.bundle.resources = {};
|
||||
}
|
||||
|
||||
// Handle both array and object format for resources
|
||||
if (Array.isArray(config.bundle.resources)) {
|
||||
if (!config.bundle.resources.includes('runtime/**/*')) {
|
||||
config.bundle.resources.push('runtime/**/*');
|
||||
fs.writeFileSync(tauriConfigPath, JSON.stringify(config, null, 2));
|
||||
console.log('✓ Updated tauri.conf.json with runtime resources');
|
||||
}
|
||||
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Copy engine modules directory from dist/engine to src-tauri/engine
|
||||
// 复制引擎模块目录从 dist/engine 到 src-tauri/engine
|
||||
const engineSrcDir = path.join(editorPath, 'dist', 'engine');
|
||||
const engineDstDir = path.join(editorPath, 'src-tauri', 'engine');
|
||||
|
||||
/**
|
||||
* Recursively copy directory
|
||||
* 递归复制目录
|
||||
*/
|
||||
function copyDirRecursive(src, dst) {
|
||||
if (!fs.existsSync(src)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!fs.existsSync(dst)) {
|
||||
fs.mkdirSync(dst, { recursive: true });
|
||||
}
|
||||
|
||||
const entries = fs.readdirSync(src, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
const srcPath = path.join(src, entry.name);
|
||||
const dstPath = path.join(dst, entry.name);
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
copyDirRecursive(srcPath, dstPath);
|
||||
} else {
|
||||
fs.copyFileSync(srcPath, dstPath);
|
||||
} else if (typeof config.bundle.resources === 'object') {
|
||||
// Object format - add runtime files if not present
|
||||
if (!config.bundle.resources['runtime/**/*']) {
|
||||
config.bundle.resources['runtime/**/*'] = '.';
|
||||
fs.writeFileSync(tauriConfigPath, JSON.stringify(config, null, 2));
|
||||
console.log('✓ Updated tauri.conf.json with runtime resources');
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (fs.existsSync(engineSrcDir)) {
|
||||
// Remove old engine directory if exists
|
||||
if (fs.existsSync(engineDstDir)) {
|
||||
fs.rmSync(engineDstDir, { recursive: true });
|
||||
}
|
||||
|
||||
if (copyDirRecursive(engineSrcDir, engineDstDir)) {
|
||||
// Count files
|
||||
let fileCount = 0;
|
||||
function countFiles(dir) {
|
||||
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
if (entry.isDirectory()) {
|
||||
countFiles(path.join(dir, entry.name));
|
||||
} else {
|
||||
fileCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
countFiles(engineDstDir);
|
||||
console.log(`✓ Copied engine modules directory (${fileCount} files)`);
|
||||
}
|
||||
} else {
|
||||
console.warn(`Engine modules directory not found: ${engineSrcDir}`);
|
||||
console.log(' Build editor-app first: pnpm --filter @esengine/editor-app build');
|
||||
}
|
||||
|
||||
// Copy esbuild binary for user code compilation
|
||||
// 复制 esbuild 二进制文件用于用户代码编译
|
||||
const binDir = path.join(editorPath, 'src-tauri', 'bin');
|
||||
if (!fs.existsSync(binDir)) {
|
||||
fs.mkdirSync(binDir, { recursive: true });
|
||||
console.log(`Created bin directory: ${binDir}`);
|
||||
}
|
||||
|
||||
// Platform-specific esbuild binary paths
|
||||
// 平台特定的 esbuild 二进制路径
|
||||
const esbuildSources = {
|
||||
win32: path.join(rootPath, 'node_modules/@esbuild/win32-x64/esbuild.exe'),
|
||||
darwin: path.join(rootPath, 'node_modules/@esbuild/darwin-x64/bin/esbuild'),
|
||||
linux: path.join(rootPath, 'node_modules/@esbuild/linux-x64/bin/esbuild'),
|
||||
};
|
||||
|
||||
const platform = process.platform;
|
||||
const esbuildSrc = esbuildSources[platform];
|
||||
const esbuildDst = path.join(binDir, platform === 'win32' ? 'esbuild.exe' : 'esbuild');
|
||||
|
||||
let esbuildBundled = false;
|
||||
if (esbuildSrc && fs.existsSync(esbuildSrc)) {
|
||||
try {
|
||||
fs.copyFileSync(esbuildSrc, esbuildDst);
|
||||
// Ensure executable permission on Unix
|
||||
if (platform !== 'win32') {
|
||||
fs.chmodSync(esbuildDst, 0o755);
|
||||
}
|
||||
const stats = fs.statSync(esbuildDst);
|
||||
console.log(`✓ Bundled esbuild binary (${(stats.size / 1024 / 1024).toFixed(2)} MB)`);
|
||||
esbuildBundled = true;
|
||||
} catch (error) {
|
||||
console.warn(`Failed to bundle esbuild: ${error.message}`);
|
||||
console.log(' User code compilation will require global esbuild installation');
|
||||
}
|
||||
} else {
|
||||
console.warn(`esbuild binary not found for platform ${platform}: ${esbuildSrc}`);
|
||||
console.log(' User code compilation will require global esbuild installation');
|
||||
}
|
||||
|
||||
// Create a placeholder file if esbuild was not bundled
|
||||
// Tauri requires resources patterns to match at least one file
|
||||
// 如果 esbuild 没有打包,创建占位文件
|
||||
// Tauri 要求资源模式至少匹配一个文件
|
||||
if (!esbuildBundled) {
|
||||
const placeholderPath = path.join(binDir, '.gitkeep');
|
||||
fs.writeFileSync(placeholderPath, '# Placeholder for Tauri resources\n# esbuild binary will be bundled during release build\n');
|
||||
console.log('✓ Created placeholder in bin directory');
|
||||
}
|
||||
|
||||
if (!success) {
|
||||
|
||||
@@ -24,17 +24,12 @@ pub struct CompileOptions {
|
||||
pub output_path: String,
|
||||
/// Output format (esm or iife) | 输出格式
|
||||
pub format: String,
|
||||
/// Global name for IIFE format | IIFE 格式的全局名称
|
||||
pub global_name: Option<String>,
|
||||
/// Whether to generate source map | 是否生成 source map
|
||||
pub source_map: bool,
|
||||
/// Whether to minify | 是否压缩
|
||||
pub minify: bool,
|
||||
/// External dependencies | 外部依赖
|
||||
pub external: Vec<String>,
|
||||
/// Module aliases (e.g., "@esengine/ecs-framework" -> "/path/to/shim.js")
|
||||
/// 模块别名
|
||||
pub alias: Option<std::collections::HashMap<String, String>>,
|
||||
/// Project root for resolving imports | 项目根目录用于解析导入
|
||||
pub project_root: String,
|
||||
}
|
||||
@@ -67,34 +62,6 @@ pub struct CompileResult {
|
||||
pub output_path: Option<String>,
|
||||
}
|
||||
|
||||
/// Environment check result.
|
||||
/// 环境检测结果。
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct EnvironmentCheckResult {
|
||||
/// Whether all required tools are available | 所有必需工具是否可用
|
||||
pub ready: bool,
|
||||
/// esbuild availability status | esbuild 可用性状态
|
||||
pub esbuild: ToolStatus,
|
||||
}
|
||||
|
||||
/// Tool availability status.
|
||||
/// 工具可用性状态。
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ToolStatus {
|
||||
/// Whether the tool is available | 工具是否可用
|
||||
pub available: bool,
|
||||
/// Tool version (if available) | 工具版本(如果可用)
|
||||
pub version: Option<String>,
|
||||
/// Tool path (if available) | 工具路径(如果可用)
|
||||
pub path: Option<String>,
|
||||
/// Source of the tool: "bundled", "local", "global" | 工具来源
|
||||
pub source: Option<String>,
|
||||
/// Error message (if not available) | 错误信息(如果不可用)
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
/// File change event sent to frontend.
|
||||
/// 发送到前端的文件变更事件。
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
@@ -106,82 +73,6 @@ pub struct FileChangeEvent {
|
||||
pub paths: Vec<String>,
|
||||
}
|
||||
|
||||
/// Check development environment.
|
||||
/// 检测开发环境。
|
||||
///
|
||||
/// Checks if all required tools (esbuild, etc.) are available.
|
||||
/// 检查所有必需的工具是否可用。
|
||||
#[command]
|
||||
pub async fn check_environment() -> Result<EnvironmentCheckResult, String> {
|
||||
let esbuild_status = check_esbuild_status();
|
||||
|
||||
Ok(EnvironmentCheckResult {
|
||||
ready: esbuild_status.available,
|
||||
esbuild: esbuild_status,
|
||||
})
|
||||
}
|
||||
|
||||
/// Check esbuild availability and get its status.
|
||||
/// 检查 esbuild 可用性并获取其状态。
|
||||
fn check_esbuild_status() -> ToolStatus {
|
||||
// Try bundled esbuild first | 首先尝试打包的 esbuild
|
||||
if let Some(bundled_path) = find_bundled_esbuild() {
|
||||
match get_esbuild_version(&bundled_path) {
|
||||
Ok(version) => {
|
||||
return ToolStatus {
|
||||
available: true,
|
||||
version: Some(version),
|
||||
path: Some(bundled_path),
|
||||
source: Some("bundled".to_string()),
|
||||
error: None,
|
||||
};
|
||||
}
|
||||
Err(e) => {
|
||||
println!("[Environment] Bundled esbuild found but failed to get version: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try global esbuild | 尝试全局 esbuild
|
||||
let global_esbuild = if cfg!(windows) { "esbuild.cmd" } else { "esbuild" };
|
||||
match get_esbuild_version(global_esbuild) {
|
||||
Ok(version) => {
|
||||
ToolStatus {
|
||||
available: true,
|
||||
version: Some(version),
|
||||
path: Some(global_esbuild.to_string()),
|
||||
source: Some("global".to_string()),
|
||||
error: None,
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
ToolStatus {
|
||||
available: false,
|
||||
version: None,
|
||||
path: None,
|
||||
source: None,
|
||||
error: Some("esbuild not found | 未找到 esbuild".to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Get esbuild version.
|
||||
/// 获取 esbuild 版本。
|
||||
fn get_esbuild_version(esbuild_path: &str) -> Result<String, String> {
|
||||
let output = Command::new(esbuild_path)
|
||||
.arg("--version")
|
||||
.output()
|
||||
.map_err(|e| format!("Failed to run esbuild: {}", e))?;
|
||||
|
||||
if output.status.success() {
|
||||
let version = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||||
Ok(version)
|
||||
} else {
|
||||
Err("esbuild --version failed".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// Compile TypeScript using esbuild.
|
||||
/// 使用 esbuild 编译 TypeScript。
|
||||
///
|
||||
@@ -215,27 +106,11 @@ pub async fn compile_typescript(options: CompileOptions) -> Result<CompileResult
|
||||
args.push("--minify".to_string());
|
||||
}
|
||||
|
||||
// Add global name for IIFE format | 添加 IIFE 格式的全局名称
|
||||
if let Some(ref global_name) = options.global_name {
|
||||
args.push(format!("--global-name={}", global_name));
|
||||
}
|
||||
|
||||
// Add external dependencies | 添加外部依赖
|
||||
for external in &options.external {
|
||||
args.push(format!("--external:{}", external));
|
||||
}
|
||||
|
||||
// Add module aliases | 添加模块别名
|
||||
if let Some(ref aliases) = options.alias {
|
||||
for (from, to) in aliases {
|
||||
args.push(format!("--alias:{}={}", from, to));
|
||||
}
|
||||
}
|
||||
|
||||
// Build full command string for error reporting | 构建完整命令字符串用于错误报告
|
||||
let cmd_str = format!("{} {}", esbuild_path, args.join(" "));
|
||||
println!("[Compiler] Running: {}", cmd_str);
|
||||
|
||||
// Run esbuild | 运行 esbuild
|
||||
let output = Command::new(&esbuild_path)
|
||||
.args(&args)
|
||||
@@ -244,7 +119,6 @@ pub async fn compile_typescript(options: CompileOptions) -> Result<CompileResult
|
||||
.map_err(|e| format!("Failed to run esbuild | 运行 esbuild 失败: {}", e))?;
|
||||
|
||||
if output.status.success() {
|
||||
println!("[Compiler] Compilation successful: {}", options.output_path);
|
||||
Ok(CompileResult {
|
||||
success: true,
|
||||
errors: vec![],
|
||||
@@ -252,37 +126,7 @@ pub async fn compile_typescript(options: CompileOptions) -> Result<CompileResult
|
||||
})
|
||||
} else {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
|
||||
println!("[Compiler] Compilation failed");
|
||||
println!("[Compiler] stdout: {}", stdout);
|
||||
println!("[Compiler] stderr: {}", stderr);
|
||||
|
||||
// Try to parse errors from both stdout and stderr | 尝试从 stdout 和 stderr 解析错误
|
||||
let mut errors = parse_esbuild_errors(&stderr);
|
||||
if errors.is_empty() {
|
||||
errors = parse_esbuild_errors(&stdout);
|
||||
}
|
||||
|
||||
// If still no parsed errors, include the raw output and command | 如果仍然没有解析到错误,包含原始输出和命令
|
||||
if errors.is_empty() {
|
||||
let combined_output = if !stderr.is_empty() && !stdout.is_empty() {
|
||||
format!("stdout: {}\nstderr: {}", stdout.trim(), stderr.trim())
|
||||
} else if !stderr.is_empty() {
|
||||
stderr.trim().to_string()
|
||||
} else if !stdout.is_empty() {
|
||||
stdout.trim().to_string()
|
||||
} else {
|
||||
format!("Command failed: {}", cmd_str)
|
||||
};
|
||||
|
||||
errors.push(CompileError {
|
||||
message: combined_output,
|
||||
file: None,
|
||||
line: None,
|
||||
column: None,
|
||||
});
|
||||
}
|
||||
let errors = parse_esbuild_errors(&stderr);
|
||||
|
||||
Ok(CompileResult {
|
||||
success: false,
|
||||
@@ -358,11 +202,6 @@ pub async fn watch_scripts(
|
||||
|
||||
println!("[UserCode] Started watching: {}", watch_path_clone.display());
|
||||
|
||||
// Debounce state | 防抖状态
|
||||
let mut pending_paths: std::collections::HashSet<String> = std::collections::HashSet::new();
|
||||
let mut last_event_time = std::time::Instant::now();
|
||||
let debounce_duration = Duration::from_millis(300);
|
||||
|
||||
// Event loop | 事件循环
|
||||
loop {
|
||||
// Check for shutdown | 检查关闭信号
|
||||
@@ -386,28 +225,19 @@ pub async fn watch_scripts(
|
||||
.collect();
|
||||
|
||||
if !ts_paths.is_empty() {
|
||||
// Only handle create/modify/remove events | 只处理创建/修改/删除事件
|
||||
match event.kind {
|
||||
EventKind::Create(_) | EventKind::Modify(_) | EventKind::Remove(_) => {
|
||||
// Add to pending paths and update last event time | 添加到待处理路径并更新最后事件时间
|
||||
for path in ts_paths {
|
||||
pending_paths.insert(path);
|
||||
}
|
||||
last_event_time = std::time::Instant::now();
|
||||
}
|
||||
let change_type = match event.kind {
|
||||
EventKind::Create(_) => "create",
|
||||
EventKind::Modify(_) => "modify",
|
||||
EventKind::Remove(_) => "remove",
|
||||
_ => continue,
|
||||
};
|
||||
}
|
||||
}
|
||||
Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {
|
||||
// Check if we should emit pending events (debounce) | 检查是否应该发送待处理事件(防抖)
|
||||
if !pending_paths.is_empty() && last_event_time.elapsed() >= debounce_duration {
|
||||
|
||||
let file_event = FileChangeEvent {
|
||||
change_type: "modify".to_string(),
|
||||
paths: pending_paths.drain().collect(),
|
||||
change_type: change_type.to_string(),
|
||||
paths: ts_paths,
|
||||
};
|
||||
|
||||
println!("[UserCode] File change detected (debounced): {:?}", file_event);
|
||||
println!("[UserCode] File change detected: {:?}", file_event);
|
||||
|
||||
// Emit event to frontend | 向前端发送事件
|
||||
if let Err(e) = app_clone.emit("user-code:file-changed", file_event) {
|
||||
@@ -415,6 +245,9 @@ pub async fn watch_scripts(
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {
|
||||
// No events, continue | 无事件,继续
|
||||
}
|
||||
Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => {
|
||||
println!("[UserCode] Watcher channel disconnected");
|
||||
break;
|
||||
@@ -467,21 +300,10 @@ pub async fn stop_watch_scripts(
|
||||
|
||||
/// Find esbuild executable path.
|
||||
/// 查找 esbuild 可执行文件路径。
|
||||
///
|
||||
/// Search order | 搜索顺序:
|
||||
/// 1. Bundled esbuild in app resources | 应用资源中打包的 esbuild
|
||||
/// 2. Local node_modules | 本地 node_modules
|
||||
/// 3. Global esbuild | 全局 esbuild
|
||||
fn find_esbuild(project_root: &str) -> Result<String, String> {
|
||||
let project_path = Path::new(project_root);
|
||||
|
||||
// Try bundled esbuild first (in app resources) | 首先尝试打包的 esbuild(在应用资源中)
|
||||
if let Some(bundled) = find_bundled_esbuild() {
|
||||
println!("[Compiler] Using bundled esbuild: {}", bundled);
|
||||
return Ok(bundled);
|
||||
}
|
||||
|
||||
// Try local node_modules | 尝试本地 node_modules
|
||||
// Try local node_modules first | 首先尝试本地 node_modules
|
||||
let local_esbuild = if cfg!(windows) {
|
||||
project_path.join("node_modules/.bin/esbuild.cmd")
|
||||
} else {
|
||||
@@ -489,7 +311,6 @@ fn find_esbuild(project_root: &str) -> Result<String, String> {
|
||||
};
|
||||
|
||||
if local_esbuild.exists() {
|
||||
println!("[Compiler] Using local esbuild: {}", local_esbuild.display());
|
||||
return Ok(local_esbuild.to_string_lossy().to_string());
|
||||
}
|
||||
|
||||
@@ -502,51 +323,11 @@ fn find_esbuild(project_root: &str) -> Result<String, String> {
|
||||
.output();
|
||||
|
||||
match check {
|
||||
Ok(output) if output.status.success() => {
|
||||
println!("[Compiler] Using global esbuild");
|
||||
Ok(global_esbuild.to_string())
|
||||
},
|
||||
Ok(output) if output.status.success() => Ok(global_esbuild.to_string()),
|
||||
_ => Err("esbuild not found. Please install esbuild: npm install -g esbuild | 未找到 esbuild,请安装: npm install -g esbuild".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// Find bundled esbuild in app resources.
|
||||
/// 在应用资源中查找打包的 esbuild。
|
||||
fn find_bundled_esbuild() -> Option<String> {
|
||||
// Get the executable path | 获取可执行文件路径
|
||||
let exe_path = std::env::current_exe().ok()?;
|
||||
let exe_dir = exe_path.parent()?;
|
||||
|
||||
// In development, resources are in src-tauri directory | 开发模式下,资源在 src-tauri 目录
|
||||
// In production, resources are next to the executable | 生产模式下,资源在可执行文件旁边
|
||||
let esbuild_name = if cfg!(windows) { "esbuild.exe" } else { "esbuild" };
|
||||
|
||||
// Try production path (resources next to exe) | 尝试生产路径(资源在 exe 旁边)
|
||||
let prod_path = exe_dir.join("bin").join(esbuild_name);
|
||||
if prod_path.exists() {
|
||||
return Some(prod_path.to_string_lossy().to_string());
|
||||
}
|
||||
|
||||
// Try development path (in src-tauri/bin) | 尝试开发路径(在 src-tauri/bin 中)
|
||||
// This handles running via `cargo tauri dev`
|
||||
let dev_path = exe_dir
|
||||
.ancestors()
|
||||
.find_map(|p| {
|
||||
let candidate = p.join("src-tauri").join("bin").join(esbuild_name);
|
||||
if candidate.exists() {
|
||||
Some(candidate)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
if let Some(path) = dev_path {
|
||||
return Some(path.to_string_lossy().to_string());
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Parse esbuild error output.
|
||||
/// 解析 esbuild 错误输出。
|
||||
fn parse_esbuild_errors(stderr: &str) -> Vec<CompileError> {
|
||||
|
||||
@@ -82,35 +82,10 @@ pub fn delete_file(path: String) -> Result<(), String> {
|
||||
}
|
||||
|
||||
/// Delete directory (recursive)
|
||||
/// 递归删除目录
|
||||
#[tauri::command]
|
||||
pub fn delete_folder(path: String) -> Result<(), String> {
|
||||
println!("[delete_folder] Attempting to delete: {}", path);
|
||||
|
||||
// Check if path exists
|
||||
// 检查路径是否存在
|
||||
let dir_path = std::path::Path::new(&path);
|
||||
if !dir_path.exists() {
|
||||
println!("[delete_folder] Path does not exist: {}", path);
|
||||
return Err(format!("Directory does not exist: {}", path));
|
||||
}
|
||||
|
||||
if !dir_path.is_dir() {
|
||||
println!("[delete_folder] Path is not a directory: {}", path);
|
||||
return Err(format!("Path is not a directory: {}", path));
|
||||
}
|
||||
|
||||
match fs::remove_dir_all(&path) {
|
||||
Ok(_) => {
|
||||
println!("[delete_folder] Successfully deleted: {}", path);
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
let error_msg = format!("Failed to delete folder {}: {}", path, e);
|
||||
eprintln!("[delete_folder] Error: {}", error_msg);
|
||||
Err(error_msg)
|
||||
}
|
||||
}
|
||||
fs::remove_dir_all(&path)
|
||||
.map_err(|e| format!("Failed to delete folder {}: {}", path, e))
|
||||
}
|
||||
|
||||
/// Rename or move file/folder
|
||||
|
||||
@@ -75,35 +75,10 @@ pub fn open_file_with_default_app(file_path: String) -> Result<(), String> {
|
||||
/// Show file in system file explorer
|
||||
#[tauri::command]
|
||||
pub fn show_in_folder(file_path: String) -> Result<(), String> {
|
||||
println!("[show_in_folder] Received path: {}", file_path);
|
||||
|
||||
#[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")
|
||||
.arg(&select_arg)
|
||||
.args(["/select,", &file_path])
|
||||
.spawn()
|
||||
.map_err(|e| format!("Failed to show in folder: {}", e))?;
|
||||
}
|
||||
@@ -142,180 +117,6 @@ pub fn get_temp_dir() -> Result<String, String> {
|
||||
.ok_or_else(|| "Failed to get temp directory".to_string())
|
||||
}
|
||||
|
||||
/// 使用 where 命令查找可执行文件路径
|
||||
/// Use 'where' command to find executable path
|
||||
#[cfg(target_os = "windows")]
|
||||
fn find_command_path(cmd: &str) -> Option<String> {
|
||||
use std::process::Command as StdCommand;
|
||||
use std::path::Path;
|
||||
|
||||
// 使用 where 命令查找
|
||||
let output = StdCommand::new("where")
|
||||
.arg(cmd)
|
||||
.output()
|
||||
.ok()?;
|
||||
|
||||
if output.status.success() {
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
// 取第一行结果(可能有多个匹配)
|
||||
if let Some(first_line) = stdout.lines().next() {
|
||||
let path = first_line.trim();
|
||||
if !path.is_empty() {
|
||||
let path_obj = Path::new(path);
|
||||
|
||||
// 检查是否是 bin 目录下的脚本(VSCode/Cursor 特征)
|
||||
// Check if it's a script in bin directory (VSCode/Cursor pattern)
|
||||
let is_bin_script = path_obj.parent()
|
||||
.map(|p| p.ends_with("bin"))
|
||||
.unwrap_or(false);
|
||||
|
||||
// 如果找到的是 .cmd 或 .bat,或者是 bin 目录下的脚本(where 可能不返回扩展名)
|
||||
// If found .cmd or .bat, or a script in bin directory (where may not return extension)
|
||||
let has_script_ext = path.ends_with(".cmd") || path.ends_with(".bat");
|
||||
|
||||
if has_script_ext || is_bin_script {
|
||||
// 尝试找 Code.exe (VSCode) 或 Cursor.exe 等
|
||||
// Try to find Code.exe (VSCode) or Cursor.exe etc.
|
||||
if let Some(bin_dir) = path_obj.parent() {
|
||||
if let Some(parent_dir) = bin_dir.parent() {
|
||||
// VSCode: bin/code.cmd -> Code.exe
|
||||
let exe_path = parent_dir.join("Code.exe");
|
||||
if exe_path.exists() {
|
||||
let exe_str = exe_path.to_string_lossy().to_string();
|
||||
println!("[find_command_path] Found {} exe at: {}", cmd, exe_str);
|
||||
return Some(exe_str);
|
||||
}
|
||||
|
||||
// Cursor: bin/cursor.cmd -> Cursor.exe
|
||||
let cursor_exe = parent_dir.join("Cursor.exe");
|
||||
if cursor_exe.exists() {
|
||||
let exe_str = cursor_exe.to_string_lossy().to_string();
|
||||
println!("[find_command_path] Found {} exe at: {}", cmd, exe_str);
|
||||
return Some(exe_str);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!("[find_command_path] Found {} at: {}", cmd, path);
|
||||
return Some(path.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
fn find_command_path(cmd: &str) -> Option<String> {
|
||||
use std::process::Command as StdCommand;
|
||||
|
||||
let output = StdCommand::new("which")
|
||||
.arg(cmd)
|
||||
.output()
|
||||
.ok()?;
|
||||
|
||||
if output.status.success() {
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
let path = stdout.trim();
|
||||
if !path.is_empty() {
|
||||
return Some(path.to_string());
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// 解析编辑器命令,返回实际可执行路径
|
||||
/// Resolve editor command to actual executable path
|
||||
fn resolve_editor_command(editor_command: &str) -> String {
|
||||
use std::path::Path;
|
||||
|
||||
// 如果命令已经是完整路径且存在,直接返回
|
||||
// If command is already a full path and exists, return it
|
||||
if Path::new(editor_command).exists() {
|
||||
return editor_command.to_string();
|
||||
}
|
||||
|
||||
// 使用系统命令查找可执行文件路径
|
||||
// Use system command to find executable path
|
||||
if let Some(path) = find_command_path(editor_command) {
|
||||
return path;
|
||||
}
|
||||
|
||||
// 回退到原始命令 | Fall back to original command
|
||||
editor_command.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));
|
||||
}
|
||||
|
||||
// 解析编辑器命令到实际路径
|
||||
// Resolve editor command to actual path
|
||||
let resolved_command = resolve_editor_command(&editor_command);
|
||||
|
||||
println!(
|
||||
"[open_with_editor] editor: {} -> {}, project: {}, file: {:?}",
|
||||
editor_command, resolved_command, normalized_project, normalized_file
|
||||
);
|
||||
|
||||
let mut cmd = Command::new(&resolved_command);
|
||||
|
||||
// VSCode/Cursor CLI 正确用法:
|
||||
// 1. 使用 --folder-uri 或直接传文件夹路径会打开新窗口
|
||||
// 2. 使用 --add 可以将文件夹添加到当前工作区
|
||||
// 3. 使用 --goto file:line:column 可以打开文件并定位
|
||||
//
|
||||
// VSCode/Cursor CLI correct usage:
|
||||
// 1. Use --folder-uri or pass folder path directly to open new window
|
||||
// 2. Use --add to add folder to current workspace
|
||||
// 3. Use --goto file:line:column to open file and navigate
|
||||
//
|
||||
// 正确命令格式: code <folder> <file>
|
||||
// 这会打开文件夹并同时打开文件
|
||||
// Correct command format: code <folder> <file>
|
||||
// This opens the folder and also opens the file
|
||||
|
||||
// Add project folder first
|
||||
// 先添加项目文件夹
|
||||
cmd.arg(&normalized_project);
|
||||
|
||||
// If a specific file is provided, add it directly (not with -g)
|
||||
// VSCode will open the folder AND the file
|
||||
// 如果提供了文件,直接添加(不使用 -g)
|
||||
// VSCode 会同时打开文件夹和文件
|
||||
if let Some(ref file) = normalized_file {
|
||||
let file_path_obj = Path::new(file);
|
||||
if file_path_obj.exists() {
|
||||
cmd.arg(file);
|
||||
}
|
||||
}
|
||||
|
||||
cmd.spawn()
|
||||
.map_err(|e| format!("Failed to open with editor '{}': {}", resolved_command, e))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get application resource directory
|
||||
#[tauri::command]
|
||||
pub fn get_app_resource_dir(app: AppHandle) -> Result<String, String> {
|
||||
@@ -337,191 +138,6 @@ pub fn get_current_dir() -> Result<String, String> {
|
||||
.map_err(|e| format!("Failed to get current directory: {}", e))
|
||||
}
|
||||
|
||||
/// Update project tsconfig.json with engine type paths
|
||||
/// 更新项目的 tsconfig.json,添加引擎类型路径
|
||||
///
|
||||
/// Scans dist/engine/ directory and adds paths for all modules with .d.ts files.
|
||||
/// 扫描 dist/engine/ 目录,为所有有 .d.ts 文件的模块添加路径。
|
||||
#[tauri::command]
|
||||
pub fn update_project_tsconfig(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));
|
||||
}
|
||||
|
||||
// Get engine modules path (dist/engine/)
|
||||
// 获取引擎模块路径
|
||||
let engine_path = get_engine_modules_base_path_internal(&app)?;
|
||||
|
||||
// Read existing tsconfig.json
|
||||
// 读取现有的 tsconfig.json
|
||||
let tsconfig_path = project.join("tsconfig.json");
|
||||
let tsconfig_editor_path = project.join("tsconfig.editor.json");
|
||||
|
||||
// Update runtime tsconfig
|
||||
// 更新运行时 tsconfig
|
||||
if tsconfig_path.exists() {
|
||||
update_tsconfig_file(&tsconfig_path, &engine_path, false)?;
|
||||
println!("[update_project_tsconfig] Updated {}", tsconfig_path.display());
|
||||
}
|
||||
|
||||
// Update editor tsconfig
|
||||
// 更新编辑器 tsconfig
|
||||
if tsconfig_editor_path.exists() {
|
||||
update_tsconfig_file(&tsconfig_editor_path, &engine_path, true)?;
|
||||
println!("[update_project_tsconfig] Updated {}", tsconfig_editor_path.display());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Internal function to get engine modules base path
|
||||
/// 内部函数:获取引擎模块基础路径
|
||||
fn get_engine_modules_base_path_internal(app: &AppHandle) -> Result<String, String> {
|
||||
let resource_dir = app.path()
|
||||
.resource_dir()
|
||||
.map_err(|e| format!("Failed to get resource directory: {}", e))?;
|
||||
|
||||
// Production mode: resource_dir/engine/
|
||||
// 生产模式
|
||||
let prod_path = resource_dir.join("engine");
|
||||
if prod_path.exists() {
|
||||
return prod_path.to_str()
|
||||
.map(|s| s.to_string())
|
||||
.ok_or_else(|| "Invalid path encoding".to_string());
|
||||
}
|
||||
|
||||
// Development mode: workspace/packages/editor-app/dist/engine/
|
||||
// 开发模式
|
||||
if let Some(ws_root) = find_workspace_root() {
|
||||
let dev_path = ws_root.join("packages").join("editor-app").join("dist").join("engine");
|
||||
if dev_path.exists() {
|
||||
return dev_path.to_str()
|
||||
.map(|s| s.to_string())
|
||||
.ok_or_else(|| "Invalid path encoding".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
Err("Engine modules directory not found".to_string())
|
||||
}
|
||||
|
||||
/// Find workspace root directory
|
||||
/// 查找工作区根目录
|
||||
fn find_workspace_root() -> Option<std::path::PathBuf> {
|
||||
std::env::current_dir()
|
||||
.ok()
|
||||
.and_then(|cwd| {
|
||||
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,
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Update a tsconfig file with engine paths
|
||||
/// 使用引擎路径更新 tsconfig 文件
|
||||
///
|
||||
/// Scans all subdirectories in engine_path for index.d.ts files.
|
||||
/// 扫描 engine_path 下所有子目录的 index.d.ts 文件。
|
||||
fn update_tsconfig_file(
|
||||
tsconfig_path: &std::path::Path,
|
||||
engine_path: &str,
|
||||
include_editor: bool,
|
||||
) -> Result<(), String> {
|
||||
use std::fs;
|
||||
|
||||
let content = fs::read_to_string(tsconfig_path)
|
||||
.map_err(|e| format!("Failed to read tsconfig: {}", e))?;
|
||||
|
||||
let mut config: serde_json::Value = serde_json::from_str(&content)
|
||||
.map_err(|e| format!("Failed to parse tsconfig: {}", e))?;
|
||||
|
||||
// Normalize path for cross-platform compatibility
|
||||
// 规范化路径以实现跨平台兼容
|
||||
let engine_path_normalized = engine_path.replace('\\', "/");
|
||||
|
||||
// Build paths mapping by scanning engine modules directory
|
||||
// 通过扫描引擎模块目录构建路径映射
|
||||
let mut paths = serde_json::Map::new();
|
||||
let mut module_count = 0;
|
||||
|
||||
let engine_dir = std::path::Path::new(engine_path);
|
||||
if let Ok(entries) = fs::read_dir(engine_dir) {
|
||||
for entry in entries.flatten() {
|
||||
let module_path = entry.path();
|
||||
if !module_path.is_dir() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let module_id = module_path.file_name()
|
||||
.and_then(|n| n.to_str())
|
||||
.unwrap_or("");
|
||||
|
||||
// Skip editor modules for runtime tsconfig
|
||||
// 运行时 tsconfig 跳过编辑器模块
|
||||
if !include_editor && module_id.ends_with("-editor") {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check for index.d.ts
|
||||
// 检查是否存在 index.d.ts
|
||||
let dts_path = module_path.join("index.d.ts");
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
println!("[update_tsconfig_file] Found {} modules with type definitions", module_count);
|
||||
|
||||
// Update compilerOptions.paths
|
||||
// 更新 compilerOptions.paths
|
||||
if let Some(compiler_options) = config.get_mut("compilerOptions") {
|
||||
if let Some(obj) = compiler_options.as_object_mut() {
|
||||
obj.insert("paths".to_string(), serde_json::Value::Object(paths));
|
||||
// Remove typeRoots since we're using paths
|
||||
// 移除 typeRoots,因为我们使用 paths
|
||||
obj.remove("typeRoots");
|
||||
}
|
||||
}
|
||||
|
||||
// Write back
|
||||
// 写回文件
|
||||
let output = serde_json::to_string_pretty(&config)
|
||||
.map_err(|e| format!("Failed to serialize tsconfig: {}", e))?;
|
||||
|
||||
fs::write(tsconfig_path, output)
|
||||
.map_err(|e| format!("Failed to write tsconfig: {}", e))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Start a local HTTP server for runtime preview
|
||||
#[tauri::command]
|
||||
pub fn start_local_server(root_path: String, port: u16) -> Result<String, String> {
|
||||
|
||||
@@ -81,8 +81,6 @@ fn main() {
|
||||
commands::open_file_with_default_app,
|
||||
commands::show_in_folder,
|
||||
commands::get_temp_dir,
|
||||
commands::open_with_editor,
|
||||
commands::update_project_tsconfig,
|
||||
commands::get_app_resource_dir,
|
||||
commands::get_current_dir,
|
||||
commands::start_local_server,
|
||||
@@ -93,7 +91,6 @@ fn main() {
|
||||
commands::compile_typescript,
|
||||
commands::watch_scripts,
|
||||
commands::stop_watch_scripts,
|
||||
commands::check_environment,
|
||||
// Build commands | 构建命令
|
||||
commands::prepare_build_directory,
|
||||
commands::copy_directory,
|
||||
@@ -157,13 +154,7 @@ fn handle_project_protocol(
|
||||
tauri::http::Response::builder()
|
||||
.status(200)
|
||||
.header("Content-Type", mime_type)
|
||||
// CORS headers for dynamic ES module imports | 动态 ES 模块导入所需的 CORS 头
|
||||
.header("Access-Control-Allow-Origin", "*")
|
||||
.header("Access-Control-Allow-Methods", "GET, OPTIONS")
|
||||
.header("Access-Control-Allow-Headers", "Content-Type")
|
||||
.header("Access-Control-Expose-Headers", "Content-Length")
|
||||
// Allow cross-origin script loading | 允许跨域脚本加载
|
||||
.header("Cross-Origin-Resource-Policy", "cross-origin")
|
||||
.body(content)
|
||||
.unwrap()
|
||||
}
|
||||
@@ -171,7 +162,6 @@ fn handle_project_protocol(
|
||||
eprintln!("Failed to read file {}: {}", file_path, e);
|
||||
tauri::http::Response::builder()
|
||||
.status(404)
|
||||
.header("Access-Control-Allow-Origin", "*")
|
||||
.body(Vec::new())
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
@@ -11,11 +11,9 @@
|
||||
"active": true,
|
||||
"targets": "all",
|
||||
"createUpdaterArtifacts": true,
|
||||
"resources": [
|
||||
"runtime/**/*",
|
||||
"engine/**/*",
|
||||
"bin/*"
|
||||
],
|
||||
"resources": {
|
||||
"runtime/**/*": "."
|
||||
},
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
|
||||
@@ -381,14 +381,6 @@ function App() {
|
||||
// 设置 Tauri project:// 协议的基础路径(用于加载插件等项目文件)
|
||||
await TauriAPI.setProjectBasePath(projectPath);
|
||||
|
||||
// 更新项目 tsconfig,直接引用引擎类型定义
|
||||
// Update project tsconfig to reference engine type definitions directly
|
||||
try {
|
||||
await TauriAPI.updateProjectTsconfig(projectPath);
|
||||
} catch (e) {
|
||||
console.warn('[App] Failed to update project tsconfig:', e);
|
||||
}
|
||||
|
||||
const settings = SettingsService.getInstance();
|
||||
settings.addRecentProject(projectPath);
|
||||
|
||||
@@ -473,9 +465,7 @@ function App() {
|
||||
};
|
||||
|
||||
const handleCreateProjectFromWizard = async (projectName: string, projectPath: string, _templateId: string) => {
|
||||
// 使用与 projectPath 相同的路径分隔符 | Use same separator as projectPath
|
||||
const sep = projectPath.includes('/') ? '/' : '\\';
|
||||
const fullProjectPath = `${projectPath}${sep}${projectName}`;
|
||||
const fullProjectPath = `${projectPath}\\${projectName}`;
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
@@ -641,13 +631,6 @@ function App() {
|
||||
await pluginLoader.unloadProjectPlugins(pluginManager);
|
||||
}
|
||||
|
||||
// 清理场景(会清理所有实体和系统)
|
||||
// Clear scene (clears all entities and systems)
|
||||
const scene = Core.scene;
|
||||
if (scene) {
|
||||
scene.end();
|
||||
}
|
||||
|
||||
// 清理模块系统
|
||||
const engineService = EngineService.getInstance();
|
||||
engineService.clearModuleSystems();
|
||||
@@ -834,31 +817,6 @@ function App() {
|
||||
onOpenProject={handleOpenProject}
|
||||
onCreateProject={handleCreateProject}
|
||||
onOpenRecentProject={handleOpenRecentProject}
|
||||
onRemoveRecentProject={(projectPath) => {
|
||||
settings.removeRecentProject(projectPath);
|
||||
// 强制重新渲染 | Force re-render
|
||||
setStatus(t('header.status.ready'));
|
||||
}}
|
||||
onDeleteProject={async (projectPath) => {
|
||||
console.log('[App] onDeleteProject called with path:', projectPath);
|
||||
try {
|
||||
console.log('[App] Calling TauriAPI.deleteFolder...');
|
||||
await TauriAPI.deleteFolder(projectPath);
|
||||
console.log('[App] deleteFolder succeeded');
|
||||
// 删除成功后从列表中移除并触发重新渲染
|
||||
// Remove from list and trigger re-render after successful deletion
|
||||
settings.removeRecentProject(projectPath);
|
||||
setStatus(t('header.status.ready'));
|
||||
} catch (error) {
|
||||
console.error('[App] 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}
|
||||
recentProjects={recentProjects}
|
||||
locale={locale}
|
||||
|
||||
@@ -168,26 +168,6 @@ export class TauriAPI {
|
||||
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
|
||||
@@ -331,20 +311,6 @@ export class TauriAPI {
|
||||
return await invoke<string>('generate_qrcode', { text });
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新项目的 tsconfig.json,添加引擎类型路径
|
||||
* Update project tsconfig.json with engine type paths
|
||||
*
|
||||
* This updates the tsconfig to point directly to engine's .d.ts files
|
||||
* instead of copying them to the project.
|
||||
* 这会更新 tsconfig 直接指向引擎的 .d.ts 文件,而不是复制到项目。
|
||||
*
|
||||
* @param projectPath 项目路径 | Project path
|
||||
*/
|
||||
static async updateProjectTsconfig(projectPath: string): Promise<void> {
|
||||
return await invoke<void>('update_project_tsconfig', { projectPath });
|
||||
}
|
||||
|
||||
/**
|
||||
* 将本地文件路径转换为 Tauri 可访问的 asset URL
|
||||
* @param filePath 本地文件路径
|
||||
@@ -357,47 +323,6 @@ export class TauriAPI {
|
||||
static convertFileSrc(filePath: string, protocol?: string): string {
|
||||
return convertFileSrc(filePath, protocol);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测开发环境
|
||||
* Check development environment
|
||||
*
|
||||
* Checks if all required tools (esbuild, etc.) are available.
|
||||
* 检查所有必需的工具是否可用。
|
||||
*
|
||||
* @returns 环境检测结果 | Environment check result
|
||||
*/
|
||||
static async checkEnvironment(): Promise<EnvironmentCheckResult> {
|
||||
return await invoke<EnvironmentCheckResult>('check_environment');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 工具可用性状态
|
||||
* Tool availability status
|
||||
*/
|
||||
export interface ToolStatus {
|
||||
/** 工具是否可用 | Whether the tool is available */
|
||||
available: boolean;
|
||||
/** 工具版本 | Tool version */
|
||||
version?: string;
|
||||
/** 工具路径 | Tool path */
|
||||
path?: string;
|
||||
/** 工具来源: "bundled", "local", "global" | Tool source */
|
||||
source?: string;
|
||||
/** 错误信息 | Error message */
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 环境检测结果
|
||||
* Environment check result
|
||||
*/
|
||||
export interface EnvironmentCheckResult {
|
||||
/** 所有必需工具是否可用 | Whether all required tools are available */
|
||||
ready: boolean;
|
||||
/** esbuild 可用性状态 | esbuild availability status */
|
||||
esbuild: ToolStatus;
|
||||
}
|
||||
|
||||
export interface DirectoryEntry {
|
||||
|
||||
@@ -14,7 +14,6 @@ import { SceneInspectorPlugin } from '../../plugins/builtin/SceneInspectorPlugin
|
||||
import { ProfilerPlugin } from '../../plugins/builtin/ProfilerPlugin';
|
||||
import { EditorAppearancePlugin } from '../../plugins/builtin/EditorAppearancePlugin';
|
||||
import { ProjectSettingsPlugin } from '../../plugins/builtin/ProjectSettingsPlugin';
|
||||
import { AssetMetaPlugin } from '../../plugins/builtin/AssetMetaPlugin';
|
||||
// Note: PluginConfigPlugin removed - module management is now unified in ProjectSettingsPlugin
|
||||
|
||||
// 统一模块插件(从编辑器包导入完整插件,包含 runtime + editor)
|
||||
@@ -39,7 +38,6 @@ export class PluginInstaller {
|
||||
{ name: 'ProfilerPlugin', plugin: ProfilerPlugin },
|
||||
{ name: 'EditorAppearancePlugin', plugin: EditorAppearancePlugin },
|
||||
{ name: 'ProjectSettingsPlugin', plugin: ProjectSettingsPlugin },
|
||||
{ name: 'AssetMetaPlugin', plugin: AssetMetaPlugin },
|
||||
];
|
||||
|
||||
for (const { name, plugin } of builtinPlugins) {
|
||||
|
||||
@@ -37,10 +37,7 @@ import {
|
||||
BuildService,
|
||||
WebBuildPipeline,
|
||||
WeChatBuildPipeline,
|
||||
moduleRegistry,
|
||||
UserCodeService,
|
||||
UserCodeTarget,
|
||||
type HotReloadEvent
|
||||
moduleRegistry
|
||||
} from '@esengine/editor-core';
|
||||
import { ViewportService } from '../../services/ViewportService';
|
||||
import { TransformComponent } from '@esengine/engine-core';
|
||||
@@ -81,7 +78,6 @@ import {
|
||||
import { TransformComponentInspector } from '../../components/inspectors/component-inspectors/TransformComponentInspector';
|
||||
import { buildFileSystem } from '../../services/BuildFileSystemService';
|
||||
import { TauriModuleFileSystem } from '../../services/TauriModuleFileSystem';
|
||||
import { PluginSDKRegistry } from '../../services/PluginSDKRegistry';
|
||||
|
||||
export interface EditorServices {
|
||||
uiRegistry: UIRegistry;
|
||||
@@ -108,7 +104,6 @@ export interface EditorServices {
|
||||
propertyRendererRegistry: PropertyRendererRegistry;
|
||||
fieldEditorRegistry: FieldEditorRegistry;
|
||||
buildService: BuildService;
|
||||
userCodeService: UserCodeService;
|
||||
}
|
||||
|
||||
export class ServiceRegistry {
|
||||
@@ -147,10 +142,6 @@ export class ServiceRegistry {
|
||||
CoreComponentRegistry.register(comp.type as any);
|
||||
}
|
||||
|
||||
// Enable hot reload for editor environment
|
||||
// 在编辑器环境中启用热更新
|
||||
CoreComponentRegistry.enableHotReload();
|
||||
|
||||
const projectService = new ProjectService(messageHub, fileAPI);
|
||||
const componentDiscovery = new ComponentDiscoveryService(messageHub);
|
||||
const propertyMetadata = new PropertyMetadataService();
|
||||
@@ -280,104 +271,6 @@ export class ServiceRegistry {
|
||||
console.warn('[ServiceRegistry] Failed to initialize ModuleRegistry:', err);
|
||||
});
|
||||
|
||||
// Initialize UserCodeService for user script compilation and loading
|
||||
// 初始化 UserCodeService 用于用户脚本编译和加载
|
||||
const userCodeService = new UserCodeService(fileSystem);
|
||||
Core.services.registerInstance(UserCodeService, userCodeService);
|
||||
|
||||
// Helper function to compile and load user scripts
|
||||
// 辅助函数:编译和加载用户脚本
|
||||
let currentProjectPath: string | null = null;
|
||||
|
||||
const compileAndLoadUserScripts = async (projectPath: string) => {
|
||||
// Ensure PluginSDKRegistry is initialized before loading user code
|
||||
// 确保在加载用户代码之前 PluginSDKRegistry 已初始化
|
||||
PluginSDKRegistry.initialize();
|
||||
|
||||
try {
|
||||
// Compile runtime scripts | 编译运行时脚本
|
||||
const compileResult = await userCodeService.compile({
|
||||
projectPath: projectPath,
|
||||
target: UserCodeTarget.Runtime
|
||||
});
|
||||
|
||||
if (compileResult.success && compileResult.outputPath) {
|
||||
// Load compiled module | 加载编译后的模块
|
||||
const module = await userCodeService.load(compileResult.outputPath, UserCodeTarget.Runtime);
|
||||
|
||||
// Register user components to editor | 注册用户组件到编辑器
|
||||
userCodeService.registerComponents(module, componentRegistry);
|
||||
|
||||
// Notify that user code has been reloaded | 通知用户代码已重新加载
|
||||
messageHub.publish('usercode:reloaded', {
|
||||
projectPath,
|
||||
exports: Object.keys(module.exports)
|
||||
});
|
||||
} else if (compileResult.errors.length > 0) {
|
||||
console.warn('[UserCodeService] Compilation errors:', compileResult.errors);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[UserCodeService] Failed to compile/load:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Subscribe to project:opened to compile and load user scripts
|
||||
// 订阅 project:opened 以编译和加载用户脚本
|
||||
messageHub.subscribe('project:opened', async (data: { path: string; type: string; name: string }) => {
|
||||
currentProjectPath = data.path;
|
||||
await compileAndLoadUserScripts(data.path);
|
||||
|
||||
// Start watching for file changes (external editor support)
|
||||
// 开始监视文件变更(支持外部编辑器)
|
||||
userCodeService.watch(data.path, async (event) => {
|
||||
console.log('[UserCodeService] Hot reload event:', event.changedFiles);
|
||||
|
||||
if (event.newModule) {
|
||||
// 1. Register new/updated components to registries
|
||||
// 1. 注册新的/更新的组件到注册表
|
||||
userCodeService.registerComponents(event.newModule, componentRegistry);
|
||||
|
||||
// 2. Hot reload: update prototype chain of existing instances
|
||||
// 2. 热更新:更新现有实例的原型链
|
||||
const updatedCount = userCodeService.hotReloadInstances(event.newModule);
|
||||
console.log(`[UserCodeService] Hot reloaded ${updatedCount} component instances`);
|
||||
|
||||
// 3. Notify that user code has been reloaded
|
||||
// 3. 通知用户代码已重新加载
|
||||
messageHub.publish('usercode:reloaded', {
|
||||
projectPath: data.path,
|
||||
exports: Object.keys(event.newModule.exports),
|
||||
updatedInstances: updatedCount
|
||||
});
|
||||
}
|
||||
}).catch(err => {
|
||||
console.warn('[UserCodeService] Failed to start file watcher:', err);
|
||||
});
|
||||
});
|
||||
|
||||
// Subscribe to project:closed to stop watching
|
||||
// 订阅 project:closed 以停止监视
|
||||
messageHub.subscribe('project:closed', async () => {
|
||||
currentProjectPath = null;
|
||||
await userCodeService.stopWatch();
|
||||
});
|
||||
|
||||
// Subscribe to script file changes (create/delete) from editor operations
|
||||
// 订阅编辑器操作的脚本文件变更(创建/删除)
|
||||
// Note: file:modified is handled by the Rust file watcher for external editor support
|
||||
// 注意:file:modified 由 Rust 文件监视器处理以支持外部编辑器
|
||||
messageHub.subscribe('file:created', async (data: { path: string }) => {
|
||||
if (currentProjectPath && this.isScriptFile(data.path)) {
|
||||
await compileAndLoadUserScripts(currentProjectPath);
|
||||
}
|
||||
});
|
||||
|
||||
messageHub.subscribe('file:deleted', async (data: { path: string }) => {
|
||||
if (currentProjectPath && this.isScriptFile(data.path)) {
|
||||
await compileAndLoadUserScripts(currentProjectPath);
|
||||
}
|
||||
});
|
||||
|
||||
// 注册默认场景模板 - 创建默认相机
|
||||
// Register default scene template - creates default camera
|
||||
this.registerDefaultSceneTemplate();
|
||||
@@ -406,8 +299,7 @@ export class ServiceRegistry {
|
||||
inspectorRegistry,
|
||||
propertyRendererRegistry,
|
||||
fieldEditorRegistry,
|
||||
buildService,
|
||||
userCodeService
|
||||
buildService
|
||||
};
|
||||
}
|
||||
|
||||
@@ -418,37 +310,6 @@ export class ServiceRegistry {
|
||||
}) as EventListener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a file path is a TypeScript script file (not in editor folder)
|
||||
* 检查文件路径是否为 TypeScript 脚本文件(不在 editor 文件夹中)
|
||||
*/
|
||||
private isScriptFile(filePath: string): boolean {
|
||||
// Must be .ts file | 必须是 .ts 文件
|
||||
if (!filePath.endsWith('.ts')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Normalize path separators | 规范化路径分隔符
|
||||
const normalizedPath = filePath.replace(/\\/g, '/');
|
||||
|
||||
// Must be in scripts folder | 必须在 scripts 文件夹中
|
||||
if (!normalizedPath.includes('/scripts/')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Exclude editor scripts | 排除编辑器脚本
|
||||
if (normalizedPath.includes('/scripts/editor/')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Exclude .esengine folder | 排除 .esengine 文件夹
|
||||
if (normalizedPath.includes('/.esengine/')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册默认场景模板
|
||||
* Register default scene template with default entities
|
||||
|
||||
@@ -40,7 +40,6 @@ import {
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
import { MessageHub, FileActionRegistry, type FileCreationTemplate } from '@esengine/editor-core';
|
||||
import { TauriAPI, DirectoryEntry } from '../api/tauri';
|
||||
import { SettingsService } from '../services/SettingsService';
|
||||
import { ContextMenu, ContextMenuItem } from './ContextMenu';
|
||||
import { PromptDialog } from './PromptDialog';
|
||||
import '../styles/ContentBrowser.css';
|
||||
@@ -125,9 +124,6 @@ export function ContentBrowser({
|
||||
const messageHub = Core.services.resolve(MessageHub);
|
||||
const fileActionRegistry = Core.services.resolve(FileActionRegistry);
|
||||
|
||||
// Refs
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// State
|
||||
const [currentPath, setCurrentPath] = useState<string | null>(null);
|
||||
const [selectedPaths, setSelectedPaths] = useState<Set<string>>(new Set());
|
||||
@@ -214,177 +210,8 @@ export function ContentBrowser({
|
||||
'Shader': { en: 'Shader', zh: '着色器' },
|
||||
'Tilemap': { en: 'Tilemap', 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;
|
||||
|
||||
/**
|
||||
* 组件添加到实体时调用
|
||||
* Called when component is added to entity
|
||||
*/
|
||||
onAddedToEntity(): void {
|
||||
console.log('${className} added to entity');
|
||||
}
|
||||
|
||||
/**
|
||||
* 组件从实体移除时调用
|
||||
* Called when component is removed from entity
|
||||
*/
|
||||
onRemovedFromEntity(): void {
|
||||
console.log('${className} removed from entity');
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
},
|
||||
{
|
||||
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]);
|
||||
|
||||
// 键盘快捷键处理 | Keyboard shortcuts handling
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
// 如果正在输入或有对话框打开,不处理快捷键
|
||||
// Skip shortcuts if typing or dialog is open
|
||||
if (
|
||||
e.target instanceof HTMLInputElement ||
|
||||
e.target instanceof HTMLTextAreaElement ||
|
||||
renameDialog ||
|
||||
deleteConfirmDialog ||
|
||||
createFileDialog
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 只在内容浏览器区域处理快捷键
|
||||
// Only handle shortcuts when content browser has focus
|
||||
if (!containerRef.current?.contains(document.activeElement) &&
|
||||
document.activeElement !== containerRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
// F2 - 重命名 | Rename
|
||||
if (e.key === 'F2' && selectedPaths.size === 1) {
|
||||
e.preventDefault();
|
||||
const selectedPath = Array.from(selectedPaths)[0];
|
||||
const asset = assets.find(a => a.path === selectedPath);
|
||||
if (asset) {
|
||||
setRenameDialog({ asset, newName: asset.name });
|
||||
}
|
||||
}
|
||||
|
||||
// Delete - 删除 | Delete
|
||||
if (e.key === 'Delete' && selectedPaths.size === 1) {
|
||||
e.preventDefault();
|
||||
const selectedPath = Array.from(selectedPaths)[0];
|
||||
const asset = assets.find(a => a.path === selectedPath);
|
||||
if (asset) {
|
||||
setDeleteConfirmDialog(asset);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
return () => document.removeEventListener('keydown', handleKeyDown);
|
||||
}, [selectedPaths, assets, renameDialog, deleteConfirmDialog, createFileDialog]);
|
||||
|
||||
const getTemplateLabel = (label: string): string => {
|
||||
const mapping = templateLabels[label];
|
||||
if (mapping) {
|
||||
@@ -566,9 +393,6 @@ export function ${name.charAt(0).toLowerCase() + name.slice(1)}(): void {
|
||||
|
||||
// Handle asset click
|
||||
const handleAssetClick = useCallback((asset: AssetItem, e: React.MouseEvent) => {
|
||||
// 聚焦容器以启用键盘快捷键 | Focus container to enable keyboard shortcuts
|
||||
containerRef.current?.focus();
|
||||
|
||||
if (e.shiftKey && lastSelectedPath) {
|
||||
const lastIndex = assets.findIndex(a => a.path === lastSelectedPath);
|
||||
const currentIndex = assets.findIndex(a => a.path === asset.path);
|
||||
@@ -615,27 +439,6 @@ export function ${name.charAt(0).toLowerCase() + name.slice(1)}(): void {
|
||||
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) {
|
||||
// 使用项目路径,如果没有则使用文件所在目录
|
||||
// Use project path, or file's parent directory if not available
|
||||
const workingDir = projectPath || asset.path.substring(0, asset.path.lastIndexOf('\\')) || asset.path.substring(0, asset.path.lastIndexOf('/'));
|
||||
try {
|
||||
await TauriAPI.openWithEditor(workingDir, editorCommand, asset.path);
|
||||
return;
|
||||
} catch (error) {
|
||||
console.error('Failed to open with editor:', error);
|
||||
// 如果失败,回退到系统默认应用
|
||||
// Fall back to system default app if failed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (fileActionRegistry) {
|
||||
const handled = await fileActionRegistry.handleDoubleClick(asset.path);
|
||||
if (handled) return;
|
||||
@@ -647,7 +450,7 @@ export function ${name.charAt(0).toLowerCase() + name.slice(1)}(): void {
|
||||
console.error('Failed to open file:', error);
|
||||
}
|
||||
}
|
||||
}, [loadAssets, onOpenScene, fileActionRegistry, projectPath]);
|
||||
}, [loadAssets, onOpenScene, fileActionRegistry]);
|
||||
|
||||
// Handle context menu
|
||||
const handleContextMenu = useCallback((e: React.MouseEvent, asset?: AssetItem) => {
|
||||
@@ -686,38 +489,21 @@ export function ${name.charAt(0).toLowerCase() + name.slice(1)}(): void {
|
||||
// Handle delete
|
||||
const handleDelete = useCallback(async (asset: AssetItem) => {
|
||||
try {
|
||||
const deletedPath = asset.path;
|
||||
|
||||
if (asset.type === 'folder') {
|
||||
await TauriAPI.deleteFolder(asset.path);
|
||||
// Also delete folder meta file if exists | 同时删除文件夹的 meta 文件
|
||||
try {
|
||||
await TauriAPI.deleteFile(`${asset.path}.meta`);
|
||||
} catch {
|
||||
// Meta file may not exist, ignore | meta 文件可能不存在,忽略
|
||||
}
|
||||
} else {
|
||||
await TauriAPI.deleteFile(asset.path);
|
||||
// Also delete corresponding meta file if exists | 同时删除对应的 meta 文件
|
||||
try {
|
||||
await TauriAPI.deleteFile(`${asset.path}.meta`);
|
||||
} catch {
|
||||
// Meta file may not exist, ignore | meta 文件可能不存在,忽略
|
||||
}
|
||||
}
|
||||
|
||||
if (currentPath) {
|
||||
await loadAssets(currentPath);
|
||||
}
|
||||
|
||||
// Notify that a file was deleted | 通知文件已删除
|
||||
messageHub?.publish('file:deleted', { path: deletedPath });
|
||||
|
||||
setDeleteConfirmDialog(null);
|
||||
} catch (error) {
|
||||
console.error('Failed to delete:', error);
|
||||
}
|
||||
}, [currentPath, loadAssets, messageHub]);
|
||||
}, [currentPath, loadAssets]);
|
||||
|
||||
// Get breadcrumbs
|
||||
const getBreadcrumbs = useCallback(() => {
|
||||
@@ -1013,10 +799,9 @@ export function ${name.charAt(0).toLowerCase() + name.slice(1)}(): void {
|
||||
icon: <ExternalLink size={16} />,
|
||||
onClick: async () => {
|
||||
try {
|
||||
console.log('[ContentBrowser] showInFolder path:', asset.path);
|
||||
await TauriAPI.showInFolder(asset.path);
|
||||
} catch (error) {
|
||||
console.error('Failed to show in folder:', error, 'Path:', asset.path);
|
||||
console.error('Failed to show in folder:', error);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1075,11 +860,7 @@ export function ${name.charAt(0).toLowerCase() + name.slice(1)}(): void {
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={`content-browser ${isDrawer ? 'is-drawer' : ''}`}
|
||||
tabIndex={-1}
|
||||
>
|
||||
<div className={`content-browser ${isDrawer ? 'is-drawer' : ''}`}>
|
||||
{/* Left Panel - Folder Tree */}
|
||||
<div className="content-browser-left">
|
||||
{/* Favorites Section */}
|
||||
@@ -1345,16 +1126,10 @@ export function ${name.charAt(0).toLowerCase() + name.slice(1)}(): void {
|
||||
)}
|
||||
|
||||
{/* Create File Dialog */}
|
||||
{createFileDialog && (() => {
|
||||
// 规范化扩展名(确保有点号前缀)
|
||||
// Normalize extension (ensure dot prefix)
|
||||
const ext = createFileDialog.template.extension.startsWith('.')
|
||||
? createFileDialog.template.extension
|
||||
: `.${createFileDialog.template.extension}`;
|
||||
return (
|
||||
{createFileDialog && (
|
||||
<PromptDialog
|
||||
title={locale === 'zh' ? `新建 ${getTemplateLabel(createFileDialog.template.label)}` : `New ${createFileDialog.template.label}`}
|
||||
message={locale === 'zh' ? `输入文件名(将添加 ${ext}):` : `Enter file name (${ext} will be added):`}
|
||||
title={`New ${createFileDialog.template.label}`}
|
||||
message={`Enter file name (.${createFileDialog.template.extension} will be added):`}
|
||||
placeholder="filename"
|
||||
confirmText={locale === 'zh' ? '创建' : 'Create'}
|
||||
cancelText={locale === 'zh' ? '取消' : 'Cancel'}
|
||||
@@ -1363,8 +1138,8 @@ export function ${name.charAt(0).toLowerCase() + name.slice(1)}(): void {
|
||||
setCreateFileDialog(null);
|
||||
|
||||
let fileName = value;
|
||||
if (!fileName.endsWith(ext)) {
|
||||
fileName = `${fileName}${ext}`;
|
||||
if (!fileName.endsWith(`.${template.extension}`)) {
|
||||
fileName = `${fileName}.${template.extension}`;
|
||||
}
|
||||
const filePath = `${parentPath}/${fileName}`;
|
||||
|
||||
@@ -1374,17 +1149,13 @@ export function ${name.charAt(0).toLowerCase() + name.slice(1)}(): void {
|
||||
if (currentPath) {
|
||||
await loadAssets(currentPath);
|
||||
}
|
||||
|
||||
// Notify that a file was created | 通知文件已创建
|
||||
messageHub?.publish('file:created', { path: filePath });
|
||||
} catch (error) {
|
||||
console.error('Failed to create file:', error);
|
||||
}
|
||||
}}
|
||||
onCancel={() => setCreateFileDialog(null)}
|
||||
/>
|
||||
);
|
||||
})()}
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
} from 'lucide-react';
|
||||
import { TauriAPI, DirectoryEntry } from '../api/tauri';
|
||||
import { MessageHub, FileActionRegistry } from '@esengine/editor-core';
|
||||
import { SettingsService } from '../services/SettingsService';
|
||||
import { Core } from '@esengine/ecs-framework';
|
||||
import { ContextMenu, ContextMenuItem } from './ContextMenu';
|
||||
import { ConfirmDialog } from './ConfirmDialog';
|
||||
@@ -834,10 +833,9 @@ export const FileTree = forwardRef<FileTreeHandle, FileTreeProps>(({ rootPath, o
|
||||
icon: <FolderOpen size={16} />,
|
||||
onClick: async () => {
|
||||
try {
|
||||
console.log('[FileTree] showInFolder path:', node.path);
|
||||
await TauriAPI.showInFolder(node.path);
|
||||
} catch (error) {
|
||||
console.error('Failed to show in folder:', error, 'Path:', node.path);
|
||||
console.error('Failed to show in folder:', error);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -901,27 +899,6 @@ export const FileTree = forwardRef<FileTreeHandle, FileTreeProps>(({ rootPath, o
|
||||
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) {
|
||||
// 使用项目路径,如果没有则使用文件所在目录
|
||||
// Use project path, or file's parent directory if not available
|
||||
const workingDir = rootPath || node.path.substring(0, node.path.lastIndexOf('\\')) || node.path.substring(0, node.path.lastIndexOf('/'));
|
||||
try {
|
||||
await TauriAPI.openWithEditor(workingDir, editorCommand, node.path);
|
||||
return;
|
||||
} catch (error) {
|
||||
console.error('Failed to open with editor:', error);
|
||||
// 如果失败,回退到系统默认应用
|
||||
// Fall back to system default app if failed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (fileActionRegistry) {
|
||||
const handled = await fileActionRegistry.handleDoubleClick(node.path);
|
||||
if (handled) {
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { getVersion } from '@tauri-apps/api/app';
|
||||
import { Globe, ChevronDown, Download, X, Loader2, Trash2, CheckCircle, AlertCircle } from 'lucide-react';
|
||||
import { Globe, ChevronDown, Download, X, Loader2 } from 'lucide-react';
|
||||
import { checkForUpdatesOnStartup, installUpdate, type UpdateCheckResult } from '../utils/updater';
|
||||
import { StartupLogo } from './StartupLogo';
|
||||
import { TauriAPI, type EnvironmentCheckResult } from '../api/tauri';
|
||||
import '../styles/StartupPage.css';
|
||||
|
||||
type Locale = 'en' | 'zh';
|
||||
@@ -12,8 +11,6 @@ interface StartupPageProps {
|
||||
onOpenProject: () => void;
|
||||
onCreateProject: () => void;
|
||||
onOpenRecentProject?: (projectPath: string) => void;
|
||||
onRemoveRecentProject?: (projectPath: string) => void;
|
||||
onDeleteProject?: (projectPath: string) => Promise<void>;
|
||||
onLocaleChange?: (locale: Locale) => void;
|
||||
recentProjects?: string[];
|
||||
locale: string;
|
||||
@@ -24,18 +21,14 @@ const LANGUAGES = [
|
||||
{ code: 'zh', name: '中文' }
|
||||
];
|
||||
|
||||
export function StartupPage({ onOpenProject, onCreateProject, onOpenRecentProject, onRemoveRecentProject, onDeleteProject, onLocaleChange, recentProjects = [], locale }: StartupPageProps) {
|
||||
export function StartupPage({ onOpenProject, onCreateProject, onOpenRecentProject, onLocaleChange, recentProjects = [], locale }: StartupPageProps) {
|
||||
const [showLogo, setShowLogo] = useState(true);
|
||||
const [hoveredProject, setHoveredProject] = useState<string | null>(null);
|
||||
const [appVersion, setAppVersion] = useState<string>('');
|
||||
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 [showUpdateBanner, setShowUpdateBanner] = useState(false);
|
||||
const [isInstalling, setIsInstalling] = useState(false);
|
||||
const [envCheck, setEnvCheck] = useState<EnvironmentCheckResult | null>(null);
|
||||
const [showEnvStatus, setShowEnvStatus] = useState(false);
|
||||
const langMenuRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -62,24 +55,6 @@ export function StartupPage({ onOpenProject, onCreateProject, onOpenRecentProjec
|
||||
});
|
||||
}, []);
|
||||
|
||||
// 启动时检测开发环境
|
||||
useEffect(() => {
|
||||
TauriAPI.checkEnvironment().then((result) => {
|
||||
setEnvCheck(result);
|
||||
// 如果环境就绪,在控制台显示信息
|
||||
if (result.ready) {
|
||||
console.log('[Environment] Ready ✓');
|
||||
console.log(`[Environment] esbuild: ${result.esbuild.version} (${result.esbuild.source})`);
|
||||
} else {
|
||||
// 环境有问题,显示提示
|
||||
setShowEnvStatus(true);
|
||||
console.warn('[Environment] Not ready:', result.esbuild.error);
|
||||
}
|
||||
}).catch((error) => {
|
||||
console.error('[Environment] Check failed:', error);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const translations = {
|
||||
en: {
|
||||
title: 'ESEngine Editor',
|
||||
@@ -91,17 +66,7 @@ export function StartupPage({ onOpenProject, onCreateProject, onOpenRecentProjec
|
||||
updateAvailable: 'New version available',
|
||||
updateNow: 'Update Now',
|
||||
installing: 'Installing...',
|
||||
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',
|
||||
envReady: 'Environment Ready',
|
||||
envNotReady: 'Environment Issue',
|
||||
esbuildReady: 'esbuild ready',
|
||||
esbuildMissing: 'esbuild not found'
|
||||
later: 'Later'
|
||||
},
|
||||
zh: {
|
||||
title: 'ESEngine 编辑器',
|
||||
@@ -113,17 +78,7 @@ export function StartupPage({ onOpenProject, onCreateProject, onOpenRecentProjec
|
||||
updateAvailable: '发现新版本',
|
||||
updateNow: '立即更新',
|
||||
installing: '正在安装...',
|
||||
later: '稍后',
|
||||
removeFromList: '从列表中移除',
|
||||
deleteProject: '删除项目',
|
||||
deleteConfirmTitle: '删除项目',
|
||||
deleteConfirmMessage: '确定要永久删除此项目吗?此操作无法撤销。',
|
||||
cancel: '取消',
|
||||
delete: '删除',
|
||||
envReady: '环境就绪',
|
||||
envNotReady: '环境问题',
|
||||
esbuildReady: 'esbuild 就绪',
|
||||
esbuildMissing: '未找到 esbuild'
|
||||
later: '稍后'
|
||||
}
|
||||
};
|
||||
|
||||
@@ -181,10 +136,6 @@ export function StartupPage({ onOpenProject, onCreateProject, onOpenRecentProjec
|
||||
onMouseEnter={() => setHoveredProject(project)}
|
||||
onMouseLeave={() => setHoveredProject(null)}
|
||||
onClick={() => onOpenRecentProject?.(project)}
|
||||
onContextMenu={(e) => {
|
||||
e.preventDefault();
|
||||
setContextMenu({ x: e.clientX, y: e.clientY, project });
|
||||
}}
|
||||
style={{ cursor: onOpenRecentProject ? 'pointer' : 'default' }}
|
||||
>
|
||||
<svg className="recent-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
@@ -194,18 +145,6 @@ export function StartupPage({ onOpenProject, onCreateProject, onOpenRecentProjec
|
||||
<div className="recent-name">{project.split(/[\\/]/).pop()}</div>
|
||||
<div className="recent-path">{project}</div>
|
||||
</div>
|
||||
{onRemoveRecentProject && (
|
||||
<button
|
||||
className="recent-remove-btn"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onRemoveRecentProject(project);
|
||||
}}
|
||||
title={t.removeFromList}
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
</button>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
@@ -249,43 +188,6 @@ export function StartupPage({ onOpenProject, onCreateProject, onOpenRecentProjec
|
||||
|
||||
<div className="startup-footer">
|
||||
<span className="startup-version">{versionText}</span>
|
||||
|
||||
{/* 环境状态指示器 | Environment Status Indicator */}
|
||||
{envCheck && (
|
||||
<div
|
||||
className={`startup-env-status ${envCheck.ready ? 'ready' : 'warning'}`}
|
||||
onClick={() => setShowEnvStatus(!showEnvStatus)}
|
||||
title={envCheck.ready ? t.envReady : t.envNotReady}
|
||||
>
|
||||
{envCheck.ready ? (
|
||||
<CheckCircle size={14} />
|
||||
) : (
|
||||
<AlertCircle size={14} />
|
||||
)}
|
||||
{showEnvStatus && (
|
||||
<div className="startup-env-tooltip">
|
||||
<div className="env-tooltip-title">
|
||||
{envCheck.ready ? t.envReady : t.envNotReady}
|
||||
</div>
|
||||
<div className={`env-tooltip-item ${envCheck.esbuild.available ? 'ok' : 'error'}`}>
|
||||
{envCheck.esbuild.available ? (
|
||||
<>
|
||||
<CheckCircle size={12} />
|
||||
<span>esbuild {envCheck.esbuild.version}</span>
|
||||
<span className="env-source">({envCheck.esbuild.source})</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<AlertCircle size={12} />
|
||||
<span>{t.esbuildMissing}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{onLocaleChange && (
|
||||
<div className="startup-locale-dropdown" ref={langMenuRef}>
|
||||
<button
|
||||
@@ -315,83 +217,6 @@ export function StartupPage({ onOpenProject, onCreateProject, onOpenRecentProjec
|
||||
</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) {
|
||||
try {
|
||||
await onDeleteProject(deleteConfirm);
|
||||
} catch (error) {
|
||||
console.error('[StartupPage] Failed to delete project:', error);
|
||||
// Error will be handled by App.tsx error dialog
|
||||
}
|
||||
}
|
||||
setDeleteConfirm(null);
|
||||
}}
|
||||
>
|
||||
{t.delete}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -25,12 +25,8 @@ 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[], hasUserRuntime: boolean = false): string {
|
||||
function generateRuntimeHtml(importMap: Record<string, string>, modules: ModuleManifest[]): string {
|
||||
const importMapScript = `<script type="importmap">
|
||||
${JSON.stringify({ imports: importMap }, null, 2).split('\n').join('\n ')}
|
||||
</script>`;
|
||||
@@ -49,44 +45,6 @@ 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>
|
||||
@@ -178,7 +136,7 @@ ${importMapScript}
|
||||
${pluginImportCode}
|
||||
|
||||
await runtime.initialize(wasmModule);
|
||||
${userRuntimeCode}
|
||||
|
||||
updateLoading('Loading scene...');
|
||||
await runtime.loadScene('./scene.json?_=' + Date.now());
|
||||
|
||||
@@ -723,9 +681,9 @@ export function Viewport({ locale = 'en', messageHub }: ViewportProps) {
|
||||
// Save editor camera state
|
||||
editorCameraRef.current = { x: camera2DOffset.x, y: camera2DOffset.y, zoom: camera2DZoom };
|
||||
setPlayState('playing');
|
||||
// Disable editor mode (hides grid, gizmos, axis indicator)
|
||||
// 禁用编辑器模式(隐藏网格、gizmos、坐标轴指示器)
|
||||
EngineService.getInstance().setEditorMode(false);
|
||||
// Hide grid and gizmos in play mode
|
||||
EngineService.getInstance().setShowGrid(false);
|
||||
EngineService.getInstance().setShowGizmos(false);
|
||||
// Switch to player camera
|
||||
syncPlayerCamera();
|
||||
engine.start();
|
||||
@@ -750,9 +708,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 editor mode (restores grid, gizmos, axis indicator based on settings)
|
||||
// 恢复编辑器模式(根据设置恢复网格、gizmos、坐标轴指示器)
|
||||
EngineService.getInstance().setEditorMode(true);
|
||||
// Restore grid and gizmos
|
||||
EngineService.getInstance().setShowGrid(showGrid);
|
||||
EngineService.getInstance().setShowGizmos(showGizmos);
|
||||
// Restore editor default background color
|
||||
EngineService.getInstance().setClearColor(0.1, 0.1, 0.12, 1.0);
|
||||
};
|
||||
@@ -930,21 +888,8 @@ 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, hasUserRuntime);
|
||||
const runtimeHtml = generateRuntimeHtml(importMap, modules);
|
||||
await TauriAPI.writeFileContent(`${runtimeDir}/index.html`, runtimeHtml);
|
||||
|
||||
// Start local server and open browser
|
||||
@@ -1009,26 +954,10 @@ export function Viewport({ locale = 'en', messageHub }: ViewportProps) {
|
||||
}
|
||||
}
|
||||
|
||||
// Write scene data
|
||||
// Write scene data and HTML with import maps
|
||||
const sceneDataStr = typeof sceneData === 'string' ? sceneData : new TextDecoder().decode(sceneData);
|
||||
await TauriAPI.writeFileContent(`${runtimeDir}/scene.json`, sceneDataStr);
|
||||
|
||||
// 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));
|
||||
await TauriAPI.writeFileContent(`${runtimeDir}/index.html`, generateRuntimeHtml(importMap, modules));
|
||||
|
||||
// Copy textures referenced in scene
|
||||
const assetsDir = `${runtimeDir}\\assets`;
|
||||
|
||||
@@ -14,15 +14,6 @@ import { EditorEngineSync } from '../services/EditorEngineSync';
|
||||
let engineInitialized = false;
|
||||
let engineInitializing = false;
|
||||
|
||||
/**
|
||||
* 重置引擎初始化状态(在项目关闭时调用)
|
||||
* Reset engine initialization state (called when project is closed)
|
||||
*/
|
||||
export function resetEngineState(): void {
|
||||
engineInitialized = false;
|
||||
engineInitializing = false;
|
||||
}
|
||||
|
||||
export interface EngineState {
|
||||
initialized: boolean;
|
||||
running: boolean;
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
/**
|
||||
* Asset Meta Plugin
|
||||
* 资产元数据插件
|
||||
*
|
||||
* Handles .meta file generation for project assets.
|
||||
* 处理项目资产的 .meta 文件生成。
|
||||
*/
|
||||
|
||||
import type { ServiceContainer } from '@esengine/ecs-framework';
|
||||
import { createLogger } from '@esengine/ecs-framework';
|
||||
import type { IPlugin, IEditorModuleLoader, ModuleManifest } from '@esengine/editor-core';
|
||||
import { AssetRegistryService } from '@esengine/editor-core';
|
||||
|
||||
const logger = createLogger('AssetMetaPlugin');
|
||||
|
||||
/**
|
||||
* Asset Meta Editor Module
|
||||
* 资产元数据编辑器模块
|
||||
*/
|
||||
class AssetMetaEditorModule implements IEditorModuleLoader {
|
||||
private _assetRegistry: AssetRegistryService | null = null;
|
||||
|
||||
async install(_services: ServiceContainer): Promise<void> {
|
||||
// 创建 AssetRegistryService 并初始化
|
||||
// Create AssetRegistryService and initialize
|
||||
this._assetRegistry = new AssetRegistryService();
|
||||
|
||||
// 初始化服务(订阅 project:opened 事件)
|
||||
// Initialize service (subscribes to project:opened event)
|
||||
await this._assetRegistry.initialize();
|
||||
|
||||
logger.info('AssetRegistryService initialized');
|
||||
}
|
||||
|
||||
async uninstall(): Promise<void> {
|
||||
if (this._assetRegistry) {
|
||||
this._assetRegistry.unloadProject();
|
||||
this._assetRegistry = null;
|
||||
}
|
||||
logger.info('Uninstalled');
|
||||
}
|
||||
|
||||
async onEditorReady(): Promise<void> {
|
||||
logger.info('Editor is ready');
|
||||
}
|
||||
}
|
||||
|
||||
const manifest: ModuleManifest = {
|
||||
id: '@esengine/asset-meta',
|
||||
name: '@esengine/asset-meta',
|
||||
displayName: 'Asset Meta',
|
||||
version: '1.0.0',
|
||||
description: 'Generates .meta files for project assets | 为项目资产生成 .meta 文件',
|
||||
category: 'Other',
|
||||
icon: 'FileJson',
|
||||
isCore: true,
|
||||
defaultEnabled: true,
|
||||
isEngineModule: false, // 不是引擎模块,不会被打包到 runtime
|
||||
canContainContent: false,
|
||||
dependencies: [],
|
||||
exports: {}
|
||||
};
|
||||
|
||||
export const AssetMetaPlugin: IPlugin = {
|
||||
manifest,
|
||||
editorModule: new AssetMetaEditorModule()
|
||||
};
|
||||
@@ -56,32 +56,6 @@ class EditorAppearanceEditorModule implements IEditorModuleLoader {
|
||||
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++'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
@@ -8,7 +8,6 @@ export { SceneInspectorPlugin } from './SceneInspectorPlugin';
|
||||
export { ProfilerPlugin } from './ProfilerPlugin';
|
||||
export { EditorAppearancePlugin } from './EditorAppearancePlugin';
|
||||
export { ProjectSettingsPlugin } from './ProjectSettingsPlugin';
|
||||
export { AssetMetaPlugin } from './AssetMetaPlugin';
|
||||
// Note: PluginConfigPlugin removed - module management is now unified in ProjectSettingsPlugin
|
||||
// TODO: Re-enable when blueprint-editor package is fixed
|
||||
// export { BlueprintPlugin } from '@esengine/blueprint-editor';
|
||||
|
||||
@@ -28,7 +28,6 @@ import {
|
||||
type GameRuntimeConfig
|
||||
} from '@esengine/runtime-core';
|
||||
import { getMaterialManager } from '@esengine/material-system';
|
||||
import { resetEngineState } from '../hooks/useEngine';
|
||||
import { convertFileSrc } from '@tauri-apps/api/core';
|
||||
import { IdGenerator } from '../utils/idGenerator';
|
||||
import { TauriAssetReader } from './TauriAssetReader';
|
||||
@@ -246,14 +245,7 @@ export class EngineService {
|
||||
ctx.uiInputSystem.unbind?.();
|
||||
}
|
||||
|
||||
// 清理 viewport | Clear viewport
|
||||
this.unregisterViewport('editor-viewport');
|
||||
|
||||
// 重置 useEngine 的模块级状态 | Reset useEngine module-level state
|
||||
resetEngineState();
|
||||
|
||||
this._modulesInitialized = false;
|
||||
this._initialized = false;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -612,26 +604,6 @@ 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.
|
||||
*/
|
||||
|
||||
@@ -70,11 +70,9 @@ export class SettingsService {
|
||||
}
|
||||
|
||||
public addRecentProject(projectPath: string): void {
|
||||
// 规范化路径,防止双重转义 | Normalize path to prevent double escaping
|
||||
const normalizedPath = projectPath.replace(/\\\\/g, '\\');
|
||||
const recentProjects = this.getRecentProjects();
|
||||
const filtered = recentProjects.filter((p) => p !== normalizedPath);
|
||||
const updated = [normalizedPath, ...filtered].slice(0, 10);
|
||||
const filtered = recentProjects.filter((p) => p !== projectPath);
|
||||
const updated = [projectPath, ...filtered].slice(0, 10);
|
||||
this.set('recentProjects', updated);
|
||||
}
|
||||
|
||||
@@ -87,64 +85,4 @@ export class SettingsService {
|
||||
public clearRecentProjects(): void {
|
||||
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 || '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,19 +22,10 @@ export class TauriFileSystemService implements IFileSystem {
|
||||
|
||||
async exists(path: string): Promise<boolean> {
|
||||
try {
|
||||
// 首先尝试作为目录列出内容
|
||||
// First try to list as directory
|
||||
await invoke('list_directory', { path });
|
||||
await invoke('read_file_content', { path });
|
||||
return true;
|
||||
} catch {
|
||||
// 如果不是目录,尝试读取文件
|
||||
// If not a directory, try reading as file
|
||||
try {
|
||||
await invoke('read_file_content', { path });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,19 +34,11 @@ export class TauriFileSystemService implements IFileSystem {
|
||||
}
|
||||
|
||||
async listDirectory(path: string): Promise<FileEntry[]> {
|
||||
const entries = await invoke<Array<{
|
||||
name: string;
|
||||
path: string;
|
||||
is_dir: boolean;
|
||||
size?: number;
|
||||
modified?: number;
|
||||
}>>('list_directory', { path });
|
||||
const entries = await invoke<Array<{ name: string; path: string; is_dir: boolean }>>('list_directory', { path });
|
||||
return entries.map((entry) => ({
|
||||
name: entry.name,
|
||||
isDirectory: entry.is_dir,
|
||||
path: entry.path,
|
||||
size: entry.size,
|
||||
modified: entry.modified ? new Date(entry.modified * 1000) : undefined
|
||||
path: entry.path
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
background: #1e1e1e;
|
||||
color: #e0e0e0;
|
||||
font-size: 12px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.content-browser.is-drawer {
|
||||
|
||||
@@ -174,32 +174,6 @@
|
||||
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 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -372,246 +346,3 @@
|
||||
opacity: 0.5;
|
||||
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;
|
||||
}
|
||||
|
||||
/* 环境状态指示器样式 | Environment Status Indicator Styles */
|
||||
.startup-env-status {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.startup-env-status.ready {
|
||||
color: #4ade80;
|
||||
}
|
||||
|
||||
.startup-env-status.ready:hover {
|
||||
background: rgba(74, 222, 128, 0.1);
|
||||
}
|
||||
|
||||
.startup-env-status.warning {
|
||||
color: #f59e0b;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
.startup-env-status.warning:hover {
|
||||
background: rgba(245, 158, 11, 0.1);
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.6; }
|
||||
}
|
||||
|
||||
.startup-env-tooltip {
|
||||
position: absolute;
|
||||
bottom: 100%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
margin-bottom: 8px;
|
||||
padding: 12px 16px;
|
||||
min-width: 200px;
|
||||
background: #2d2d30;
|
||||
border: 1px solid #3e3e42;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.startup-env-tooltip::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
border: 6px solid transparent;
|
||||
border-top-color: #3e3e42;
|
||||
}
|
||||
|
||||
.env-tooltip-title {
|
||||
margin-bottom: 10px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid #3e3e42;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.env-tooltip-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.env-tooltip-item.ok {
|
||||
color: #4ade80;
|
||||
}
|
||||
|
||||
.env-tooltip-item.error {
|
||||
color: #f87171;
|
||||
}
|
||||
|
||||
.env-source {
|
||||
opacity: 0.6;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
@@ -213,15 +213,6 @@ 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)
|
||||
|
||||
@@ -1159,11 +1159,6 @@ 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');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
* Uses .meta files to persistently store each asset's GUID.
|
||||
*/
|
||||
|
||||
import { Core, createLogger, PlatformDetector } from '@esengine/ecs-framework';
|
||||
import { Core, createLogger } from '@esengine/ecs-framework';
|
||||
import { MessageHub } from './MessageHub';
|
||||
import {
|
||||
AssetMetaManager,
|
||||
@@ -20,8 +20,6 @@ import {
|
||||
IMetaFileSystem,
|
||||
inferAssetType
|
||||
} from '@esengine/asset-system-editor';
|
||||
import type { IFileSystem, FileEntry } from './IFileSystem';
|
||||
import { IFileSystemService } from './IFileSystem';
|
||||
|
||||
// Logger for AssetRegistry using core's logger
|
||||
const logger = createLogger('AssetRegistry');
|
||||
@@ -116,9 +114,6 @@ const EXTENSION_TYPE_MAP: Record<string, AssetRegistryType> = {
|
||||
// Data
|
||||
'.json': 'json',
|
||||
'.txt': 'text',
|
||||
// Scripts
|
||||
'.ts': 'script',
|
||||
'.js': 'script',
|
||||
// Custom types
|
||||
'.btree': 'btree',
|
||||
'.ecs': 'scene',
|
||||
@@ -127,8 +122,17 @@ const EXTENSION_TYPE_MAP: Record<string, AssetRegistryType> = {
|
||||
'.tsx': 'tileset',
|
||||
};
|
||||
|
||||
// 使用从 IFileSystem.ts 导入的标准接口
|
||||
// Using standard interface imported from IFileSystem.ts
|
||||
/**
|
||||
* File system interface for asset scanning
|
||||
*/
|
||||
interface IFileSystem {
|
||||
readDir(path: string): Promise<string[]>;
|
||||
readFile(path: string): Promise<string>;
|
||||
writeFile(path: string, content: string): Promise<void>;
|
||||
exists(path: string): Promise<boolean>;
|
||||
stat(path: string): Promise<{ size: number; mtime: number; isDirectory: boolean }>;
|
||||
isDirectory(path: string): Promise<boolean>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple in-memory asset database
|
||||
@@ -215,9 +219,6 @@ export class AssetRegistryService {
|
||||
/** Asset meta manager for .meta file management */
|
||||
private _metaManager: AssetMetaManager;
|
||||
|
||||
/** Tauri event unlisten function | Tauri 事件取消监听函数 */
|
||||
private _eventUnlisten: (() => void) | undefined;
|
||||
|
||||
/** Manifest file name */
|
||||
static readonly MANIFEST_FILE = 'asset-manifest.json';
|
||||
/** Current manifest version */
|
||||
@@ -242,9 +243,9 @@ export class AssetRegistryService {
|
||||
async initialize(): Promise<void> {
|
||||
if (this._initialized) return;
|
||||
|
||||
// Get file system service using the exported Symbol
|
||||
// 使用导出的 Symbol 获取文件系统服务
|
||||
this._fileSystem = Core.services.tryResolve(IFileSystemService) as IFileSystem | null;
|
||||
// Get file system service
|
||||
const IFileSystemServiceKey = Symbol.for('IFileSystemService');
|
||||
this._fileSystem = Core.services.tryResolve(IFileSystemServiceKey) as IFileSystem | null;
|
||||
|
||||
// Get message hub
|
||||
this._messageHub = Core.services.tryResolve(MessageHub) as MessageHub | null;
|
||||
@@ -253,11 +254,10 @@ export class AssetRegistryService {
|
||||
if (this._messageHub) {
|
||||
this._messageHub.subscribe('project:opened', this._onProjectOpened.bind(this));
|
||||
this._messageHub.subscribe('project:closed', this._onProjectClosed.bind(this));
|
||||
} else {
|
||||
logger.warn('MessageHub not available, cannot subscribe to project events');
|
||||
}
|
||||
|
||||
this._initialized = true;
|
||||
logger.info('AssetRegistryService initialized');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -288,16 +288,18 @@ export class AssetRegistryService {
|
||||
this._metaManager.clear();
|
||||
|
||||
// Setup MetaManager with file system adapter
|
||||
// 设置 MetaManager 的文件系统适配器
|
||||
const metaFs: IMetaFileSystem = {
|
||||
exists: (path: string) => this._fileSystem!.exists(path),
|
||||
readText: (path: string) => this._fileSystem!.readFile(path),
|
||||
writeText: (path: string, content: string) => this._fileSystem!.writeFile(path, content),
|
||||
delete: async (path: string) => {
|
||||
// Try to delete using deleteFile
|
||||
// 尝试使用 deleteFile 删除
|
||||
// Try to delete, ignore if not exists
|
||||
try {
|
||||
await this._fileSystem!.deleteFile(path);
|
||||
// Note: IFileSystem may not have delete, handle gracefully
|
||||
const fs = this._fileSystem as IFileSystem & { delete?: (p: string) => Promise<void> };
|
||||
if (fs.delete) {
|
||||
await fs.delete(path);
|
||||
}
|
||||
} catch {
|
||||
// Ignore delete errors
|
||||
}
|
||||
@@ -314,10 +316,6 @@ export class AssetRegistryService {
|
||||
// Save updated manifest
|
||||
await this._saveManifest();
|
||||
|
||||
// Subscribe to file change events (Tauri only)
|
||||
// 订阅文件变化事件(仅 Tauri 环境)
|
||||
await this._subscribeToFileChanges();
|
||||
|
||||
logger.info(`Project assets loaded: ${this._database.getStatistics().totalAssets} assets`);
|
||||
|
||||
// Publish event
|
||||
@@ -327,77 +325,10 @@ export class AssetRegistryService {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to file change events from Tauri backend
|
||||
* 订阅来自 Tauri 后端的文件变化事件
|
||||
*/
|
||||
private async _subscribeToFileChanges(): Promise<void> {
|
||||
// Only in Tauri environment
|
||||
// 仅在 Tauri 环境中
|
||||
if (!PlatformDetector.isTauriEnvironment()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { listen } = await import('@tauri-apps/api/event');
|
||||
|
||||
// Listen to user-code:file-changed event
|
||||
// 监听 user-code:file-changed 事件
|
||||
this._eventUnlisten = await listen<{
|
||||
changeType: string;
|
||||
paths: string[];
|
||||
}>('user-code:file-changed', async (event) => {
|
||||
const { changeType, paths } = event.payload;
|
||||
|
||||
logger.debug('File change event received | 收到文件变化事件', { changeType, paths });
|
||||
|
||||
// Handle file creation - register new assets and generate .meta
|
||||
// 处理文件创建 - 注册新资产并生成 .meta
|
||||
if (changeType === 'create' || changeType === 'modify') {
|
||||
for (const absolutePath of paths) {
|
||||
// Skip .meta files
|
||||
if (absolutePath.endsWith('.meta')) continue;
|
||||
|
||||
// Register or refresh the asset
|
||||
await this.registerAsset(absolutePath);
|
||||
}
|
||||
} else if (changeType === 'remove') {
|
||||
for (const absolutePath of paths) {
|
||||
// Skip .meta files
|
||||
if (absolutePath.endsWith('.meta')) continue;
|
||||
|
||||
// Unregister the asset
|
||||
await this.unregisterAsset(absolutePath);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
logger.info('Subscribed to file change events | 已订阅文件变化事件');
|
||||
} catch (error) {
|
||||
logger.warn('Failed to subscribe to file change events | 订阅文件变化事件失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribe from file change events
|
||||
* 取消订阅文件变化事件
|
||||
*/
|
||||
private _unsubscribeFromFileChanges(): void {
|
||||
if (this._eventUnlisten) {
|
||||
this._eventUnlisten();
|
||||
this._eventUnlisten = undefined;
|
||||
logger.debug('Unsubscribed from file change events | 已取消订阅文件变化事件');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unload current project
|
||||
*/
|
||||
unloadProject(): void {
|
||||
// Unsubscribe from file change events
|
||||
// 取消订阅文件变化事件
|
||||
this._unsubscribeFromFileChanges();
|
||||
|
||||
this._projectPath = null;
|
||||
this._manifest = null;
|
||||
this._database.clear();
|
||||
@@ -467,61 +398,53 @@ export class AssetRegistryService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan all project directories for assets
|
||||
* 扫描项目中所有目录的资产
|
||||
* Scan assets directory and register all assets
|
||||
*/
|
||||
private async _scanAssetsDirectory(): Promise<void> {
|
||||
if (!this._fileSystem || !this._projectPath) return;
|
||||
|
||||
const sep = this._projectPath.includes('\\') ? '\\' : '/';
|
||||
const assetsPath = `${this._projectPath}${sep}assets`;
|
||||
|
||||
// 扫描多个目录:assets, scripts, scenes
|
||||
// Scan multiple directories: assets, scripts, scenes
|
||||
const directoriesToScan = [
|
||||
{ path: `${this._projectPath}${sep}assets`, name: 'assets' },
|
||||
{ path: `${this._projectPath}${sep}scripts`, name: 'scripts' },
|
||||
{ path: `${this._projectPath}${sep}scenes`, name: 'scenes' }
|
||||
];
|
||||
|
||||
for (const dir of directoriesToScan) {
|
||||
try {
|
||||
const exists = await this._fileSystem.exists(dir.path);
|
||||
if (!exists) continue;
|
||||
|
||||
await this._scanDirectory(dir.path, dir.name);
|
||||
} catch (error) {
|
||||
logger.error(`Failed to scan ${dir.name} directory:`, error);
|
||||
try {
|
||||
const exists = await this._fileSystem.exists(assetsPath);
|
||||
if (!exists) {
|
||||
logger.info('No assets directory found');
|
||||
return;
|
||||
}
|
||||
|
||||
await this._scanDirectory(assetsPath, 'assets');
|
||||
} catch (error) {
|
||||
logger.error('Failed to scan assets directory:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively scan a directory
|
||||
* 递归扫描目录
|
||||
*/
|
||||
private async _scanDirectory(absolutePath: string, relativePath: string): Promise<void> {
|
||||
if (!this._fileSystem) return;
|
||||
|
||||
try {
|
||||
// 使用标准 IFileSystem.listDirectory
|
||||
// Use standard IFileSystem.listDirectory
|
||||
const entries: FileEntry[] = await this._fileSystem.listDirectory(absolutePath);
|
||||
const entries = await this._fileSystem.readDir(absolutePath);
|
||||
const sep = absolutePath.includes('\\') ? '\\' : '/';
|
||||
|
||||
for (const entry of entries) {
|
||||
const entryAbsPath = entry.path || `${absolutePath}${sep}${entry.name}`;
|
||||
const entryRelPath = `${relativePath}/${entry.name}`;
|
||||
const entryAbsPath = `${absolutePath}${sep}${entry}`;
|
||||
const entryRelPath = `${relativePath}/${entry}`;
|
||||
|
||||
try {
|
||||
if (entry.isDirectory) {
|
||||
const isDir = await this._fileSystem.isDirectory(entryAbsPath);
|
||||
|
||||
if (isDir) {
|
||||
// Recursively scan subdirectory
|
||||
await this._scanDirectory(entryAbsPath, entryRelPath);
|
||||
} else {
|
||||
// Register file as asset with size from entry
|
||||
await this._registerAssetFile(entryAbsPath, entryRelPath, entry.size, entry.modified);
|
||||
// Register file as asset
|
||||
await this._registerAssetFile(entryAbsPath, entryRelPath);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn(`Failed to process entry ${entry.name}:`, error);
|
||||
logger.warn(`Failed to process entry ${entry}:`, error);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -531,19 +454,8 @@ export class AssetRegistryService {
|
||||
|
||||
/**
|
||||
* Register a single asset file
|
||||
* 注册单个资产文件
|
||||
*
|
||||
* @param absolutePath - 绝对路径 | Absolute path
|
||||
* @param relativePath - 相对路径 | Relative path
|
||||
* @param size - 文件大小(可选)| File size (optional)
|
||||
* @param modified - 修改时间(可选)| Modified time (optional)
|
||||
*/
|
||||
private async _registerAssetFile(
|
||||
absolutePath: string,
|
||||
relativePath: string,
|
||||
size?: number,
|
||||
modified?: Date
|
||||
): Promise<void> {
|
||||
private async _registerAssetFile(absolutePath: string, relativePath: string): Promise<void> {
|
||||
if (!this._fileSystem || !this._manifest) return;
|
||||
|
||||
// Skip .meta files
|
||||
@@ -559,16 +471,18 @@ export class AssetRegistryService {
|
||||
// Skip unknown file types
|
||||
if (!assetType || assetType === 'binary') return;
|
||||
|
||||
// Use provided size/modified or default values
|
||||
const fileSize = size ?? 0;
|
||||
const fileMtime = modified ? modified.getTime() : Date.now();
|
||||
// Get file info
|
||||
let stat: { size: number; mtime: number };
|
||||
try {
|
||||
stat = await this._fileSystem.stat(absolutePath);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
// Use MetaManager to get or create meta (with .meta file)
|
||||
let meta: IAssetMeta;
|
||||
try {
|
||||
logger.debug(`Creating/loading meta for: ${relativePath}`);
|
||||
meta = await this._metaManager.getOrCreateMeta(absolutePath);
|
||||
logger.debug(`Meta created/loaded for ${relativePath}: guid=${meta.guid}`);
|
||||
} catch (e) {
|
||||
logger.warn(`Failed to get meta for ${relativePath}:`, e);
|
||||
return;
|
||||
@@ -596,9 +510,9 @@ export class AssetRegistryService {
|
||||
path: relativePath,
|
||||
type: assetType,
|
||||
name,
|
||||
size: fileSize,
|
||||
size: stat.size,
|
||||
hash: '', // Could compute hash if needed
|
||||
lastModified: fileMtime
|
||||
lastModified: stat.mtime
|
||||
};
|
||||
|
||||
// Register in database
|
||||
|
||||
@@ -94,15 +94,11 @@ export class ProjectService implements IService {
|
||||
scriptsPath: 'scripts',
|
||||
buildOutput: '.esengine/compiled',
|
||||
scenesPath: 'scenes',
|
||||
defaultScene: 'main.ecs',
|
||||
plugins: { enabledPlugins: [] },
|
||||
modules: { disabledModules: [] }
|
||||
defaultScene: 'main.ecs'
|
||||
};
|
||||
|
||||
await this.fileAPI.writeFileContent(configPath, JSON.stringify(config, null, 2));
|
||||
|
||||
// Create scenes folder and default scene
|
||||
// 创建场景文件夹和默认场景
|
||||
const scenesPath = `${projectPath}${sep}${config.scenesPath}`;
|
||||
await this.fileAPI.createDirectory(scenesPath);
|
||||
|
||||
@@ -115,71 +111,6 @@ export class ProjectService implements IService {
|
||||
}) as string;
|
||||
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 tsconfig.json for runtime scripts (components, systems)
|
||||
// 创建运行时脚本的 tsconfig.json(组件、系统等)
|
||||
// Note: paths will be populated by update_project_tsconfig when project is opened
|
||||
// 注意:paths 会在项目打开时由 update_project_tsconfig 填充
|
||||
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
|
||||
// paths will be added by editor when project is opened
|
||||
// paths 会在编辑器打开项目时添加
|
||||
},
|
||||
include: ['scripts/**/*.ts'],
|
||||
exclude: ['scripts/editor/**/*.ts', '.esengine']
|
||||
};
|
||||
const tsConfigPath = `${projectPath}${sep}tsconfig.json`;
|
||||
await this.fileAPI.writeFileContent(tsConfigPath, JSON.stringify(tsConfig, null, 2));
|
||||
|
||||
// Create tsconfig.editor.json for editor extension scripts
|
||||
// 创建编辑器扩展脚本的 tsconfig.editor.json
|
||||
const tsConfigEditor = {
|
||||
compilerOptions: {
|
||||
target: 'ES2020',
|
||||
module: 'ESNext',
|
||||
moduleResolution: 'bundler',
|
||||
lib: ['ES2020', 'DOM'],
|
||||
strict: true,
|
||||
esModuleInterop: true,
|
||||
skipLibCheck: true,
|
||||
forceConsistentCasingInFileNames: true,
|
||||
experimentalDecorators: true,
|
||||
emitDecoratorMetadata: true,
|
||||
noEmit: true
|
||||
// paths will be added by editor when project is opened
|
||||
// paths 会在编辑器打开项目时添加
|
||||
},
|
||||
include: ['scripts/editor/**/*.ts'],
|
||||
exclude: ['.esengine']
|
||||
};
|
||||
const tsConfigEditorPath = `${projectPath}${sep}tsconfig.editor.json`;
|
||||
await this.fileAPI.writeFileContent(tsConfigEditorPath, JSON.stringify(tsConfigEditor, null, 2));
|
||||
|
||||
await this.messageHub.publish('project:created', {
|
||||
path: projectPath
|
||||
});
|
||||
@@ -327,10 +258,8 @@ export class ProjectService implements IService {
|
||||
scenesPath: config.scenesPath || 'scenes',
|
||||
defaultScene: config.defaultScene || 'main.ecs',
|
||||
uiDesignResolution: config.uiDesignResolution,
|
||||
// Provide default empty plugins config for legacy projects
|
||||
// 为旧项目提供默认的空插件配置
|
||||
plugins: config.plugins || { enabledPlugins: [] },
|
||||
modules: config.modules || { disabledModules: [] }
|
||||
plugins: config.plugins,
|
||||
modules: config.modules
|
||||
};
|
||||
logger.debug('Loaded config result:', result);
|
||||
return result;
|
||||
|
||||
@@ -62,10 +62,6 @@ export class SceneManagerService implements IService {
|
||||
throw new Error('No active scene');
|
||||
}
|
||||
|
||||
// 确保编辑器模式下设置 isEditorMode,延迟组件生命周期回调
|
||||
// Ensure isEditorMode is set in editor to defer component lifecycle callbacks
|
||||
scene.isEditorMode = true;
|
||||
|
||||
// 只移除实体,保留系统(系统由模块管理)
|
||||
// Only remove entities, preserve systems (systems managed by modules)
|
||||
scene.entities.removeAllEntities();
|
||||
@@ -121,11 +117,6 @@ export class SceneManagerService implements IService {
|
||||
if (!scene) {
|
||||
throw new Error('No active scene');
|
||||
}
|
||||
|
||||
// 确保编辑器模式下设置 isEditorMode,延迟组件生命周期回调
|
||||
// Ensure isEditorMode is set in editor to defer component lifecycle callbacks
|
||||
scene.isEditorMode = true;
|
||||
|
||||
scene.deserialize(jsonData, {
|
||||
strategy: 'replace'
|
||||
});
|
||||
|
||||
@@ -215,9 +215,8 @@ export interface IUserCodeService {
|
||||
* - Classes extending System
|
||||
*
|
||||
* @param module - User code module | 用户代码模块
|
||||
* @param componentRegistry - Optional ComponentRegistry to register components | 可选的 ComponentRegistry 用于注册组件
|
||||
*/
|
||||
registerComponents(module: UserCodeModule, componentRegistry?: any): void;
|
||||
registerComponents(module: UserCodeModule): void;
|
||||
|
||||
/**
|
||||
* Register editor extensions from user module.
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
*/
|
||||
|
||||
import type { IService } from '@esengine/ecs-framework';
|
||||
import { Injectable, createLogger, PlatformDetector, ComponentRegistry as CoreComponentRegistry } from '@esengine/ecs-framework';
|
||||
import { Injectable, createLogger } from '@esengine/ecs-framework';
|
||||
import type {
|
||||
IUserCodeService,
|
||||
UserScriptInfo,
|
||||
@@ -110,14 +110,9 @@ export class UserCodeService implements IService, IUserCodeService {
|
||||
const errors: CompileError[] = [];
|
||||
const warnings: CompileError[] = [];
|
||||
|
||||
// Store project path for later use in load() | 存储项目路径供 load() 使用
|
||||
this._currentProjectPath = options.projectPath;
|
||||
|
||||
const sep = options.projectPath.includes('\\') ? '\\' : '/';
|
||||
const scriptsDir = `${options.projectPath}${sep}${SCRIPTS_DIR}`;
|
||||
// Ensure consistent path separators | 确保路径分隔符一致
|
||||
const userCodeOutputDir = USER_CODE_OUTPUT_DIR.replace(/\//g, sep);
|
||||
const outputDir = options.outputDir || `${options.projectPath}${sep}${userCodeOutputDir}`;
|
||||
const outputDir = options.outputDir || `${options.projectPath}${sep}${USER_CODE_OUTPUT_DIR}`;
|
||||
|
||||
try {
|
||||
// Scan scripts first | 先扫描脚本
|
||||
@@ -151,35 +146,14 @@ export class UserCodeService implements IService, IUserCodeService {
|
||||
const entryPath = `${outputDir}${sep}_entry_${options.target}.ts`;
|
||||
await this._fileSystem.writeFile(entryPath, entryContent);
|
||||
|
||||
// Create shim files for framework dependencies | 创建框架依赖的 shim 文件
|
||||
await this._createDependencyShims(outputDir, options.target);
|
||||
|
||||
// Determine global name for IIFE output | 确定 IIFE 输出的全局名称
|
||||
const globalName = options.target === UserCodeTarget.Runtime
|
||||
? '__USER_RUNTIME_EXPORTS__'
|
||||
: '__USER_EDITOR_EXPORTS__';
|
||||
|
||||
// Build alias map for framework dependencies | 构建框架依赖的别名映射
|
||||
const shimPath = `${outputDir}${sep}_shim_ecs_framework.js`.replace(/\\/g, '/');
|
||||
const alias: Record<string, string> = {
|
||||
'@esengine/ecs-framework': shimPath,
|
||||
'@esengine/core': shimPath,
|
||||
'@esengine/engine-core': shimPath,
|
||||
'@esengine/math': shimPath
|
||||
};
|
||||
|
||||
// Compile using esbuild (via Tauri command or direct) | 使用 esbuild 编译
|
||||
// Use IIFE format to avoid ES module import issues in Tauri
|
||||
// 使用 IIFE 格式以避免 Tauri 中的 ES 模块导入问题
|
||||
const compileResult = await this._runEsbuild({
|
||||
entryPath,
|
||||
outputPath,
|
||||
format: 'iife', // Always use IIFE for Tauri compatibility | 始终使用 IIFE 以兼容 Tauri
|
||||
globalName,
|
||||
format: options.format || 'esm',
|
||||
sourceMap: options.sourceMap ?? true,
|
||||
minify: options.minify ?? false,
|
||||
external: [], // Don't use external, use alias instead | 不使用 external,使用 alias
|
||||
alias,
|
||||
external: this._getExternalDependencies(options.target),
|
||||
projectRoot: options.projectPath
|
||||
});
|
||||
|
||||
@@ -233,30 +207,12 @@ export class UserCodeService implements IService, IUserCodeService {
|
||||
*/
|
||||
async load(modulePath: string, target: UserCodeTarget): Promise<UserCodeModule> {
|
||||
try {
|
||||
let moduleExports: Record<string, any>;
|
||||
// Add cache-busting query parameter for hot reload | 添加缓存破坏参数用于热更新
|
||||
const cacheBuster = `?t=${Date.now()}`;
|
||||
const moduleUrl = `file://${modulePath}${cacheBuster}`;
|
||||
|
||||
if (PlatformDetector.isTauriEnvironment()) {
|
||||
// In Tauri, read file content and execute via script tag
|
||||
// 在 Tauri 中,读取文件内容并通过 script 标签执行
|
||||
// This avoids CORS and module resolution issues
|
||||
// 这避免了 CORS 和模块解析问题
|
||||
const { invoke } = await import('@tauri-apps/api/core');
|
||||
|
||||
const content = await invoke<string>('read_file_content', {
|
||||
path: modulePath
|
||||
});
|
||||
|
||||
logger.debug(`Loading module via script injection`, { originalPath: modulePath });
|
||||
|
||||
// Execute module code and capture exports | 执行模块代码并捕获导出
|
||||
moduleExports = await this._executeModuleCode(content, target);
|
||||
} else {
|
||||
// Fallback to file:// for non-Tauri environments
|
||||
// 非 Tauri 环境使用 file://
|
||||
const cacheBuster = `?t=${Date.now()}`;
|
||||
const moduleUrl = `file://${modulePath}${cacheBuster}`;
|
||||
moduleExports = await import(/* @vite-ignore */ moduleUrl);
|
||||
}
|
||||
// Dynamic import the module | 动态导入模块
|
||||
const moduleExports = await import(/* @vite-ignore */ moduleUrl);
|
||||
|
||||
const module: UserCodeModule = {
|
||||
id: `user-${target}-${Date.now()}`,
|
||||
@@ -317,7 +273,7 @@ export class UserCodeService implements IService, IUserCodeService {
|
||||
*
|
||||
* @param module - User code module | 用户代码模块
|
||||
*/
|
||||
registerComponents(module: UserCodeModule, componentRegistry?: any): void {
|
||||
registerComponents(module: UserCodeModule): void {
|
||||
if (module.target !== UserCodeTarget.Runtime) {
|
||||
logger.warn('Cannot register components from editor module | 无法从编辑器模块注册组件');
|
||||
return;
|
||||
@@ -333,38 +289,10 @@ export class UserCodeService implements IService, IUserCodeService {
|
||||
|
||||
// Check if it's a Component subclass | 检查是否是 Component 子类
|
||||
if (this._isComponentClass(exported)) {
|
||||
// Register with ComponentRegistry | 注册到 ComponentRegistry
|
||||
// Note: Actual registration depends on runtime context
|
||||
// 注意:实际注册取决于运行时上下文
|
||||
logger.debug(`Found component: ${name} | 发现组件: ${name}`);
|
||||
|
||||
// Register with Core ComponentRegistry for serialization/deserialization
|
||||
// 注册到核心 ComponentRegistry 用于序列化/反序列化
|
||||
try {
|
||||
CoreComponentRegistry.register(exported);
|
||||
// Debug: verify registration
|
||||
const registeredType = CoreComponentRegistry.getComponentType(name);
|
||||
if (registeredType) {
|
||||
logger.info(`Component ${name} registered to core registry successfully`);
|
||||
} else {
|
||||
logger.warn(`Component ${name} registered but not found by name lookup`);
|
||||
}
|
||||
} catch (err) {
|
||||
logger.warn(`Failed to register component ${name} to core registry | 注册组件 ${name} 到核心注册表失败:`, err);
|
||||
}
|
||||
|
||||
// Register with Editor ComponentRegistry for UI display
|
||||
// 注册到编辑器 ComponentRegistry 用于 UI 显示
|
||||
if (componentRegistry && typeof componentRegistry.register === 'function') {
|
||||
try {
|
||||
componentRegistry.register({
|
||||
name: name,
|
||||
type: exported,
|
||||
category: 'User', // User-defined components | 用户自定义组件
|
||||
description: `User component: ${name}`
|
||||
});
|
||||
} catch (err) {
|
||||
logger.warn(`Failed to register component ${name} to editor registry | 注册组件 ${name} 到编辑器注册表失败:`, err);
|
||||
}
|
||||
}
|
||||
|
||||
componentCount++;
|
||||
}
|
||||
|
||||
@@ -381,83 +309,6 @@ export class UserCodeService implements IService, IUserCodeService {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Hot reload: update existing component instances to use new class prototype.
|
||||
* 热更新:更新现有组件实例以使用新类的原型。
|
||||
*
|
||||
* This is the core of hot reload - it updates the prototype chain of existing
|
||||
* instances so they use the new methods from the updated class while preserving
|
||||
* their data (properties).
|
||||
* 这是热更新的核心 - 它更新现有实例的原型链,使它们使用更新后类的新方法,
|
||||
* 同时保留它们的数据(属性)。
|
||||
*
|
||||
* @param module - New user code module | 新的用户代码模块
|
||||
* @returns Number of instances updated | 更新的实例数量
|
||||
*/
|
||||
hotReloadInstances(module: UserCodeModule): number {
|
||||
if (module.target !== UserCodeTarget.Runtime) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Access scene through Core.scene
|
||||
// 通过 Core.scene 访问场景
|
||||
const Core = (window as any).__ESENGINE__?.ecsFramework?.Core;
|
||||
const scene = Core?.scene;
|
||||
if (!scene || !scene.entities) {
|
||||
logger.warn('No active scene for hot reload | 没有活动场景用于热更新');
|
||||
return 0;
|
||||
}
|
||||
|
||||
let updatedCount = 0;
|
||||
|
||||
// EntityList.buffer contains all entities
|
||||
// EntityList.buffer 包含所有实体
|
||||
const entities: any[] = scene.entities.buffer || [];
|
||||
|
||||
for (const entity of entities) {
|
||||
if (!entity) continue;
|
||||
|
||||
// entity.components is a getter that returns readonly Component[]
|
||||
// entity.components 是一个 getter,返回 readonly Component[]
|
||||
const components = entity.components;
|
||||
if (!components || !Array.isArray(components)) continue;
|
||||
|
||||
for (const component of components) {
|
||||
if (!component) continue;
|
||||
|
||||
// Get the component's type name
|
||||
// 获取组件的类型名称
|
||||
const typeName = component.constructor?.name;
|
||||
if (!typeName) continue;
|
||||
|
||||
// Check if we have a new version of this component class
|
||||
// 检查是否有此组件类的新版本
|
||||
const newClass = module.exports[typeName];
|
||||
if (!newClass || typeof newClass !== 'function') continue;
|
||||
|
||||
// Check if this is actually a different class (hot reload scenario)
|
||||
// 检查这是否确实是不同的类(热更新场景)
|
||||
if (component.constructor === newClass) continue;
|
||||
|
||||
// Update the prototype chain to use the new class
|
||||
// 更新原型链以使用新类
|
||||
try {
|
||||
Object.setPrototypeOf(component, newClass.prototype);
|
||||
updatedCount++;
|
||||
logger.debug(`Hot reloaded component instance: ${typeName} on entity ${entity.name || entity.id}`);
|
||||
} catch (err) {
|
||||
logger.warn(`Failed to hot reload ${typeName}:`, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (updatedCount > 0) {
|
||||
logger.info(`Hot reload: updated ${updatedCount} component instances | 热更新:更新了 ${updatedCount} 个组件实例`);
|
||||
}
|
||||
|
||||
return updatedCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register editor extensions from user module.
|
||||
* 从用户模块注册编辑器扩展。
|
||||
@@ -522,7 +373,7 @@ export class UserCodeService implements IService, IUserCodeService {
|
||||
|
||||
try {
|
||||
// Check if we're in Tauri environment | 检查是否在 Tauri 环境
|
||||
if (PlatformDetector.isTauriEnvironment()) {
|
||||
if (typeof window !== 'undefined' && '__TAURI__' in window) {
|
||||
const { invoke } = await import('@tauri-apps/api/core');
|
||||
const { listen } = await import('@tauri-apps/api/event');
|
||||
|
||||
@@ -610,7 +461,7 @@ export class UserCodeService implements IService, IUserCodeService {
|
||||
}
|
||||
|
||||
// Stop backend file watcher | 停止后端文件监视器
|
||||
if (PlatformDetector.isTauriEnvironment()) {
|
||||
if (typeof window !== 'undefined' && '__TAURI__' in window) {
|
||||
const { invoke } = await import('@tauri-apps/api/core');
|
||||
await invoke('stop_watch_scripts', {
|
||||
projectPath: this._currentProjectPath
|
||||
@@ -726,17 +577,6 @@ export class UserCodeService implements IService, IUserCodeService {
|
||||
/**
|
||||
* Build entry point content that re-exports all user scripts.
|
||||
* 构建重新导出所有用户脚本的入口点内容。
|
||||
*
|
||||
* Entry file is in: {projectPath}/.esengine/compiled/_entry_runtime.ts
|
||||
* Scripts are in: {projectPath}/scripts/
|
||||
* So the relative path from entry to scripts is: ../../scripts/
|
||||
*
|
||||
* For IIFE format, we inject shims that map global variables to module imports.
|
||||
* This allows user code to use `import { Component } from '@esengine/ecs-framework'`
|
||||
* while actually accessing `window.__ESENGINE_FRAMEWORK__`.
|
||||
* 对于 IIFE 格式,我们注入 shim 将全局变量映射到模块导入。
|
||||
* 这使用户代码可以使用 `import { Component } from '@esengine/ecs-framework'`,
|
||||
* 实际上访问的是 `window.__ESENGINE_FRAMEWORK__`。
|
||||
*/
|
||||
private _buildEntryPoint(
|
||||
scripts: UserScriptInfo[],
|
||||
@@ -749,60 +589,22 @@ export class UserCodeService implements IService, IUserCodeService {
|
||||
''
|
||||
];
|
||||
|
||||
// Entry file is in .esengine/compiled/, need to go up 2 levels to reach project root
|
||||
// 入口文件在 .esengine/compiled/ 目录,需要上升 2 级到达项目根目录
|
||||
const relativePrefix = `../../${SCRIPTS_DIR}`;
|
||||
|
||||
for (const script of scripts) {
|
||||
// Convert absolute path to relative import | 将绝对路径转换为相对导入
|
||||
const relativePath = script.relativePath.replace(/\\/g, '/').replace(/\.tsx?$/, '');
|
||||
|
||||
if (script.exports.length > 0) {
|
||||
lines.push(`export { ${script.exports.join(', ')} } from '${relativePrefix}/${relativePath}';`);
|
||||
lines.push(`export { ${script.exports.join(', ')} } from './${SCRIPTS_DIR}/${relativePath}';`);
|
||||
} else {
|
||||
// Re-export everything if we couldn't detect specific exports
|
||||
// 如果无法检测到具体导出,则重新导出所有内容
|
||||
lines.push(`export * from '${relativePrefix}/${relativePath}';`);
|
||||
lines.push(`export * from './${SCRIPTS_DIR}/${relativePath}';`);
|
||||
}
|
||||
}
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create shim files that map global variables to module imports.
|
||||
* 创建将全局变量映射到模块导入的 shim 文件。
|
||||
*
|
||||
* This is used for IIFE format to resolve external dependencies.
|
||||
* The shim exports the global __ESENGINE__.ecsFramework which is set by PluginSDKRegistry.
|
||||
* 这用于 IIFE 格式解析外部依赖。
|
||||
* shim 导出全局的 __ESENGINE__.ecsFramework,由 PluginSDKRegistry 设置。
|
||||
*
|
||||
* @param outputDir - Output directory | 输出目录
|
||||
* @param target - Target environment | 目标环境
|
||||
* @returns Array of shim file paths | shim 文件路径数组
|
||||
*/
|
||||
private async _createDependencyShims(
|
||||
outputDir: string,
|
||||
target: UserCodeTarget
|
||||
): Promise<string[]> {
|
||||
const sep = outputDir.includes('\\') ? '\\' : '/';
|
||||
const shimPaths: string[] = [];
|
||||
|
||||
// Create shim for @esengine/ecs-framework | 为 @esengine/ecs-framework 创建 shim
|
||||
// This uses window.__ESENGINE__.ecsFramework set by PluginSDKRegistry
|
||||
// 这使用 PluginSDKRegistry 设置的 window.__ESENGINE__.ecsFramework
|
||||
const ecsShimPath = `${outputDir}${sep}_shim_ecs_framework.js`;
|
||||
const ecsShimContent = `// Shim for @esengine/ecs-framework
|
||||
// Maps to window.__ESENGINE__.ecsFramework set by PluginSDKRegistry
|
||||
module.exports = (typeof window !== 'undefined' && window.__ESENGINE__ && window.__ESENGINE__.ecsFramework) || {};
|
||||
`;
|
||||
await this._fileSystem.writeFile(ecsShimPath, ecsShimContent);
|
||||
shimPaths.push(ecsShimPath);
|
||||
|
||||
return shimPaths;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get external dependencies that should not be bundled.
|
||||
* 获取不应打包的外部依赖。
|
||||
@@ -838,16 +640,14 @@ module.exports = (typeof window !== 'undefined' && window.__ESENGINE__ && window
|
||||
entryPath: string;
|
||||
outputPath: string;
|
||||
format: 'esm' | 'iife';
|
||||
globalName?: string;
|
||||
sourceMap: boolean;
|
||||
minify: boolean;
|
||||
external: string[];
|
||||
alias?: Record<string, string>;
|
||||
projectRoot: string;
|
||||
}): Promise<{ success: boolean; errors: CompileError[] }> {
|
||||
try {
|
||||
// Check if we're in Tauri environment | 检查是否在 Tauri 环境
|
||||
if (PlatformDetector.isTauriEnvironment()) {
|
||||
if (typeof window !== 'undefined' && '__TAURI__' in window) {
|
||||
// Use Tauri command | 使用 Tauri 命令
|
||||
const { invoke } = await import('@tauri-apps/api/core');
|
||||
|
||||
@@ -865,11 +665,9 @@ module.exports = (typeof window !== 'undefined' && window.__ESENGINE__ && window
|
||||
entryPath: options.entryPath,
|
||||
outputPath: options.outputPath,
|
||||
format: options.format,
|
||||
globalName: options.globalName,
|
||||
sourceMap: options.sourceMap,
|
||||
minify: options.minify,
|
||||
external: options.external,
|
||||
alias: options.alias,
|
||||
projectRoot: options.projectRoot
|
||||
}
|
||||
});
|
||||
@@ -903,56 +701,6 @@ module.exports = (typeof window !== 'undefined' && window.__ESENGINE__ && window
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute compiled module code and return exports.
|
||||
* 执行编译后的模块代码并返回导出。
|
||||
*
|
||||
* The code should be in IIFE format that sets a global variable.
|
||||
* 代码应该是设置全局变量的 IIFE 格式。
|
||||
*
|
||||
* @param code - Compiled JavaScript code | 编译后的 JavaScript 代码
|
||||
* @param target - Target environment | 目标环境
|
||||
* @returns Module exports | 模块导出
|
||||
*/
|
||||
private async _executeModuleCode(
|
||||
code: string,
|
||||
target: UserCodeTarget
|
||||
): Promise<Record<string, any>> {
|
||||
// Determine global name based on target | 根据目标确定全局名称
|
||||
const globalName = target === UserCodeTarget.Runtime
|
||||
? '__USER_RUNTIME_EXPORTS__'
|
||||
: '__USER_EDITOR_EXPORTS__';
|
||||
|
||||
// Clear any previous exports | 清除之前的导出
|
||||
(window as any)[globalName] = undefined;
|
||||
|
||||
try {
|
||||
// esbuild generates: var __USER_RUNTIME_EXPORTS__ = (() => {...})();
|
||||
// When executed via new Function(), var declarations stay in function scope
|
||||
// We need to replace "var globalName" with "window.globalName" to expose it
|
||||
// esbuild 生成: var __USER_RUNTIME_EXPORTS__ = (() => {...})();
|
||||
// 通过 new Function() 执行时,var 声明在函数作用域内
|
||||
// 需要替换 "var globalName" 为 "window.globalName" 以暴露到全局
|
||||
const modifiedCode = code.replace(
|
||||
new RegExp(`^"use strict";\\s*var ${globalName}`, 'm'),
|
||||
`"use strict";\nwindow.${globalName}`
|
||||
);
|
||||
|
||||
// Execute the IIFE code | 执行 IIFE 代码
|
||||
// eslint-disable-next-line no-new-func
|
||||
const executeScript = new Function(modifiedCode);
|
||||
executeScript();
|
||||
|
||||
// Get exports from global | 从全局获取导出
|
||||
const exports = (window as any)[globalName] || {};
|
||||
|
||||
return exports;
|
||||
} catch (error) {
|
||||
logger.error('Failed to execute user code | 执行用户代码失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure directory exists, create if not.
|
||||
* 确保目录存在,如果不存在则创建。
|
||||
@@ -971,29 +719,17 @@ module.exports = (typeof window !== 'undefined' && window.__ESENGINE__ && window
|
||||
/**
|
||||
* Check if a class extends Component.
|
||||
* 检查类是否继承自 Component。
|
||||
*
|
||||
* Uses the actual Component class from the global framework to check inheritance.
|
||||
* 使用全局框架中的实际 Component 类来检查继承关系。
|
||||
*/
|
||||
private _isComponentClass(cls: any): boolean {
|
||||
// Get Component class from global framework | 从全局框架获取 Component 类
|
||||
const framework = (window as any).__ESENGINE__?.ecsFramework;
|
||||
|
||||
if (!framework?.Component) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Use instanceof or prototype chain check | 使用 instanceof 或原型链检查
|
||||
try {
|
||||
const ComponentClass = framework.Component;
|
||||
|
||||
// Check if cls.prototype is an instance of Component
|
||||
// 检查 cls.prototype 是否是 Component 的实例
|
||||
return cls.prototype instanceof ComponentClass ||
|
||||
ComponentClass.prototype.isPrototypeOf(cls.prototype);
|
||||
} catch {
|
||||
return false;
|
||||
// Check prototype chain for Component | 检查原型链中是否有 Component
|
||||
let proto = cls.prototype;
|
||||
while (proto) {
|
||||
if (proto.constructor?.name === 'Component') {
|
||||
return true;
|
||||
}
|
||||
proto = Object.getPrototypeOf(proto);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -77,14 +77,6 @@ pub struct Engine {
|
||||
/// Whether to show gizmos.
|
||||
/// 是否显示辅助工具。
|
||||
show_gizmos: bool,
|
||||
|
||||
/// Whether the engine is running in editor mode.
|
||||
/// 引擎是否在编辑器模式下运行。
|
||||
///
|
||||
/// When false (runtime mode), editor-only UI like grid, gizmos,
|
||||
/// and axis indicator are automatically hidden.
|
||||
/// 当为 false(运行时模式)时,编辑器专用 UI(如网格、gizmos、坐标轴指示器)会自动隐藏。
|
||||
is_editor: bool,
|
||||
}
|
||||
|
||||
impl Engine {
|
||||
@@ -124,7 +116,6 @@ impl Engine {
|
||||
show_grid: true,
|
||||
viewport_manager: ViewportManager::new(),
|
||||
show_gizmos: true,
|
||||
is_editor: true, // 默认为编辑器模式 | Default to editor mode
|
||||
})
|
||||
}
|
||||
|
||||
@@ -163,7 +154,6 @@ impl Engine {
|
||||
show_grid: true,
|
||||
viewport_manager: ViewportManager::new(),
|
||||
show_gizmos: true,
|
||||
is_editor: true, // 默认为编辑器模式 | Default to editor mode
|
||||
})
|
||||
}
|
||||
|
||||
@@ -222,9 +212,8 @@ impl Engine {
|
||||
let [r, g, b, a] = self.renderer.get_clear_color();
|
||||
self.context.clear(r, g, b, a);
|
||||
|
||||
// Render grid first (background) - only in editor mode
|
||||
// 首先渲染网格(背景)- 仅在编辑器模式下
|
||||
if self.is_editor && self.show_grid {
|
||||
// Render grid first (background)
|
||||
if self.show_grid {
|
||||
self.grid_renderer.render(self.context.gl(), self.renderer.camera());
|
||||
self.grid_renderer.render_axes(self.context.gl(), self.renderer.camera());
|
||||
}
|
||||
@@ -232,9 +221,8 @@ impl Engine {
|
||||
// Render sprites
|
||||
self.renderer.render(self.context.gl(), &self.texture_manager)?;
|
||||
|
||||
// Render gizmos on top - only in editor mode
|
||||
// 在顶部渲染 gizmos - 仅在编辑器模式下
|
||||
if self.is_editor && self.show_gizmos {
|
||||
// Render gizmos on top
|
||||
if self.show_gizmos {
|
||||
self.gizmo_renderer.render(self.context.gl(), self.renderer.camera());
|
||||
// Render axis indicator in corner
|
||||
// 在角落渲染坐标轴指示器
|
||||
@@ -423,23 +411,6 @@ impl Engine {
|
||||
self.show_gizmos
|
||||
}
|
||||
|
||||
/// Set editor mode.
|
||||
/// 设置编辑器模式。
|
||||
///
|
||||
/// When false (runtime mode), editor-only UI like grid, gizmos,
|
||||
/// and axis indicator are automatically hidden regardless of their individual settings.
|
||||
/// 当为 false(运行时模式)时,编辑器专用 UI(如网格、gizmos、坐标轴指示器)
|
||||
/// 会自动隐藏,无论它们的单独设置如何。
|
||||
pub fn set_editor_mode(&mut self, is_editor: bool) {
|
||||
self.is_editor = is_editor;
|
||||
}
|
||||
|
||||
/// Get editor mode.
|
||||
/// 获取编辑器模式。
|
||||
pub fn is_editor(&self) -> bool {
|
||||
self.is_editor
|
||||
}
|
||||
|
||||
/// Set clear color for the active viewport.
|
||||
/// 设置活动视口的清除颜色。
|
||||
pub fn set_clear_color(&mut self, r: f32, g: f32, b: f32, a: f32) {
|
||||
@@ -533,9 +504,8 @@ impl Engine {
|
||||
renderer_camera.rotation = camera.rotation;
|
||||
renderer_camera.set_viewport(camera.viewport_width(), camera.viewport_height());
|
||||
|
||||
// Render grid if enabled - only in editor mode
|
||||
// 渲染网格(如果启用)- 仅在编辑器模式下
|
||||
if self.is_editor && show_grid {
|
||||
// Render grid if enabled
|
||||
if show_grid {
|
||||
self.grid_renderer.render(viewport.gl(), &camera);
|
||||
self.grid_renderer.render_axes(viewport.gl(), &camera);
|
||||
}
|
||||
@@ -543,9 +513,8 @@ impl Engine {
|
||||
// Render sprites
|
||||
self.renderer.render(viewport.gl(), &self.texture_manager)?;
|
||||
|
||||
// Render gizmos if enabled - only in editor mode
|
||||
// 渲染 gizmos(如果启用)- 仅在编辑器模式下
|
||||
if self.is_editor && show_gizmos {
|
||||
// Render gizmos if enabled
|
||||
if show_gizmos {
|
||||
self.gizmo_renderer.render(viewport.gl(), &camera);
|
||||
// Render axis indicator in corner
|
||||
// 在角落渲染坐标轴指示器
|
||||
|
||||
@@ -390,24 +390,6 @@ impl GameEngine {
|
||||
self.engine.set_show_gizmos(show);
|
||||
}
|
||||
|
||||
/// Set editor mode.
|
||||
/// 设置编辑器模式。
|
||||
///
|
||||
/// When false (runtime mode), editor-only UI like grid, gizmos,
|
||||
/// and axis indicator are automatically hidden.
|
||||
/// 当为 false(运行时模式)时,编辑器专用 UI(如网格、gizmos、坐标轴指示器)会自动隐藏。
|
||||
#[wasm_bindgen(js_name = setEditorMode)]
|
||||
pub fn set_editor_mode(&mut self, is_editor: bool) {
|
||||
self.engine.set_editor_mode(is_editor);
|
||||
}
|
||||
|
||||
/// Get editor mode.
|
||||
/// 获取编辑器模式。
|
||||
#[wasm_bindgen(js_name = isEditorMode)]
|
||||
pub fn is_editor_mode(&self) -> bool {
|
||||
self.engine.is_editor()
|
||||
}
|
||||
|
||||
// ===== Multi-viewport API =====
|
||||
// ===== 多视口 API =====
|
||||
|
||||
|
||||
@@ -138,9 +138,9 @@ export class BrowserRuntime {
|
||||
this._runtime.assetManager.setReader(this._assetReader);
|
||||
}
|
||||
|
||||
// Disable editor mode (hides grid, gizmos, axis indicator)
|
||||
// 禁用编辑器模式(隐藏网格、gizmos、坐标轴指示器)
|
||||
this._runtime.setEditorMode(false);
|
||||
// Browser-specific settings (no editor UI)
|
||||
this._runtime.setShowGrid(false);
|
||||
this._runtime.setShowGizmos(false);
|
||||
|
||||
this._initialized = true;
|
||||
console.log('[Runtime] Initialized');
|
||||
|
||||
@@ -218,12 +218,6 @@ export class GameRuntime {
|
||||
Core.setScene(this._scene);
|
||||
}
|
||||
|
||||
// 编辑器模式下设置 isEditorMode,延迟组件生命周期回调
|
||||
// Set isEditorMode in editor mode to defer component lifecycle callbacks
|
||||
if (this._platform.isEditorMode()) {
|
||||
this._scene.isEditorMode = true;
|
||||
}
|
||||
|
||||
// 6. 添加基础系统
|
||||
this._scene.addSystem(new HierarchySystem());
|
||||
this._scene.addSystem(new TransformSystem());
|
||||
@@ -408,12 +402,6 @@ export class GameRuntime {
|
||||
this._renderSystem.setPreviewMode(true);
|
||||
}
|
||||
|
||||
// 调用场景 begin() 触发延迟的组件生命周期回调
|
||||
// Call scene begin() to trigger deferred component lifecycle callbacks
|
||||
if (this._scene) {
|
||||
this._scene.begin();
|
||||
}
|
||||
|
||||
// 启用游戏逻辑系统
|
||||
this._enableGameLogicSystems();
|
||||
|
||||
@@ -588,31 +576,6 @@ export class GameRuntime {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置编辑器模式
|
||||
* 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 {
|
||||
if (this._bridge) {
|
||||
this._bridge.setEditorMode(isEditor);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取编辑器模式
|
||||
* Get editor mode
|
||||
*/
|
||||
isEditorMode(): boolean {
|
||||
if (this._bridge) {
|
||||
return this._bridge.isEditorMode();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置清除颜色
|
||||
* Set clear color
|
||||
|
||||
@@ -110,9 +110,7 @@ function copyModule(module) {
|
||||
fs.copyFileSync(module.moduleJsonPath, destModuleJson);
|
||||
|
||||
// Copy dist/index.js if exists
|
||||
// 如果存在则拷贝 dist/index.js
|
||||
let hasRuntime = false;
|
||||
let hasTypes = false;
|
||||
let sizeKB = 'N/A';
|
||||
|
||||
if (fs.existsSync(module.distPath)) {
|
||||
@@ -120,7 +118,6 @@ function copyModule(module) {
|
||||
fs.copyFileSync(module.distPath, destIndexJs);
|
||||
|
||||
// Also copy source map if exists
|
||||
// 如果存在则拷贝 source map
|
||||
const sourceMapPath = module.distPath + '.map';
|
||||
if (fs.existsSync(sourceMapPath)) {
|
||||
fs.copyFileSync(sourceMapPath, destIndexJs + '.map');
|
||||
@@ -131,16 +128,7 @@ function copyModule(module) {
|
||||
hasRuntime = true;
|
||||
}
|
||||
|
||||
// Copy type definitions (.d.ts) if exists
|
||||
// 如果存在则拷贝类型定义文件 (.d.ts)
|
||||
const typesPath = module.distPath.replace(/\.js$/, '.d.ts');
|
||||
if (fs.existsSync(typesPath)) {
|
||||
const destDts = path.join(moduleOutputDir, 'index.d.ts');
|
||||
fs.copyFileSync(typesPath, destDts);
|
||||
hasTypes = true;
|
||||
}
|
||||
|
||||
return { hasRuntime, hasTypes, sizeKB };
|
||||
return { hasRuntime, sizeKB };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -215,11 +203,10 @@ function main() {
|
||||
const moduleInfos = [];
|
||||
|
||||
for (const module of modules) {
|
||||
const { hasRuntime, hasTypes, sizeKB } = copyModule(module);
|
||||
const { hasRuntime, sizeKB } = copyModule(module);
|
||||
|
||||
if (hasRuntime) {
|
||||
const typesIndicator = hasTypes ? ' +types' : '';
|
||||
console.log(` ✓ ${module.id} (${sizeKB} KB${typesIndicator})`);
|
||||
console.log(` ✓ ${module.id} (${sizeKB} KB)`);
|
||||
} else {
|
||||
console.log(` ○ ${module.id} (config only)`);
|
||||
}
|
||||
@@ -229,7 +216,6 @@ function main() {
|
||||
name: module.name,
|
||||
displayName: module.displayName,
|
||||
hasRuntime,
|
||||
hasTypes,
|
||||
editorPackage: module.editorPackage,
|
||||
isCore: module.isCore,
|
||||
category: module.category
|
||||
|
||||
Reference in New Issue
Block a user