diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 66de7352..eaac9e8b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,6 +8,7 @@ on: - 'package.json' - 'pnpm-lock.yaml' - 'tsconfig.json' + - 'turbo.json' - 'jest.config.*' - '.github/workflows/ci.yml' pull_request: @@ -17,11 +18,12 @@ on: - 'package.json' - 'pnpm-lock.yaml' - 'tsconfig.json' + - 'turbo.json' - 'jest.config.*' - '.github/workflows/ci.yml' jobs: - test: + ci: runs-on: ubuntu-latest steps: @@ -39,22 +41,47 @@ jobs: node-version: '20.x' cache: 'pnpm' + - name: Install Rust stable + uses: dtolnay/rust-toolchain@stable + with: + targets: wasm32-unknown-unknown + + # 缓存 Rust 编译结果 + - name: Cache Rust dependencies + uses: Swatinem/rust-cache@v2 + with: + workspaces: packages/engine + cache-on-failure: true + + # 缓存 wasm-pack + - name: Cache wasm-pack + uses: actions/cache@v4 + with: + path: ~/.cargo/bin/wasm-pack + key: wasm-pack-${{ runner.os }} + + - name: Install wasm-pack + run: | + if ! command -v wasm-pack &> /dev/null; then + cargo install wasm-pack + fi + - name: Install dependencies run: pnpm install - - name: Install Rust stable - uses: dtolnay/rust-toolchain@stable + # 缓存 Turbo + - name: Cache Turbo + uses: actions/cache@v4 + with: + path: .turbo + key: turbo-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ github.sha }} + restore-keys: | + turbo-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}- + turbo-${{ runner.os }}- - - name: Install wasm-pack - run: cargo install wasm-pack - - - name: Build core package first - run: pnpm run build:core - - - name: Build engine WASM package - run: | - cd packages/engine - pnpm run build + # 构建所有包 + - name: Build all packages + run: pnpm run build - name: Copy WASM files to ecs-engine-bindgen run: | @@ -64,30 +91,15 @@ jobs: cp packages/engine/pkg/es_engine_bg.wasm packages/ecs-engine-bindgen/src/wasm/ cp packages/engine/pkg/es_engine_bg.wasm.d.ts packages/ecs-engine-bindgen/src/wasm/ - - name: Build dependent packages for type declarations - run: | - cd packages/platform-common && pnpm run build - cd ../asset-system && pnpm run build - cd ../components && pnpm run build - cd ../editor-core && pnpm run build - cd ../ui && pnpm run build - cd ../editor-runtime && pnpm run build - cd ../behavior-tree && pnpm run build - cd ../tilemap && pnpm run build - cd ../physics-rapier2d && pnpm run build - cd ../node-editor && pnpm run build - cd ../blueprint && pnpm run build - - - name: Build ecs-engine-bindgen - run: | - cd packages/ecs-engine-bindgen && pnpm run build - + # 类型检查 - name: Type check run: pnpm run type-check + # Lint 检查 - name: Lint check run: pnpm run lint + # 测试 - name: Run tests with coverage run: pnpm run test:ci @@ -100,39 +112,16 @@ jobs: name: codecov-umbrella fail_ci_if_error: false - build: - runs-on: ubuntu-latest - needs: test - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Install pnpm - uses: pnpm/action-setup@v2 - with: - version: 8 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '20.x' - cache: 'pnpm' - - - name: Install dependencies - run: pnpm install - - - name: Build project - run: pnpm run build - - - name: Build npm package + # 构建 npm 包 + - name: Build npm packages run: pnpm run build:npm + # 上传构建产物 - name: Upload build artifacts uses: actions/upload-artifact@v4 with: name: build-artifacts path: | - bin/ - dist/ + packages/*/dist/ + packages/*/bin/ retention-days: 7 diff --git a/.github/workflows/release-editor.yml b/.github/workflows/release-editor.yml index 5129d771..560dca57 100644 --- a/.github/workflows/release-editor.yml +++ b/.github/workflows/release-editor.yml @@ -71,39 +71,13 @@ jobs: node -e "const pkg=require('./package.json'); pkg.version='${{ github.event.inputs.version }}'; require('fs').writeFileSync('./package.json', JSON.stringify(pkg, null, 2)+'\n')" node scripts/sync-version.js - # ===== 第一层:基础包(无依赖) ===== - - name: Build core package - run: pnpm run build:core - - - name: Build math package - run: | - cd packages/math - pnpm run build - - - name: Build platform-common package - run: | - cd packages/platform-common - pnpm run build - - # ===== 第二层:依赖 core 的包 ===== - - name: Build asset-system package - run: | - cd packages/asset-system - pnpm run build - - - name: Build components package - run: | - cd packages/components - pnpm run build - - # ===== 第三层:Rust WASM 引擎 ===== - name: Install wasm-pack run: cargo install wasm-pack - - name: Build engine package (Rust WASM) - run: | - cd packages/engine - pnpm run build + # 使用 Turborepo 自动按依赖顺序构建所有包 + # 这会自动处理:core -> asset-system -> editor-core -> ui -> 等等 + - name: Build all packages with Turborepo + run: pnpm run build - name: Copy WASM files to ecs-engine-bindgen shell: bash @@ -114,60 +88,6 @@ jobs: cp packages/engine/pkg/es_engine_bg.wasm packages/ecs-engine-bindgen/src/wasm/ cp packages/engine/pkg/es_engine_bg.wasm.d.ts packages/ecs-engine-bindgen/src/wasm/ - - name: Build ecs-engine-bindgen package - run: | - cd packages/ecs-engine-bindgen - pnpm run build - - # ===== 第四层:依赖 ecs-engine-bindgen/asset-system 的包 ===== - - name: Build editor-core package - run: | - cd packages/editor-core - pnpm run build - - # ===== 第五层:依赖 editor-core 的包 ===== - - name: Build UI package - run: | - cd packages/ui - pnpm run build - - - name: Build tilemap package - run: | - cd packages/tilemap - pnpm run build - - - name: Build editor-runtime package - run: | - cd packages/editor-runtime - pnpm run build - - - name: Build node-editor package - run: | - cd packages/node-editor - pnpm run build - - # ===== 第六层:依赖 editor-runtime 的包 ===== - - name: Build behavior-tree package - run: | - cd packages/behavior-tree - pnpm run build - - - name: Build physics-rapier2d package - run: | - cd packages/physics-rapier2d - pnpm run build - - - name: Build blueprint package - run: | - cd packages/blueprint - pnpm run build - - # ===== 第七层:平台包(依赖 ui, tilemap, behavior-tree, physics-rapier2d) ===== - - name: Build platform-web package - run: | - cd packages/platform-web - pnpm run build - - name: Bundle runtime files for Tauri run: | cd packages/editor-app @@ -220,16 +140,16 @@ jobs: delete-branch: true title: "chore(editor): Release v${{ github.event.inputs.version }}" body: | - ## 🚀 Release v${{ github.event.inputs.version }} + ## Release v${{ github.event.inputs.version }} This PR updates the editor version after successful release build. ### Changes - - ✅ Updated `packages/editor-app/package.json` → `${{ github.event.inputs.version }}` - - ✅ Updated `packages/editor-app/src-tauri/tauri.conf.json` → `${{ github.event.inputs.version }}` + - Updated `packages/editor-app/package.json` → `${{ github.event.inputs.version }}` + - Updated `packages/editor-app/src-tauri/tauri.conf.json` → `${{ github.event.inputs.version }}` ### Release - - 📦 [GitHub Release](https://github.com/${{ github.repository }}/releases/tag/editor-v${{ github.event.inputs.version }}) + - [GitHub Release](https://github.com/${{ github.repository }}/releases/tag/editor-v${{ github.event.inputs.version }}) --- *This PR was automatically created by the release workflow.* diff --git a/.gitignore b/.gitignore index ededeb18..bcc91689 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,9 @@ dist/ .cache/ .build-cache/ +# Turborepo +.turbo/ + # IDE 配置 .idea/ .vscode/ diff --git a/docs/.vitepress/config.mjs b/docs/.vitepress/config.mjs index ae01353d..be45a583 100644 --- a/docs/.vitepress/config.mjs +++ b/docs/.vitepress/config.mjs @@ -65,6 +65,7 @@ export default defineConfig({ collapsed: false, items: [ { text: '实体类 (Entity)', link: '/guide/entity' }, + { text: '层级系统 (Hierarchy)', link: '/guide/hierarchy' }, { text: '组件系统 (Component)', link: '/guide/component' }, { text: '实体查询系统', link: '/guide/entity-query' }, { diff --git a/docs/guide/entity.md b/docs/guide/entity.md index d0a2a1aa..158bf3c7 100644 --- a/docs/guide/entity.md +++ b/docs/guide/entity.md @@ -9,6 +9,12 @@ - 提供唯一标识(ID) - 管理组件的生命周期 +::: tip 关于父子层级关系 +实体间的父子层级关系通过 `HierarchyComponent` 和 `HierarchySystem` 管理,而非 Entity 内置属性。这种设计遵循 ECS 组合原则 —— 只有需要层级关系的实体才添加此组件。 + +详见 [层级系统](./hierarchy.md) 文档。 +::: + ## 创建实体 **重要提示:实体必须通过场景创建,不支持手动创建!** @@ -285,4 +291,10 @@ entity.components.forEach(component => { }); ``` -实体是 ECS 架构的核心概念之一,理解如何正确使用实体将帮助你构建高效、可维护的游戏代码。 \ No newline at end of file +实体是 ECS 架构的核心概念之一,理解如何正确使用实体将帮助你构建高效、可维护的游戏代码。 + +## 下一步 + +- 了解 [层级系统](./hierarchy.md) 建立实体间的父子关系 +- 了解 [组件系统](./component.md) 为实体添加功能 +- 了解 [场景管理](./scene.md) 组织和管理实体 \ No newline at end of file diff --git a/docs/guide/hierarchy.md b/docs/guide/hierarchy.md new file mode 100644 index 00000000..4c49f704 --- /dev/null +++ b/docs/guide/hierarchy.md @@ -0,0 +1,437 @@ +# 层级系统 + +在游戏开发中,实体间的父子层级关系是常见需求。ECS Framework 采用组件化方式管理层级关系,通过 `HierarchyComponent` 和 `HierarchySystem` 实现,完全遵循 ECS 组合原则。 + +## 设计理念 + +### 为什么不在 Entity 中内置层级? + +传统的游戏对象模型(如 Unity 的 GameObject)将层级关系内置于实体中。ECS Framework 选择组件化方案的原因: + +1. **ECS 组合原则**:层级是一种"功能",应该通过组件添加,而非所有实体都具备 +2. **按需使用**:只有需要层级关系的实体才添加 `HierarchyComponent` +3. **数据与逻辑分离**:`HierarchyComponent` 存储数据,`HierarchySystem` 处理逻辑 +4. **序列化友好**:层级关系作为组件数据可以轻松序列化和反序列化 + +## 基本概念 + +### HierarchyComponent + +存储层级关系数据的组件: + +```typescript +import { HierarchyComponent } from '@esengine/ecs-framework'; + +// HierarchyComponent 的核心属性 +interface HierarchyComponent { + parentId: number | null; // 父实体 ID,null 表示根实体 + childIds: number[]; // 子实体 ID 列表 + depth: number; // 在层级中的深度(由系统维护) + bActiveInHierarchy: boolean; // 在层级中是否激活(由系统维护) +} +``` + +### HierarchySystem + +处理层级逻辑的系统,提供所有层级操作的 API: + +```typescript +import { HierarchySystem } from '@esengine/ecs-framework'; + +// 获取系统 +const hierarchySystem = scene.getEntityProcessor(HierarchySystem); +``` + +## 快速开始 + +### 添加系统到场景 + +```typescript +import { Scene, HierarchySystem } from '@esengine/ecs-framework'; + +class GameScene extends Scene { + protected initialize(): void { + // 添加层级系统 + this.addSystem(new HierarchySystem()); + + // 添加其他系统... + } +} +``` + +### 建立父子关系 + +```typescript +// 创建实体 +const parent = scene.createEntity("Parent"); +const child1 = scene.createEntity("Child1"); +const child2 = scene.createEntity("Child2"); + +// 获取层级系统 +const hierarchySystem = scene.getEntityProcessor(HierarchySystem); + +// 设置父子关系(自动添加 HierarchyComponent) +hierarchySystem.setParent(child1, parent); +hierarchySystem.setParent(child2, parent); + +// 现在 parent 有两个子实体 +``` + +### 查询层级 + +```typescript +// 获取父实体 +const parentEntity = hierarchySystem.getParent(child1); + +// 获取所有子实体 +const children = hierarchySystem.getChildren(parent); + +// 获取子实体数量 +const count = hierarchySystem.getChildCount(parent); + +// 检查是否有子实体 +const hasKids = hierarchySystem.hasChildren(parent); + +// 获取在层级中的深度 +const depth = hierarchySystem.getDepth(child1); // 返回 1 +``` + +## API 参考 + +### 父子关系操作 + +#### setParent + +设置实体的父级: + +```typescript +// 设置父级 +hierarchySystem.setParent(child, parent); + +// 移动到根级(无父级) +hierarchySystem.setParent(child, null); +``` + +#### insertChildAt + +在指定位置插入子实体: + +```typescript +// 在第一个位置插入 +hierarchySystem.insertChildAt(parent, child, 0); + +// 追加到末尾 +hierarchySystem.insertChildAt(parent, child, -1); +``` + +#### removeChild + +从父级移除子实体(子实体变为根级): + +```typescript +const success = hierarchySystem.removeChild(parent, child); +``` + +#### removeAllChildren + +移除所有子实体: + +```typescript +hierarchySystem.removeAllChildren(parent); +``` + +### 层级查询 + +#### getParent / getChildren + +```typescript +const parent = hierarchySystem.getParent(entity); +const children = hierarchySystem.getChildren(entity); +``` + +#### getRoot + +获取实体的根节点: + +```typescript +const root = hierarchySystem.getRoot(deepChild); +``` + +#### getRootEntities + +获取所有根实体(没有父级的实体): + +```typescript +const roots = hierarchySystem.getRootEntities(); +``` + +#### isAncestorOf / isDescendantOf + +检查祖先/后代关系: + +```typescript +// grandparent -> parent -> child +const isAncestor = hierarchySystem.isAncestorOf(grandparent, child); // true +const isDescendant = hierarchySystem.isDescendantOf(child, grandparent); // true +``` + +### 层级遍历 + +#### findChild + +根据名称查找子实体: + +```typescript +// 直接子级中查找 +const child = hierarchySystem.findChild(parent, "ChildName"); + +// 递归查找所有后代 +const deepChild = hierarchySystem.findChild(parent, "DeepChild", true); +``` + +#### findChildrenByTag + +根据标签查找子实体: + +```typescript +// 查找直接子级 +const tagged = hierarchySystem.findChildrenByTag(parent, TAG_ENEMY); + +// 递归查找 +const allTagged = hierarchySystem.findChildrenByTag(parent, TAG_ENEMY, true); +``` + +#### forEachChild + +遍历子实体: + +```typescript +// 遍历直接子级 +hierarchySystem.forEachChild(parent, (child) => { + console.log(child.name); +}); + +// 递归遍历所有后代 +hierarchySystem.forEachChild(parent, (child) => { + console.log(child.name); +}, true); +``` + +### 层级状态 + +#### isActiveInHierarchy + +检查实体在层级中是否激活(考虑所有祖先的激活状态): + +```typescript +// 如果 parent.active = false,即使 child.active = true +// isActiveInHierarchy(child) 也会返回 false +const activeInHierarchy = hierarchySystem.isActiveInHierarchy(child); +``` + +#### getDepth + +获取实体在层级中的深度(根实体深度为 0): + +```typescript +const depth = hierarchySystem.getDepth(entity); +``` + +### 扁平化层级(用于 UI 渲染) + +```typescript +// 用于实现可展开/折叠的层级树视图 +const expandedIds = new Set([parent.id]); + +const flatNodes = hierarchySystem.flattenHierarchy(expandedIds); +// 返回 [{ entity, depth, bHasChildren, bIsExpanded }, ...] +``` + +## 完整示例 + +### 创建游戏角色层级 + +```typescript +import { + Scene, + HierarchySystem, + HierarchyComponent +} from '@esengine/ecs-framework'; + +class GameScene extends Scene { + private hierarchySystem!: HierarchySystem; + + protected initialize(): void { + // 添加层级系统 + this.hierarchySystem = new HierarchySystem(); + this.addSystem(this.hierarchySystem); + + // 创建角色层级 + this.createPlayerHierarchy(); + } + + private createPlayerHierarchy(): void { + // 根实体 + const player = this.createEntity("Player"); + player.addComponent(new Transform(0, 0)); + + // 身体部件 + const body = this.createEntity("Body"); + body.addComponent(new Sprite("body.png")); + this.hierarchySystem.setParent(body, player); + + // 武器(挂载在身体上) + const weapon = this.createEntity("Weapon"); + weapon.addComponent(new Sprite("sword.png")); + this.hierarchySystem.setParent(weapon, body); + + // 特效(挂载在武器上) + const effect = this.createEntity("WeaponEffect"); + effect.addComponent(new ParticleEmitter()); + this.hierarchySystem.setParent(effect, weapon); + + // 查询层级信息 + console.log(`Player 层级深度: ${this.hierarchySystem.getDepth(player)}`); // 0 + console.log(`Weapon 层级深度: ${this.hierarchySystem.getDepth(weapon)}`); // 2 + console.log(`Effect 层级深度: ${this.hierarchySystem.getDepth(effect)}`); // 3 + } + + public equipNewWeapon(weaponName: string): void { + const body = this.findEntity("Body"); + const oldWeapon = this.hierarchySystem.findChild(body!, "Weapon"); + + if (oldWeapon) { + // 移除旧武器的所有子实体 + this.hierarchySystem.removeAllChildren(oldWeapon); + oldWeapon.destroy(); + } + + // 创建新武器 + const newWeapon = this.createEntity("Weapon"); + newWeapon.addComponent(new Sprite(`${weaponName}.png`)); + this.hierarchySystem.setParent(newWeapon, body!); + } +} +``` + +### 层级变换系统 + +结合 Transform 组件实现层级变换: + +```typescript +import { EntitySystem, Matcher, HierarchySystem, HierarchyComponent } from '@esengine/ecs-framework'; + +class HierarchyTransformSystem extends EntitySystem { + private hierarchySystem!: HierarchySystem; + + constructor() { + super(Matcher.empty().all(Transform, HierarchyComponent)); + } + + public onAddedToScene(): void { + // 获取层级系统引用 + this.hierarchySystem = this.scene!.getEntityProcessor(HierarchySystem)!; + } + + protected process(entities: readonly Entity[]): void { + // 按深度排序,确保父级先更新 + const sorted = [...entities].sort((a, b) => { + return this.hierarchySystem.getDepth(a) - this.hierarchySystem.getDepth(b); + }); + + for (const entity of sorted) { + const transform = entity.getComponent(Transform)!; + const parent = this.hierarchySystem.getParent(entity); + + if (parent) { + const parentTransform = parent.getComponent(Transform); + if (parentTransform) { + // 计算世界坐标 + transform.worldX = parentTransform.worldX + transform.localX; + transform.worldY = parentTransform.worldY + transform.localY; + } + } else { + // 根实体,本地坐标即世界坐标 + transform.worldX = transform.localX; + transform.worldY = transform.localY; + } + } + } +} +``` + +## 性能优化 + +### 缓存机制 + +`HierarchySystem` 内置了缓存机制: + +- `depth` 和 `bActiveInHierarchy` 由系统自动维护 +- 使用 `bCacheDirty` 标记优化更新 +- 层级变化时自动标记所有子级缓存为脏 + +### 最佳实践 + +1. **避免深层嵌套**:系统限制最大深度为 32 层 +2. **批量操作**:构建复杂层级时,尽量一次性设置好所有父子关系 +3. **按需添加**:只有真正需要层级关系的实体才添加 `HierarchyComponent` +4. **缓存系统引用**:避免每次调用都获取 `HierarchySystem` + +```typescript +// 好的做法 +class MySystem extends EntitySystem { + private hierarchySystem!: HierarchySystem; + + onAddedToScene() { + this.hierarchySystem = this.scene!.getEntityProcessor(HierarchySystem)!; + } + + process() { + // 使用缓存的引用 + const parent = this.hierarchySystem.getParent(entity); + } +} + +// 避免的做法 +process() { + // 每次都获取,性能较差 + const system = this.scene!.getEntityProcessor(HierarchySystem); +} +``` + +## 迁移指南 + +如果你之前使用的是旧版 Entity 内置的层级 API,请参考以下迁移指南: + +| 旧 API (已移除) | 新 API | +|----------------|--------| +| `entity.parent` | `hierarchySystem.getParent(entity)` | +| `entity.children` | `hierarchySystem.getChildren(entity)` | +| `entity.addChild(child)` | `hierarchySystem.setParent(child, entity)` | +| `entity.removeChild(child)` | `hierarchySystem.removeChild(entity, child)` | +| `entity.findChild(name)` | `hierarchySystem.findChild(entity, name)` | +| `entity.activeInHierarchy` | `hierarchySystem.isActiveInHierarchy(entity)` | + +### 迁移示例 + +```typescript +// 旧代码 +const parent = scene.createEntity("Parent"); +const child = scene.createEntity("Child"); +parent.addChild(child); +const found = parent.findChild("Child"); + +// 新代码 +const hierarchySystem = scene.getEntityProcessor(HierarchySystem); + +const parent = scene.createEntity("Parent"); +const child = scene.createEntity("Child"); +hierarchySystem.setParent(child, parent); +const found = hierarchySystem.findChild(parent, "Child"); +``` + +## 下一步 + +- 了解 [实体类](./entity.md) 的其他功能 +- 了解 [场景管理](./scene.md) 如何组织实体和系统 +- 探索 [变换系统](./transform.md) 实现空间层级变换 diff --git a/package.json b/package.json index e9096b62..02d88374 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "version": "2.1.29", "description": "ECS Framework Monorepo - 高性能ECS框架及其网络插件", "private": true, + "packageManager": "pnpm@10.22.0", "workspaces": [ "packages/*" ], @@ -17,16 +18,18 @@ ], "scripts": { "bootstrap": "lerna bootstrap", - "clean": "lerna run clean", - "build": "npm run build:core && npm run build:math", - "build:core": "cd packages/core && npm run build", - "build:math": "cd packages/math && npm run build", - "build:npm": "npm run build:npm:core && npm run build:npm:math", + "clean": "turbo run clean", + "build": "turbo run build", + "build:filter": "turbo run build --filter", + "build:core": "turbo run build --filter=@esengine/ecs-framework", + "build:math": "turbo run build --filter=@esengine/ecs-framework-math", + "build:editor": "turbo run build --filter=@esengine/editor-app...", + "build:npm": "turbo run build:npm", "build:npm:core": "cd packages/core && npm run build:npm", "build:npm:math": "cd packages/math && npm run build:npm", - "test": "lerna run test", - "test:coverage": "lerna run test:coverage", - "test:ci": "lerna run test:ci", + "test": "turbo run test", + "test:coverage": "turbo run test:coverage", + "test:ci": "turbo run test:ci", "prepare:publish": "npm run build:npm && node scripts/pre-publish-check.cjs", "sync:versions": "node scripts/sync-versions.cjs", "publish:all": "npm run prepare:publish && npm run publish:all:dist", @@ -51,9 +54,9 @@ "copy:worker-demo": "node scripts/update-worker-demo.js", "format": "prettier --write \"packages/**/src/**/*.{ts,tsx,js,jsx}\"", "format:check": "prettier --check \"packages/**/src/**/*.{ts,tsx,js,jsx}\"", - "type-check": "lerna run type-check", - "lint": "eslint \"packages/**/src/**/*.{ts,tsx,js,jsx}\"", - "lint:fix": "eslint \"packages/**/src/**/*.{ts,tsx,js,jsx}\" --fix", + "type-check": "turbo run type-check", + "lint": "turbo run lint", + "lint:fix": "turbo run lint:fix", "build:wasm": "cd packages/engine && wasm-pack build --dev --out-dir pkg", "build:wasm:release": "cd packages/engine && wasm-pack build --release --out-dir pkg" }, @@ -92,6 +95,7 @@ "semver": "^7.6.3", "size-limit": "^11.0.2", "ts-jest": "^29.4.0", + "turbo": "^2.6.1", "typedoc": "^0.28.13", "typedoc-plugin-markdown": "^4.9.0", "typescript": "^5.8.3", diff --git a/packages/asset-system/package.json b/packages/asset-system/package.json index 84c1c8a1..60a22882 100644 --- a/packages/asset-system/package.json +++ b/packages/asset-system/package.json @@ -2,24 +2,24 @@ "name": "@esengine/asset-system", "version": "1.0.0", "description": "Asset management system for ES Engine", + "type": "module", "main": "dist/index.js", - "module": "dist/index.mjs", + "module": "dist/index.js", "types": "dist/index.d.ts", "exports": { ".": { - "import": "./dist/index.mjs", - "require": "./dist/index.js", - "types": "./dist/index.d.ts" + "types": "./dist/index.d.ts", + "import": "./dist/index.js" } }, "files": [ "dist" ], "scripts": { - "build": "rollup -c", - "build:npm": "npm run build", + "build": "tsup", + "build:watch": "tsup --watch", "clean": "rimraf dist", - "type-check": "npx tsc --noEmit" + "type-check": "tsc --noEmit" }, "keywords": [ "ecs", @@ -29,16 +29,11 @@ ], "author": "yhh", "license": "MIT", - "peerDependencies": { - "@esengine/ecs-framework": "^2.0.0" - }, "devDependencies": { - "@rollup/plugin-commonjs": "^28.0.3", - "@rollup/plugin-node-resolve": "^16.0.1", - "@rollup/plugin-typescript": "^11.1.6", + "@esengine/ecs-framework": "workspace:*", + "@esengine/build-config": "workspace:*", "rimraf": "^5.0.0", - "rollup": "^4.42.0", - "rollup-plugin-dts": "^6.2.1", + "tsup": "^8.0.0", "typescript": "^5.8.3" }, "publishConfig": { diff --git a/packages/asset-system/rollup.config.js b/packages/asset-system/rollup.config.js deleted file mode 100644 index d512c7db..00000000 --- a/packages/asset-system/rollup.config.js +++ /dev/null @@ -1,49 +0,0 @@ -import typescript from '@rollup/plugin-typescript'; -import resolve from '@rollup/plugin-node-resolve'; -import commonjs from '@rollup/plugin-commonjs'; -import dts from 'rollup-plugin-dts'; - -const external = ['@esengine/ecs-framework']; - -export default [ - // ESM and CJS builds - { - input: 'src/index.ts', - output: [ - { - file: 'dist/index.js', - format: 'cjs', - sourcemap: true, - exports: 'named' - }, - { - file: 'dist/index.mjs', - format: 'es', - sourcemap: true - } - ], - external, - plugins: [ - resolve({ - preferBuiltins: false, - browser: true - }), - commonjs(), - typescript({ - tsconfig: './tsconfig.json', - declaration: false, - declarationMap: false - }) - ] - }, - // Type definitions - { - input: 'src/index.ts', - output: { - file: 'dist/index.d.ts', - format: 'es' - }, - external, - plugins: [dts()] - } -]; \ No newline at end of file diff --git a/packages/asset-system/tsconfig.build.json b/packages/asset-system/tsconfig.build.json new file mode 100644 index 00000000..bf8abf7b --- /dev/null +++ b/packages/asset-system/tsconfig.build.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ES2020", + "moduleResolution": "bundler", + "lib": ["ES2020", "DOM"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "declaration": true, + "declarationMap": true, + "resolveJsonModule": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/asset-system/tsup.config.ts b/packages/asset-system/tsup.config.ts new file mode 100644 index 00000000..f704a430 --- /dev/null +++ b/packages/asset-system/tsup.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'tsup'; +import { runtimeOnlyPreset } from '../build-config/src/presets/plugin-tsup'; + +export default defineConfig({ + ...runtimeOnlyPreset(), + tsconfig: 'tsconfig.build.json' +}); diff --git a/packages/audio/package.json b/packages/audio/package.json new file mode 100644 index 00000000..843015f2 --- /dev/null +++ b/packages/audio/package.json @@ -0,0 +1,46 @@ +{ + "name": "@esengine/audio", + "version": "1.0.0", + "description": "ECS-based audio system", + "esengine": { + "plugin": true, + "pluginExport": "AudioPlugin", + "category": "audio", + "isEnginePlugin": true + }, + "main": "dist/index.js", + "module": "dist/index.js", + "types": "dist/index.d.ts", + "type": "module", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "files": [ + "dist" + ], + "scripts": { + "build": "tsup", + "build:watch": "tsup --watch", + "type-check": "tsc --noEmit", + "clean": "rimraf dist" + }, + "devDependencies": { + "@esengine/ecs-framework": "workspace:*", + "@esengine/engine-core": "workspace:*", + "@esengine/build-config": "workspace:*", + "rimraf": "^5.0.5", + "tsup": "^8.0.0", + "typescript": "^5.3.3" + }, + "keywords": [ + "ecs", + "audio", + "sound", + "music" + ], + "author": "yhh", + "license": "MIT" +} diff --git a/packages/audio/src/AudioPlugin.ts b/packages/audio/src/AudioPlugin.ts new file mode 100644 index 00000000..4e7ccb2e --- /dev/null +++ b/packages/audio/src/AudioPlugin.ts @@ -0,0 +1,24 @@ +import type { ComponentRegistry as ComponentRegistryType } from '@esengine/ecs-framework'; +import type { IRuntimeModule, IPlugin, PluginDescriptor } from '@esengine/engine-core'; +import { AudioSourceComponent } from './AudioSourceComponent'; + +class AudioRuntimeModule implements IRuntimeModule { + registerComponents(registry: typeof ComponentRegistryType): void { + registry.register(AudioSourceComponent); + } +} + +const descriptor: PluginDescriptor = { + id: '@esengine/audio', + name: 'Audio', + version: '1.0.0', + description: '音频组件', + category: 'audio', + enabledByDefault: true, + isEnginePlugin: true +}; + +export const AudioPlugin: IPlugin = { + descriptor, + runtimeModule: new AudioRuntimeModule() +}; diff --git a/packages/audio/src/AudioSourceComponent.ts b/packages/audio/src/AudioSourceComponent.ts new file mode 100644 index 00000000..b78656eb --- /dev/null +++ b/packages/audio/src/AudioSourceComponent.ts @@ -0,0 +1,43 @@ +import { Component, ECSComponent, Serializable, Serialize, Property } from '@esengine/ecs-framework'; + +@ECSComponent('AudioSource') +@Serializable({ version: 1, typeId: 'AudioSource' }) +export class AudioSourceComponent extends Component { + @Serialize() + @Property({ type: 'asset', label: 'Audio Clip', assetType: 'audio' }) + clip: string = ''; + + /** 范围 [0, 1] */ + @Serialize() + @Property({ type: 'number', label: 'Volume', min: 0, max: 1, step: 0.01 }) + volume: number = 1; + + @Serialize() + @Property({ type: 'number', label: 'Pitch', min: 0.1, max: 3, step: 0.1 }) + pitch: number = 1; + + @Serialize() + @Property({ type: 'boolean', label: 'Loop' }) + loop: boolean = false; + + @Serialize() + @Property({ type: 'boolean', label: 'Play On Awake' }) + playOnAwake: boolean = false; + + @Serialize() + @Property({ type: 'boolean', label: 'Mute' }) + mute: boolean = false; + + /** 0 = 2D, 1 = 3D */ + @Serialize() + @Property({ type: 'number', label: 'Spatial Blend', min: 0, max: 1, step: 0.1 }) + spatialBlend: number = 0; + + @Serialize() + @Property({ type: 'number', label: 'Min Distance' }) + minDistance: number = 1; + + @Serialize() + @Property({ type: 'number', label: 'Max Distance' }) + maxDistance: number = 500; +} diff --git a/packages/audio/src/index.ts b/packages/audio/src/index.ts new file mode 100644 index 00000000..66bf81d0 --- /dev/null +++ b/packages/audio/src/index.ts @@ -0,0 +1,2 @@ +export { AudioSourceComponent } from './AudioSourceComponent'; +export { AudioPlugin } from './AudioPlugin'; diff --git a/packages/audio/tsconfig.build.json b/packages/audio/tsconfig.build.json new file mode 100644 index 00000000..f39a0594 --- /dev/null +++ b/packages/audio/tsconfig.build.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "composite": false, + "declaration": true, + "declarationMap": true, + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "**/*.test.ts"] +} diff --git a/packages/audio/tsconfig.json b/packages/audio/tsconfig.json new file mode 100644 index 00000000..02f5f187 --- /dev/null +++ b/packages/audio/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"], + "references": [ + { "path": "../core" } + ] +} diff --git a/packages/audio/tsup.config.ts b/packages/audio/tsup.config.ts new file mode 100644 index 00000000..f704a430 --- /dev/null +++ b/packages/audio/tsup.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'tsup'; +import { runtimeOnlyPreset } from '../build-config/src/presets/plugin-tsup'; + +export default defineConfig({ + ...runtimeOnlyPreset(), + tsconfig: 'tsconfig.build.json' +}); diff --git a/packages/behavior-tree-editor/package.json b/packages/behavior-tree-editor/package.json new file mode 100644 index 00000000..1ae5139e --- /dev/null +++ b/packages/behavior-tree-editor/package.json @@ -0,0 +1,49 @@ +{ + "name": "@esengine/behavior-tree-editor", + "version": "1.0.0", + "description": "Editor support for @esengine/behavior-tree - visual editor, inspectors, and tools", + "main": "dist/index.js", + "module": "dist/index.js", + "types": "dist/index.d.ts", + "type": "module", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "files": [ + "dist" + ], + "scripts": { + "build": "tsup", + "build:watch": "tsup --watch", + "type-check": "tsc --noEmit", + "clean": "rimraf dist" + }, + "dependencies": { + "@esengine/behavior-tree": "workspace:*" + }, + "devDependencies": { + "@esengine/ecs-framework": "workspace:*", + "@esengine/engine-core": "workspace:*", + "@esengine/editor-core": "workspace:*", + "@esengine/editor-runtime": "workspace:*", + "@esengine/node-editor": "workspace:*", + "@esengine/build-config": "workspace:*", + "lucide-react": "^0.545.0", + "react": "^18.3.1", + "zustand": "^5.0.8", + "@types/react": "^18.3.12", + "rimraf": "^5.0.5", + "tsup": "^8.0.0", + "typescript": "^5.3.3" + }, + "keywords": [ + "ecs", + "behavior-tree", + "editor" + ], + "author": "", + "license": "MIT" +} diff --git a/packages/behavior-tree/src/editor/BehaviorTreePlugin.ts b/packages/behavior-tree-editor/src/BehaviorTreePlugin.ts similarity index 78% rename from packages/behavior-tree/src/editor/BehaviorTreePlugin.ts rename to packages/behavior-tree-editor/src/BehaviorTreePlugin.ts index eedbd84b..b5cdca6f 100644 --- a/packages/behavior-tree/src/editor/BehaviorTreePlugin.ts +++ b/packages/behavior-tree-editor/src/BehaviorTreePlugin.ts @@ -21,18 +21,16 @@ export const descriptor: PluginDescriptor = { { name: 'BehaviorTreeRuntime', type: 'runtime', - loadingPhase: 'default', - entry: './src/index.ts' + loadingPhase: 'default' }, { name: 'BehaviorTreeEditor', type: 'editor', - loadingPhase: 'default', - entry: './src/editor/index.ts' + loadingPhase: 'default' } ], dependencies: [ - { id: '@esengine/core', version: '>=1.0.0' } + { id: '@esengine/engine-core', version: '>=1.0.0', optional: true } ], icon: 'GitBranch' }; diff --git a/packages/behavior-tree/src/editor/PluginContext.ts b/packages/behavior-tree-editor/src/PluginContext.ts similarity index 100% rename from packages/behavior-tree/src/editor/PluginContext.ts rename to packages/behavior-tree-editor/src/PluginContext.ts diff --git a/packages/behavior-tree/src/editor/application/commands/CommandManager.ts b/packages/behavior-tree-editor/src/application/commands/CommandManager.ts similarity index 100% rename from packages/behavior-tree/src/editor/application/commands/CommandManager.ts rename to packages/behavior-tree-editor/src/application/commands/CommandManager.ts diff --git a/packages/behavior-tree/src/editor/application/commands/ICommand.ts b/packages/behavior-tree-editor/src/application/commands/ICommand.ts similarity index 100% rename from packages/behavior-tree/src/editor/application/commands/ICommand.ts rename to packages/behavior-tree-editor/src/application/commands/ICommand.ts diff --git a/packages/behavior-tree/src/editor/application/commands/ITreeState.ts b/packages/behavior-tree-editor/src/application/commands/ITreeState.ts similarity index 100% rename from packages/behavior-tree/src/editor/application/commands/ITreeState.ts rename to packages/behavior-tree-editor/src/application/commands/ITreeState.ts diff --git a/packages/behavior-tree/src/editor/application/commands/tree/AddConnectionCommand.ts b/packages/behavior-tree-editor/src/application/commands/tree/AddConnectionCommand.ts similarity index 100% rename from packages/behavior-tree/src/editor/application/commands/tree/AddConnectionCommand.ts rename to packages/behavior-tree-editor/src/application/commands/tree/AddConnectionCommand.ts diff --git a/packages/behavior-tree/src/editor/application/commands/tree/CreateNodeCommand.ts b/packages/behavior-tree-editor/src/application/commands/tree/CreateNodeCommand.ts similarity index 100% rename from packages/behavior-tree/src/editor/application/commands/tree/CreateNodeCommand.ts rename to packages/behavior-tree-editor/src/application/commands/tree/CreateNodeCommand.ts diff --git a/packages/behavior-tree/src/editor/application/commands/tree/DeleteNodeCommand.ts b/packages/behavior-tree-editor/src/application/commands/tree/DeleteNodeCommand.ts similarity index 100% rename from packages/behavior-tree/src/editor/application/commands/tree/DeleteNodeCommand.ts rename to packages/behavior-tree-editor/src/application/commands/tree/DeleteNodeCommand.ts diff --git a/packages/behavior-tree/src/editor/application/commands/tree/MoveNodeCommand.ts b/packages/behavior-tree-editor/src/application/commands/tree/MoveNodeCommand.ts similarity index 100% rename from packages/behavior-tree/src/editor/application/commands/tree/MoveNodeCommand.ts rename to packages/behavior-tree-editor/src/application/commands/tree/MoveNodeCommand.ts diff --git a/packages/behavior-tree/src/editor/application/commands/tree/RemoveConnectionCommand.ts b/packages/behavior-tree-editor/src/application/commands/tree/RemoveConnectionCommand.ts similarity index 100% rename from packages/behavior-tree/src/editor/application/commands/tree/RemoveConnectionCommand.ts rename to packages/behavior-tree-editor/src/application/commands/tree/RemoveConnectionCommand.ts diff --git a/packages/behavior-tree/src/editor/application/commands/tree/UpdateNodeDataCommand.ts b/packages/behavior-tree-editor/src/application/commands/tree/UpdateNodeDataCommand.ts similarity index 100% rename from packages/behavior-tree/src/editor/application/commands/tree/UpdateNodeDataCommand.ts rename to packages/behavior-tree-editor/src/application/commands/tree/UpdateNodeDataCommand.ts diff --git a/packages/behavior-tree/src/editor/application/commands/tree/index.ts b/packages/behavior-tree-editor/src/application/commands/tree/index.ts similarity index 100% rename from packages/behavior-tree/src/editor/application/commands/tree/index.ts rename to packages/behavior-tree-editor/src/application/commands/tree/index.ts diff --git a/packages/behavior-tree/src/editor/application/interfaces/IExecutionHooks.ts b/packages/behavior-tree-editor/src/application/interfaces/IExecutionHooks.ts similarity index 100% rename from packages/behavior-tree/src/editor/application/interfaces/IExecutionHooks.ts rename to packages/behavior-tree-editor/src/application/interfaces/IExecutionHooks.ts diff --git a/packages/behavior-tree/src/editor/application/services/BlackboardManager.ts b/packages/behavior-tree-editor/src/application/services/BlackboardManager.ts similarity index 100% rename from packages/behavior-tree/src/editor/application/services/BlackboardManager.ts rename to packages/behavior-tree-editor/src/application/services/BlackboardManager.ts diff --git a/packages/behavior-tree/src/editor/application/services/ExecutionController.ts b/packages/behavior-tree-editor/src/application/services/ExecutionController.ts similarity index 100% rename from packages/behavior-tree/src/editor/application/services/ExecutionController.ts rename to packages/behavior-tree-editor/src/application/services/ExecutionController.ts diff --git a/packages/behavior-tree/src/editor/application/services/GlobalBlackboardService.ts b/packages/behavior-tree-editor/src/application/services/GlobalBlackboardService.ts similarity index 98% rename from packages/behavior-tree/src/editor/application/services/GlobalBlackboardService.ts rename to packages/behavior-tree-editor/src/application/services/GlobalBlackboardService.ts index b291a0e2..4a5108f3 100644 --- a/packages/behavior-tree/src/editor/application/services/GlobalBlackboardService.ts +++ b/packages/behavior-tree-editor/src/application/services/GlobalBlackboardService.ts @@ -1,4 +1,4 @@ -import { GlobalBlackboardConfig, BlackboardValueType, BlackboardVariable } from '../../..'; +import { type GlobalBlackboardConfig, BlackboardValueType, type BlackboardVariable } from '@esengine/behavior-tree'; import { createLogger } from '@esengine/ecs-framework'; const logger = createLogger('GlobalBlackboardService'); diff --git a/packages/behavior-tree/src/editor/application/state/BehaviorTreeDataStore.ts b/packages/behavior-tree-editor/src/application/state/BehaviorTreeDataStore.ts similarity index 99% rename from packages/behavior-tree/src/editor/application/state/BehaviorTreeDataStore.ts rename to packages/behavior-tree-editor/src/application/state/BehaviorTreeDataStore.ts index cdee1330..0d472621 100644 --- a/packages/behavior-tree/src/editor/application/state/BehaviorTreeDataStore.ts +++ b/packages/behavior-tree-editor/src/application/state/BehaviorTreeDataStore.ts @@ -1,7 +1,7 @@ import { createStore } from '@esengine/editor-runtime'; const create = createStore; -import { NodeTemplates, NodeTemplate } from '../../..'; +import { NodeTemplates, type NodeTemplate } from '@esengine/behavior-tree'; import { BehaviorTree } from '../../domain/models/BehaviorTree'; import { Node } from '../../domain/models/Node'; import { Connection, ConnectionType } from '../../domain/models/Connection'; diff --git a/packages/behavior-tree/src/editor/application/use-cases/AddConnectionUseCase.ts b/packages/behavior-tree-editor/src/application/use-cases/AddConnectionUseCase.ts similarity index 100% rename from packages/behavior-tree/src/editor/application/use-cases/AddConnectionUseCase.ts rename to packages/behavior-tree-editor/src/application/use-cases/AddConnectionUseCase.ts diff --git a/packages/behavior-tree/src/editor/application/use-cases/CreateNodeUseCase.ts b/packages/behavior-tree-editor/src/application/use-cases/CreateNodeUseCase.ts similarity index 95% rename from packages/behavior-tree/src/editor/application/use-cases/CreateNodeUseCase.ts rename to packages/behavior-tree-editor/src/application/use-cases/CreateNodeUseCase.ts index 1e4e4540..86faaeeb 100644 --- a/packages/behavior-tree/src/editor/application/use-cases/CreateNodeUseCase.ts +++ b/packages/behavior-tree-editor/src/application/use-cases/CreateNodeUseCase.ts @@ -1,4 +1,4 @@ -import { NodeTemplate } from '../../..'; +import type { NodeTemplate } from '@esengine/behavior-tree'; import { Node } from '../../domain/models/Node'; import { Position } from '../../domain/value-objects/Position'; import { INodeFactory } from '../../domain/interfaces/INodeFactory'; diff --git a/packages/behavior-tree/src/editor/application/use-cases/DeleteNodeUseCase.ts b/packages/behavior-tree-editor/src/application/use-cases/DeleteNodeUseCase.ts similarity index 100% rename from packages/behavior-tree/src/editor/application/use-cases/DeleteNodeUseCase.ts rename to packages/behavior-tree-editor/src/application/use-cases/DeleteNodeUseCase.ts diff --git a/packages/behavior-tree/src/editor/application/use-cases/MoveNodeUseCase.ts b/packages/behavior-tree-editor/src/application/use-cases/MoveNodeUseCase.ts similarity index 100% rename from packages/behavior-tree/src/editor/application/use-cases/MoveNodeUseCase.ts rename to packages/behavior-tree-editor/src/application/use-cases/MoveNodeUseCase.ts diff --git a/packages/behavior-tree/src/editor/application/use-cases/RemoveConnectionUseCase.ts b/packages/behavior-tree-editor/src/application/use-cases/RemoveConnectionUseCase.ts similarity index 100% rename from packages/behavior-tree/src/editor/application/use-cases/RemoveConnectionUseCase.ts rename to packages/behavior-tree-editor/src/application/use-cases/RemoveConnectionUseCase.ts diff --git a/packages/behavior-tree/src/editor/application/use-cases/UpdateNodeDataUseCase.ts b/packages/behavior-tree-editor/src/application/use-cases/UpdateNodeDataUseCase.ts similarity index 100% rename from packages/behavior-tree/src/editor/application/use-cases/UpdateNodeDataUseCase.ts rename to packages/behavior-tree-editor/src/application/use-cases/UpdateNodeDataUseCase.ts diff --git a/packages/behavior-tree/src/editor/application/use-cases/ValidateTreeUseCase.ts b/packages/behavior-tree-editor/src/application/use-cases/ValidateTreeUseCase.ts similarity index 100% rename from packages/behavior-tree/src/editor/application/use-cases/ValidateTreeUseCase.ts rename to packages/behavior-tree-editor/src/application/use-cases/ValidateTreeUseCase.ts diff --git a/packages/behavior-tree/src/editor/application/use-cases/index.ts b/packages/behavior-tree-editor/src/application/use-cases/index.ts similarity index 100% rename from packages/behavior-tree/src/editor/application/use-cases/index.ts rename to packages/behavior-tree-editor/src/application/use-cases/index.ts diff --git a/packages/behavior-tree/src/editor/compiler/BehaviorTreeCompiler.tsx b/packages/behavior-tree-editor/src/compiler/BehaviorTreeCompiler.tsx similarity index 99% rename from packages/behavior-tree/src/editor/compiler/BehaviorTreeCompiler.tsx rename to packages/behavior-tree-editor/src/compiler/BehaviorTreeCompiler.tsx index 16ab5fb6..23cc78c9 100644 --- a/packages/behavior-tree/src/editor/compiler/BehaviorTreeCompiler.tsx +++ b/packages/behavior-tree-editor/src/compiler/BehaviorTreeCompiler.tsx @@ -10,7 +10,7 @@ import { createLogger, } from '@esengine/editor-runtime'; import { GlobalBlackboardTypeGenerator } from '../generators/GlobalBlackboardTypeGenerator'; -import { EditorFormatConverter, BehaviorTreeAssetSerializer } from '../..'; +import { EditorFormatConverter, BehaviorTreeAssetSerializer } from '@esengine/behavior-tree'; import { useBehaviorTreeDataStore } from '../application/state/BehaviorTreeDataStore'; const { File, FolderTree, FolderOpen } = Icons; diff --git a/packages/behavior-tree/src/editor/components/BehaviorTreeEditor.tsx b/packages/behavior-tree-editor/src/components/BehaviorTreeEditor.tsx similarity index 99% rename from packages/behavior-tree/src/editor/components/BehaviorTreeEditor.tsx rename to packages/behavior-tree-editor/src/components/BehaviorTreeEditor.tsx index 3c3d5679..13efddfb 100644 --- a/packages/behavior-tree/src/editor/components/BehaviorTreeEditor.tsx +++ b/packages/behavior-tree-editor/src/components/BehaviorTreeEditor.tsx @@ -1,5 +1,5 @@ import { React, useEffect, useMemo, useRef, useState, useCallback } from '@esengine/editor-runtime'; -import { NodeTemplate, BlackboardValueType } from '../..'; +import { BlackboardValueType, type NodeTemplate } from '@esengine/behavior-tree'; import { useBehaviorTreeDataStore, BehaviorTreeNode, ROOT_NODE_ID } from '../stores'; import { useUIStore } from '../stores'; import { showToast as notificationShowToast } from '../services/NotificationService'; diff --git a/packages/behavior-tree/src/editor/components/blackboard/BlackboardPanel.tsx b/packages/behavior-tree-editor/src/components/blackboard/BlackboardPanel.tsx similarity index 100% rename from packages/behavior-tree/src/editor/components/blackboard/BlackboardPanel.tsx rename to packages/behavior-tree-editor/src/components/blackboard/BlackboardPanel.tsx diff --git a/packages/behavior-tree/src/editor/components/canvas/BehaviorTreeCanvas.tsx b/packages/behavior-tree-editor/src/components/canvas/BehaviorTreeCanvas.tsx similarity index 100% rename from packages/behavior-tree/src/editor/components/canvas/BehaviorTreeCanvas.tsx rename to packages/behavior-tree-editor/src/components/canvas/BehaviorTreeCanvas.tsx diff --git a/packages/behavior-tree/src/editor/components/canvas/GridBackground.tsx b/packages/behavior-tree-editor/src/components/canvas/GridBackground.tsx similarity index 100% rename from packages/behavior-tree/src/editor/components/canvas/GridBackground.tsx rename to packages/behavior-tree-editor/src/components/canvas/GridBackground.tsx diff --git a/packages/behavior-tree/src/editor/components/canvas/index.ts b/packages/behavior-tree-editor/src/components/canvas/index.ts similarity index 100% rename from packages/behavior-tree/src/editor/components/canvas/index.ts rename to packages/behavior-tree-editor/src/components/canvas/index.ts diff --git a/packages/behavior-tree/src/editor/components/common/DraggablePanel.tsx b/packages/behavior-tree-editor/src/components/common/DraggablePanel.tsx similarity index 100% rename from packages/behavior-tree/src/editor/components/common/DraggablePanel.tsx rename to packages/behavior-tree-editor/src/components/common/DraggablePanel.tsx diff --git a/packages/behavior-tree/src/editor/components/connections/ConnectionLayer.tsx b/packages/behavior-tree-editor/src/components/connections/ConnectionLayer.tsx similarity index 100% rename from packages/behavior-tree/src/editor/components/connections/ConnectionLayer.tsx rename to packages/behavior-tree-editor/src/components/connections/ConnectionLayer.tsx diff --git a/packages/behavior-tree/src/editor/components/connections/ConnectionRenderer.tsx b/packages/behavior-tree-editor/src/components/connections/ConnectionRenderer.tsx similarity index 100% rename from packages/behavior-tree/src/editor/components/connections/ConnectionRenderer.tsx rename to packages/behavior-tree-editor/src/components/connections/ConnectionRenderer.tsx diff --git a/packages/behavior-tree/src/editor/components/connections/index.ts b/packages/behavior-tree-editor/src/components/connections/index.ts similarity index 100% rename from packages/behavior-tree/src/editor/components/connections/index.ts rename to packages/behavior-tree-editor/src/components/connections/index.ts diff --git a/packages/behavior-tree/src/editor/components/menu/NodeContextMenu.tsx b/packages/behavior-tree-editor/src/components/menu/NodeContextMenu.tsx similarity index 100% rename from packages/behavior-tree/src/editor/components/menu/NodeContextMenu.tsx rename to packages/behavior-tree-editor/src/components/menu/NodeContextMenu.tsx diff --git a/packages/behavior-tree/src/editor/components/menu/QuickCreateMenu.tsx b/packages/behavior-tree-editor/src/components/menu/QuickCreateMenu.tsx similarity index 99% rename from packages/behavior-tree/src/editor/components/menu/QuickCreateMenu.tsx rename to packages/behavior-tree-editor/src/components/menu/QuickCreateMenu.tsx index 98c2be9e..170d6d1b 100644 --- a/packages/behavior-tree/src/editor/components/menu/QuickCreateMenu.tsx +++ b/packages/behavior-tree-editor/src/components/menu/QuickCreateMenu.tsx @@ -1,6 +1,6 @@ import { React, useRef, useEffect, useState, useMemo, Icons } from '@esengine/editor-runtime'; import type { LucideIcon } from '@esengine/editor-runtime'; -import { NodeTemplate } from '../../..'; +import type { NodeTemplate } from '@esengine/behavior-tree'; import { NodeFactory } from '../../infrastructure/factories/NodeFactory'; const { Search, X, ChevronDown, ChevronRight } = Icons; diff --git a/packages/behavior-tree/src/editor/components/nodes/BehaviorTreeNode.tsx b/packages/behavior-tree-editor/src/components/nodes/BehaviorTreeNode.tsx similarity index 99% rename from packages/behavior-tree/src/editor/components/nodes/BehaviorTreeNode.tsx rename to packages/behavior-tree-editor/src/components/nodes/BehaviorTreeNode.tsx index d3a375d5..fe3cb6c3 100644 --- a/packages/behavior-tree/src/editor/components/nodes/BehaviorTreeNode.tsx +++ b/packages/behavior-tree-editor/src/components/nodes/BehaviorTreeNode.tsx @@ -1,6 +1,6 @@ import { React, Icons } from '@esengine/editor-runtime'; import type { LucideIcon } from '@esengine/editor-runtime'; -import { PropertyDefinition } from '../../..'; +import type { PropertyDefinition } from '@esengine/behavior-tree'; import { Node as BehaviorTreeNodeType } from '../../domain/models/Node'; import { Connection } from '../../domain/models/Connection'; diff --git a/packages/behavior-tree/src/editor/components/nodes/BehaviorTreeNodeRenderer.tsx b/packages/behavior-tree-editor/src/components/nodes/BehaviorTreeNodeRenderer.tsx similarity index 100% rename from packages/behavior-tree/src/editor/components/nodes/BehaviorTreeNodeRenderer.tsx rename to packages/behavior-tree-editor/src/components/nodes/BehaviorTreeNodeRenderer.tsx diff --git a/packages/behavior-tree/src/editor/components/nodes/index.ts b/packages/behavior-tree-editor/src/components/nodes/index.ts similarity index 100% rename from packages/behavior-tree/src/editor/components/nodes/index.ts rename to packages/behavior-tree-editor/src/components/nodes/index.ts diff --git a/packages/behavior-tree/src/editor/components/panels/BehaviorTreeEditorPanel.css b/packages/behavior-tree-editor/src/components/panels/BehaviorTreeEditorPanel.css similarity index 100% rename from packages/behavior-tree/src/editor/components/panels/BehaviorTreeEditorPanel.css rename to packages/behavior-tree-editor/src/components/panels/BehaviorTreeEditorPanel.css diff --git a/packages/behavior-tree/src/editor/components/panels/BehaviorTreeEditorPanel.tsx b/packages/behavior-tree-editor/src/components/panels/BehaviorTreeEditorPanel.tsx similarity index 100% rename from packages/behavior-tree/src/editor/components/panels/BehaviorTreeEditorPanel.tsx rename to packages/behavior-tree-editor/src/components/panels/BehaviorTreeEditorPanel.tsx diff --git a/packages/behavior-tree/src/editor/components/panels/BehaviorTreePropertiesPanel.css b/packages/behavior-tree-editor/src/components/panels/BehaviorTreePropertiesPanel.css similarity index 100% rename from packages/behavior-tree/src/editor/components/panels/BehaviorTreePropertiesPanel.css rename to packages/behavior-tree-editor/src/components/panels/BehaviorTreePropertiesPanel.css diff --git a/packages/behavior-tree/src/editor/components/toolbar/EditorToolbar.tsx b/packages/behavior-tree-editor/src/components/toolbar/EditorToolbar.tsx similarity index 100% rename from packages/behavior-tree/src/editor/components/toolbar/EditorToolbar.tsx rename to packages/behavior-tree-editor/src/components/toolbar/EditorToolbar.tsx diff --git a/packages/behavior-tree/src/editor/config/editorConstants.ts b/packages/behavior-tree-editor/src/config/editorConstants.ts similarity index 94% rename from packages/behavior-tree/src/editor/config/editorConstants.ts rename to packages/behavior-tree-editor/src/config/editorConstants.ts index d9ab11b9..da1e7830 100644 --- a/packages/behavior-tree/src/editor/config/editorConstants.ts +++ b/packages/behavior-tree-editor/src/config/editorConstants.ts @@ -1,4 +1,4 @@ -import { NodeTemplate, NodeType } from '../..'; +import { NodeType, type NodeTemplate } from '@esengine/behavior-tree'; import { Icons } from '@esengine/editor-runtime'; import type { LucideIcon } from '@esengine/editor-runtime'; diff --git a/packages/behavior-tree/src/editor/constants/index.ts b/packages/behavior-tree-editor/src/constants/index.ts similarity index 100% rename from packages/behavior-tree/src/editor/constants/index.ts rename to packages/behavior-tree-editor/src/constants/index.ts diff --git a/packages/behavior-tree/src/editor/domain/constants/RootNode.ts b/packages/behavior-tree-editor/src/domain/constants/RootNode.ts similarity index 91% rename from packages/behavior-tree/src/editor/domain/constants/RootNode.ts rename to packages/behavior-tree-editor/src/domain/constants/RootNode.ts index 2db45804..69f566c9 100644 --- a/packages/behavior-tree/src/editor/domain/constants/RootNode.ts +++ b/packages/behavior-tree-editor/src/domain/constants/RootNode.ts @@ -1,6 +1,6 @@ import { Node } from '../models/Node'; import { Position } from '../value-objects/Position'; -import { NodeTemplate } from '../../..'; +import type { NodeTemplate } from '@esengine/behavior-tree'; export const ROOT_NODE_ID = 'root-node'; diff --git a/packages/behavior-tree/src/editor/domain/errors/DomainError.ts b/packages/behavior-tree-editor/src/domain/errors/DomainError.ts similarity index 100% rename from packages/behavior-tree/src/editor/domain/errors/DomainError.ts rename to packages/behavior-tree-editor/src/domain/errors/DomainError.ts diff --git a/packages/behavior-tree/src/editor/domain/errors/NodeNotFoundError.ts b/packages/behavior-tree-editor/src/domain/errors/NodeNotFoundError.ts similarity index 100% rename from packages/behavior-tree/src/editor/domain/errors/NodeNotFoundError.ts rename to packages/behavior-tree-editor/src/domain/errors/NodeNotFoundError.ts diff --git a/packages/behavior-tree/src/editor/domain/errors/ValidationError.ts b/packages/behavior-tree-editor/src/domain/errors/ValidationError.ts similarity index 100% rename from packages/behavior-tree/src/editor/domain/errors/ValidationError.ts rename to packages/behavior-tree-editor/src/domain/errors/ValidationError.ts diff --git a/packages/behavior-tree/src/editor/domain/errors/index.ts b/packages/behavior-tree-editor/src/domain/errors/index.ts similarity index 100% rename from packages/behavior-tree/src/editor/domain/errors/index.ts rename to packages/behavior-tree-editor/src/domain/errors/index.ts diff --git a/packages/behavior-tree/src/editor/domain/index.ts b/packages/behavior-tree-editor/src/domain/index.ts similarity index 100% rename from packages/behavior-tree/src/editor/domain/index.ts rename to packages/behavior-tree-editor/src/domain/index.ts diff --git a/packages/behavior-tree/src/editor/domain/interfaces/INodeFactory.ts b/packages/behavior-tree-editor/src/domain/interfaces/INodeFactory.ts similarity index 91% rename from packages/behavior-tree/src/editor/domain/interfaces/INodeFactory.ts rename to packages/behavior-tree-editor/src/domain/interfaces/INodeFactory.ts index 2ac10fd6..9edb0f34 100644 --- a/packages/behavior-tree/src/editor/domain/interfaces/INodeFactory.ts +++ b/packages/behavior-tree-editor/src/domain/interfaces/INodeFactory.ts @@ -1,4 +1,4 @@ -import { NodeTemplate } from '../../..'; +import type { NodeTemplate } from '@esengine/behavior-tree'; import { Node } from '../models/Node'; import { Position } from '../value-objects'; diff --git a/packages/behavior-tree/src/editor/domain/interfaces/IRepository.ts b/packages/behavior-tree-editor/src/domain/interfaces/IRepository.ts similarity index 100% rename from packages/behavior-tree/src/editor/domain/interfaces/IRepository.ts rename to packages/behavior-tree-editor/src/domain/interfaces/IRepository.ts diff --git a/packages/behavior-tree/src/editor/domain/interfaces/ISerializer.ts b/packages/behavior-tree-editor/src/domain/interfaces/ISerializer.ts similarity index 100% rename from packages/behavior-tree/src/editor/domain/interfaces/ISerializer.ts rename to packages/behavior-tree-editor/src/domain/interfaces/ISerializer.ts diff --git a/packages/behavior-tree/src/editor/domain/interfaces/IValidator.ts b/packages/behavior-tree-editor/src/domain/interfaces/IValidator.ts similarity index 100% rename from packages/behavior-tree/src/editor/domain/interfaces/IValidator.ts rename to packages/behavior-tree-editor/src/domain/interfaces/IValidator.ts diff --git a/packages/behavior-tree/src/editor/domain/interfaces/index.ts b/packages/behavior-tree-editor/src/domain/interfaces/index.ts similarity index 100% rename from packages/behavior-tree/src/editor/domain/interfaces/index.ts rename to packages/behavior-tree-editor/src/domain/interfaces/index.ts diff --git a/packages/behavior-tree/src/editor/domain/models/BehaviorTree.ts b/packages/behavior-tree-editor/src/domain/models/BehaviorTree.ts similarity index 100% rename from packages/behavior-tree/src/editor/domain/models/BehaviorTree.ts rename to packages/behavior-tree-editor/src/domain/models/BehaviorTree.ts diff --git a/packages/behavior-tree/src/editor/domain/models/Blackboard.ts b/packages/behavior-tree-editor/src/domain/models/Blackboard.ts similarity index 100% rename from packages/behavior-tree/src/editor/domain/models/Blackboard.ts rename to packages/behavior-tree-editor/src/domain/models/Blackboard.ts diff --git a/packages/behavior-tree/src/editor/domain/models/Connection.ts b/packages/behavior-tree-editor/src/domain/models/Connection.ts similarity index 100% rename from packages/behavior-tree/src/editor/domain/models/Connection.ts rename to packages/behavior-tree-editor/src/domain/models/Connection.ts diff --git a/packages/behavior-tree/src/editor/domain/models/Node.ts b/packages/behavior-tree-editor/src/domain/models/Node.ts similarity index 98% rename from packages/behavior-tree/src/editor/domain/models/Node.ts rename to packages/behavior-tree-editor/src/domain/models/Node.ts index 476525af..bb1a7d50 100644 --- a/packages/behavior-tree/src/editor/domain/models/Node.ts +++ b/packages/behavior-tree-editor/src/domain/models/Node.ts @@ -1,4 +1,4 @@ -import { NodeTemplate } from '../../..'; +import type { NodeTemplate } from '@esengine/behavior-tree'; import { Position, NodeType } from '../value-objects'; import { ValidationError } from '../errors'; diff --git a/packages/behavior-tree/src/editor/domain/models/index.ts b/packages/behavior-tree-editor/src/domain/models/index.ts similarity index 100% rename from packages/behavior-tree/src/editor/domain/models/index.ts rename to packages/behavior-tree-editor/src/domain/models/index.ts diff --git a/packages/behavior-tree/src/editor/domain/services/TreeValidator.ts b/packages/behavior-tree-editor/src/domain/services/TreeValidator.ts similarity index 100% rename from packages/behavior-tree/src/editor/domain/services/TreeValidator.ts rename to packages/behavior-tree-editor/src/domain/services/TreeValidator.ts diff --git a/packages/behavior-tree/src/editor/domain/services/index.ts b/packages/behavior-tree-editor/src/domain/services/index.ts similarity index 100% rename from packages/behavior-tree/src/editor/domain/services/index.ts rename to packages/behavior-tree-editor/src/domain/services/index.ts diff --git a/packages/behavior-tree/src/editor/domain/value-objects/NodeType.ts b/packages/behavior-tree-editor/src/domain/value-objects/NodeType.ts similarity index 100% rename from packages/behavior-tree/src/editor/domain/value-objects/NodeType.ts rename to packages/behavior-tree-editor/src/domain/value-objects/NodeType.ts diff --git a/packages/behavior-tree/src/editor/domain/value-objects/Position.ts b/packages/behavior-tree-editor/src/domain/value-objects/Position.ts similarity index 100% rename from packages/behavior-tree/src/editor/domain/value-objects/Position.ts rename to packages/behavior-tree-editor/src/domain/value-objects/Position.ts diff --git a/packages/behavior-tree/src/editor/domain/value-objects/Size.ts b/packages/behavior-tree-editor/src/domain/value-objects/Size.ts similarity index 100% rename from packages/behavior-tree/src/editor/domain/value-objects/Size.ts rename to packages/behavior-tree-editor/src/domain/value-objects/Size.ts diff --git a/packages/behavior-tree/src/editor/domain/value-objects/index.ts b/packages/behavior-tree-editor/src/domain/value-objects/index.ts similarity index 100% rename from packages/behavior-tree/src/editor/domain/value-objects/index.ts rename to packages/behavior-tree-editor/src/domain/value-objects/index.ts diff --git a/packages/behavior-tree/src/editor/generators/GlobalBlackboardTypeGenerator.ts b/packages/behavior-tree-editor/src/generators/GlobalBlackboardTypeGenerator.ts similarity index 99% rename from packages/behavior-tree/src/editor/generators/GlobalBlackboardTypeGenerator.ts rename to packages/behavior-tree-editor/src/generators/GlobalBlackboardTypeGenerator.ts index dfb05952..7af7bc5d 100644 --- a/packages/behavior-tree/src/editor/generators/GlobalBlackboardTypeGenerator.ts +++ b/packages/behavior-tree-editor/src/generators/GlobalBlackboardTypeGenerator.ts @@ -1,4 +1,4 @@ -import { GlobalBlackboardConfig, BlackboardValueType } from '../..'; +import { BlackboardValueType, type GlobalBlackboardConfig } from '@esengine/behavior-tree'; /** * 类型生成配置选项 diff --git a/packages/behavior-tree/src/editor/generators/LocalBlackboardTypeGenerator.ts b/packages/behavior-tree-editor/src/generators/LocalBlackboardTypeGenerator.ts similarity index 100% rename from packages/behavior-tree/src/editor/generators/LocalBlackboardTypeGenerator.ts rename to packages/behavior-tree-editor/src/generators/LocalBlackboardTypeGenerator.ts diff --git a/packages/behavior-tree/src/editor/hooks/index.ts b/packages/behavior-tree-editor/src/hooks/index.ts similarity index 100% rename from packages/behavior-tree/src/editor/hooks/index.ts rename to packages/behavior-tree-editor/src/hooks/index.ts diff --git a/packages/behavior-tree/src/editor/hooks/useCanvasInteraction.ts b/packages/behavior-tree-editor/src/hooks/useCanvasInteraction.ts similarity index 100% rename from packages/behavior-tree/src/editor/hooks/useCanvasInteraction.ts rename to packages/behavior-tree-editor/src/hooks/useCanvasInteraction.ts diff --git a/packages/behavior-tree/src/editor/hooks/useCanvasMouseEvents.ts b/packages/behavior-tree-editor/src/hooks/useCanvasMouseEvents.ts similarity index 100% rename from packages/behavior-tree/src/editor/hooks/useCanvasMouseEvents.ts rename to packages/behavior-tree-editor/src/hooks/useCanvasMouseEvents.ts diff --git a/packages/behavior-tree/src/editor/hooks/useCommandHistory.ts b/packages/behavior-tree-editor/src/hooks/useCommandHistory.ts similarity index 100% rename from packages/behavior-tree/src/editor/hooks/useCommandHistory.ts rename to packages/behavior-tree-editor/src/hooks/useCommandHistory.ts diff --git a/packages/behavior-tree/src/editor/hooks/useConnectionOperations.ts b/packages/behavior-tree-editor/src/hooks/useConnectionOperations.ts similarity index 100% rename from packages/behavior-tree/src/editor/hooks/useConnectionOperations.ts rename to packages/behavior-tree-editor/src/hooks/useConnectionOperations.ts diff --git a/packages/behavior-tree/src/editor/hooks/useContextMenu.ts b/packages/behavior-tree-editor/src/hooks/useContextMenu.ts similarity index 100% rename from packages/behavior-tree/src/editor/hooks/useContextMenu.ts rename to packages/behavior-tree-editor/src/hooks/useContextMenu.ts diff --git a/packages/behavior-tree/src/editor/hooks/useDropHandler.ts b/packages/behavior-tree-editor/src/hooks/useDropHandler.ts similarity index 98% rename from packages/behavior-tree/src/editor/hooks/useDropHandler.ts rename to packages/behavior-tree-editor/src/hooks/useDropHandler.ts index 43482349..a1ebe2a6 100644 --- a/packages/behavior-tree/src/editor/hooks/useDropHandler.ts +++ b/packages/behavior-tree-editor/src/hooks/useDropHandler.ts @@ -1,5 +1,5 @@ import { useState, type RefObject, React, createLogger } from '@esengine/editor-runtime'; -import { NodeTemplate, NodeType } from '../..'; +import { NodeType, type NodeTemplate } from '@esengine/behavior-tree'; import { Position } from '../domain/value-objects/Position'; import { useNodeOperations } from './useNodeOperations'; diff --git a/packages/behavior-tree/src/editor/hooks/useEditorHandlers.ts b/packages/behavior-tree-editor/src/hooks/useEditorHandlers.ts similarity index 100% rename from packages/behavior-tree/src/editor/hooks/useEditorHandlers.ts rename to packages/behavior-tree-editor/src/hooks/useEditorHandlers.ts diff --git a/packages/behavior-tree/src/editor/hooks/useEditorState.ts b/packages/behavior-tree-editor/src/hooks/useEditorState.ts similarity index 100% rename from packages/behavior-tree/src/editor/hooks/useEditorState.ts rename to packages/behavior-tree-editor/src/hooks/useEditorState.ts diff --git a/packages/behavior-tree/src/editor/hooks/useExecutionController.ts b/packages/behavior-tree-editor/src/hooks/useExecutionController.ts similarity index 100% rename from packages/behavior-tree/src/editor/hooks/useExecutionController.ts rename to packages/behavior-tree-editor/src/hooks/useExecutionController.ts diff --git a/packages/behavior-tree/src/editor/hooks/useKeyboardShortcuts.ts b/packages/behavior-tree-editor/src/hooks/useKeyboardShortcuts.ts similarity index 100% rename from packages/behavior-tree/src/editor/hooks/useKeyboardShortcuts.ts rename to packages/behavior-tree-editor/src/hooks/useKeyboardShortcuts.ts diff --git a/packages/behavior-tree/src/editor/hooks/useNodeDrag.ts b/packages/behavior-tree-editor/src/hooks/useNodeDrag.ts similarity index 100% rename from packages/behavior-tree/src/editor/hooks/useNodeDrag.ts rename to packages/behavior-tree-editor/src/hooks/useNodeDrag.ts diff --git a/packages/behavior-tree/src/editor/hooks/useNodeOperations.ts b/packages/behavior-tree-editor/src/hooks/useNodeOperations.ts similarity index 97% rename from packages/behavior-tree/src/editor/hooks/useNodeOperations.ts rename to packages/behavior-tree-editor/src/hooks/useNodeOperations.ts index aa52006f..6be1b5d6 100644 --- a/packages/behavior-tree/src/editor/hooks/useNodeOperations.ts +++ b/packages/behavior-tree-editor/src/hooks/useNodeOperations.ts @@ -1,5 +1,5 @@ import { useCallback, useMemo, CommandManager } from '@esengine/editor-runtime'; -import { NodeTemplate } from '../..'; +import type { NodeTemplate } from '@esengine/behavior-tree'; import { Position } from '../domain/value-objects/Position'; import { INodeFactory } from '../domain/interfaces/INodeFactory'; import { TreeStateAdapter } from '../application/state/BehaviorTreeDataStore'; diff --git a/packages/behavior-tree/src/editor/hooks/useNodeTracking.ts b/packages/behavior-tree-editor/src/hooks/useNodeTracking.ts similarity index 100% rename from packages/behavior-tree/src/editor/hooks/useNodeTracking.ts rename to packages/behavior-tree-editor/src/hooks/useNodeTracking.ts diff --git a/packages/behavior-tree/src/editor/hooks/usePortConnection.ts b/packages/behavior-tree-editor/src/hooks/usePortConnection.ts similarity index 99% rename from packages/behavior-tree/src/editor/hooks/usePortConnection.ts rename to packages/behavior-tree-editor/src/hooks/usePortConnection.ts index 65c90c07..e425cc26 100644 --- a/packages/behavior-tree/src/editor/hooks/usePortConnection.ts +++ b/packages/behavior-tree-editor/src/hooks/usePortConnection.ts @@ -1,6 +1,6 @@ import { type RefObject, React } from '@esengine/editor-runtime'; import { BehaviorTreeNode, Connection, ROOT_NODE_ID, useUIStore } from '../stores'; -import { PropertyDefinition } from '../..'; +import type { PropertyDefinition } from '@esengine/behavior-tree'; import { useConnectionOperations } from './useConnectionOperations'; interface UsePortConnectionParams { diff --git a/packages/behavior-tree/src/editor/hooks/useQuickCreateMenu.ts b/packages/behavior-tree-editor/src/hooks/useQuickCreateMenu.ts similarity index 99% rename from packages/behavior-tree/src/editor/hooks/useQuickCreateMenu.ts rename to packages/behavior-tree-editor/src/hooks/useQuickCreateMenu.ts index 777de355..eac9a74b 100644 --- a/packages/behavior-tree/src/editor/hooks/useQuickCreateMenu.ts +++ b/packages/behavior-tree-editor/src/hooks/useQuickCreateMenu.ts @@ -1,5 +1,5 @@ import { useState, type RefObject } from '@esengine/editor-runtime'; -import { NodeTemplate } from '../..'; +import type { NodeTemplate } from '@esengine/behavior-tree'; import { BehaviorTreeNode, Connection, useBehaviorTreeDataStore } from '../stores'; import { Node } from '../domain/models/Node'; import { Position } from '../domain/value-objects/Position'; diff --git a/packages/behavior-tree/src/editor/index.ts b/packages/behavior-tree-editor/src/index.ts similarity index 96% rename from packages/behavior-tree/src/editor/index.ts rename to packages/behavior-tree-editor/src/index.ts index ff1d3ec1..6772feae 100644 --- a/packages/behavior-tree/src/editor/index.ts +++ b/packages/behavior-tree-editor/src/index.ts @@ -4,7 +4,7 @@ */ import type { ServiceContainer } from '@esengine/ecs-framework'; -import { TransformComponent } from '@esengine/ecs-components'; +import { TransformComponent } from '@esengine/engine-core'; import { type IEditorModuleLoader, type IPluginLoader, @@ -20,6 +20,7 @@ import { MessageHub, IMessageHub, FileActionRegistry, + IFileActionRegistry, IDialogService, IFileSystemService, type IDialog, @@ -28,8 +29,8 @@ import { PluginAPI, } from '@esengine/editor-runtime'; -// Runtime imports (relative paths since we're in the same package) -import { BehaviorTreeRuntimeComponent } from '../execution/BehaviorTreeRuntimeComponent'; +// Runtime imports from @esengine/behavior-tree package +import { BehaviorTreeRuntimeComponent, BehaviorTreeRuntimeModule } from '@esengine/behavior-tree'; // Editor components and services import { BehaviorTreeService } from './services/BehaviorTreeService'; @@ -41,9 +42,8 @@ import { useBehaviorTreeDataStore } from './stores'; import { createRootNode } from './domain/constants/RootNode'; import { PluginContext } from './PluginContext'; -// Import descriptor from local file, runtime module from main module +// Import descriptor from local file import { descriptor } from './BehaviorTreePlugin'; -import { BehaviorTreeRuntimeModule } from '../BehaviorTreeRuntimeModule'; // 导入编辑器 CSS 样式(会被 vite 自动处理并注入到 DOM) // Import editor CSS styles (automatically handled and injected by vite) @@ -86,7 +86,7 @@ export class BehaviorTreeEditorModule implements IEditorModuleLoader { private registerAssetCreationMappings(services: ServiceContainer): void { try { - const fileActionRegistry = services.resolve(FileActionRegistry); + const fileActionRegistry = services.resolve(IFileActionRegistry); if (fileActionRegistry) { fileActionRegistry.registerAssetCreationMapping({ extension: '.btree', diff --git a/packages/behavior-tree/src/editor/infrastructure/events/EditorEventBus.ts b/packages/behavior-tree-editor/src/infrastructure/events/EditorEventBus.ts similarity index 100% rename from packages/behavior-tree/src/editor/infrastructure/events/EditorEventBus.ts rename to packages/behavior-tree-editor/src/infrastructure/events/EditorEventBus.ts diff --git a/packages/behavior-tree/src/editor/infrastructure/factories/NodeFactory.ts b/packages/behavior-tree-editor/src/infrastructure/factories/NodeFactory.ts similarity index 97% rename from packages/behavior-tree/src/editor/infrastructure/factories/NodeFactory.ts rename to packages/behavior-tree-editor/src/infrastructure/factories/NodeFactory.ts index 4bb9228c..cc0670b2 100644 --- a/packages/behavior-tree/src/editor/infrastructure/factories/NodeFactory.ts +++ b/packages/behavior-tree-editor/src/infrastructure/factories/NodeFactory.ts @@ -1,4 +1,4 @@ -import { NodeTemplate, NodeTemplates } from '../../..'; +import { NodeTemplates, type NodeTemplate } from '@esengine/behavior-tree'; import { Node } from '../../domain/models/Node'; import { Position } from '../../domain/value-objects/Position'; import { INodeFactory } from '../../domain/interfaces/INodeFactory'; diff --git a/packages/behavior-tree/src/editor/infrastructure/factories/index.ts b/packages/behavior-tree-editor/src/infrastructure/factories/index.ts similarity index 100% rename from packages/behavior-tree/src/editor/infrastructure/factories/index.ts rename to packages/behavior-tree-editor/src/infrastructure/factories/index.ts diff --git a/packages/behavior-tree/src/editor/infrastructure/index.ts b/packages/behavior-tree-editor/src/infrastructure/index.ts similarity index 100% rename from packages/behavior-tree/src/editor/infrastructure/index.ts rename to packages/behavior-tree-editor/src/infrastructure/index.ts diff --git a/packages/behavior-tree/src/editor/infrastructure/serialization/BehaviorTreeSerializer.ts b/packages/behavior-tree-editor/src/infrastructure/serialization/BehaviorTreeSerializer.ts similarity index 99% rename from packages/behavior-tree/src/editor/infrastructure/serialization/BehaviorTreeSerializer.ts rename to packages/behavior-tree-editor/src/infrastructure/serialization/BehaviorTreeSerializer.ts index 9fd868e0..51f3d989 100644 --- a/packages/behavior-tree/src/editor/infrastructure/serialization/BehaviorTreeSerializer.ts +++ b/packages/behavior-tree-editor/src/infrastructure/serialization/BehaviorTreeSerializer.ts @@ -1,6 +1,6 @@ import { BehaviorTree } from '../../domain/models/BehaviorTree'; import { ISerializer, SerializationFormat } from '../../domain/interfaces/ISerializer'; -import { BehaviorTreeAssetSerializer, EditorFormatConverter } from '../../..'; +import { BehaviorTreeAssetSerializer, EditorFormatConverter } from '@esengine/behavior-tree'; /** * 序列化选项 diff --git a/packages/behavior-tree/src/editor/infrastructure/serialization/index.ts b/packages/behavior-tree-editor/src/infrastructure/serialization/index.ts similarity index 100% rename from packages/behavior-tree/src/editor/infrastructure/serialization/index.ts rename to packages/behavior-tree-editor/src/infrastructure/serialization/index.ts diff --git a/packages/behavior-tree/src/editor/infrastructure/services/NodeRegistryService.ts b/packages/behavior-tree-editor/src/infrastructure/services/NodeRegistryService.ts similarity index 98% rename from packages/behavior-tree/src/editor/infrastructure/services/NodeRegistryService.ts rename to packages/behavior-tree-editor/src/infrastructure/services/NodeRegistryService.ts index e567f7a7..7154c304 100644 --- a/packages/behavior-tree/src/editor/infrastructure/services/NodeRegistryService.ts +++ b/packages/behavior-tree-editor/src/infrastructure/services/NodeRegistryService.ts @@ -1,4 +1,4 @@ -import { NodeTemplate, NodeMetadataRegistry, NodeMetadata, NodeType } from '../../..'; +import { NodeMetadataRegistry, NodeType, type NodeTemplate, type NodeMetadata } from '@esengine/behavior-tree'; /** * 简化的节点注册配置 diff --git a/packages/behavior-tree/src/editor/infrastructure/validation/BehaviorTreeValidator.ts b/packages/behavior-tree-editor/src/infrastructure/validation/BehaviorTreeValidator.ts similarity index 100% rename from packages/behavior-tree/src/editor/infrastructure/validation/BehaviorTreeValidator.ts rename to packages/behavior-tree-editor/src/infrastructure/validation/BehaviorTreeValidator.ts diff --git a/packages/behavior-tree/src/editor/interfaces/IEditorExtensions.ts b/packages/behavior-tree-editor/src/interfaces/IEditorExtensions.ts similarity index 99% rename from packages/behavior-tree/src/editor/interfaces/IEditorExtensions.ts rename to packages/behavior-tree-editor/src/interfaces/IEditorExtensions.ts index 0d1faa0a..fca0a475 100644 --- a/packages/behavior-tree/src/editor/interfaces/IEditorExtensions.ts +++ b/packages/behavior-tree-editor/src/interfaces/IEditorExtensions.ts @@ -1,6 +1,6 @@ import { React, createLogger } from '@esengine/editor-runtime'; import type { LucideIcon } from '@esengine/editor-runtime'; -import { NodeTemplate } from '../..'; +import type { NodeTemplate } from '@esengine/behavior-tree'; import { Node as BehaviorTreeNode } from '../domain/models/Node'; const logger = createLogger('IEditorExtensions'); diff --git a/packages/behavior-tree/src/editor/providers/BehaviorTreeNodeInspectorProvider.tsx b/packages/behavior-tree-editor/src/providers/BehaviorTreeNodeInspectorProvider.tsx similarity index 99% rename from packages/behavior-tree/src/editor/providers/BehaviorTreeNodeInspectorProvider.tsx rename to packages/behavior-tree-editor/src/providers/BehaviorTreeNodeInspectorProvider.tsx index 6ae22f38..9887c78c 100644 --- a/packages/behavior-tree/src/editor/providers/BehaviorTreeNodeInspectorProvider.tsx +++ b/packages/behavior-tree-editor/src/providers/BehaviorTreeNodeInspectorProvider.tsx @@ -10,7 +10,7 @@ import { PluginAPI, } from '@esengine/editor-runtime'; import { Node as BehaviorTreeNode } from '../domain/models/Node'; -import { PropertyDefinition } from '../..'; +import type { PropertyDefinition } from '@esengine/behavior-tree'; /** * 节点属性编辑器组件 diff --git a/packages/behavior-tree/src/editor/services/BehaviorTreeService.ts b/packages/behavior-tree-editor/src/services/BehaviorTreeService.ts similarity index 100% rename from packages/behavior-tree/src/editor/services/BehaviorTreeService.ts rename to packages/behavior-tree-editor/src/services/BehaviorTreeService.ts diff --git a/packages/behavior-tree/src/editor/services/FileSystemService.ts b/packages/behavior-tree-editor/src/services/FileSystemService.ts similarity index 100% rename from packages/behavior-tree/src/editor/services/FileSystemService.ts rename to packages/behavior-tree-editor/src/services/FileSystemService.ts diff --git a/packages/behavior-tree/src/editor/services/NotificationService.ts b/packages/behavior-tree-editor/src/services/NotificationService.ts similarity index 100% rename from packages/behavior-tree/src/editor/services/NotificationService.ts rename to packages/behavior-tree-editor/src/services/NotificationService.ts diff --git a/packages/behavior-tree/src/editor/services/index.ts b/packages/behavior-tree-editor/src/services/index.ts similarity index 100% rename from packages/behavior-tree/src/editor/services/index.ts rename to packages/behavior-tree-editor/src/services/index.ts diff --git a/packages/behavior-tree/src/editor/stores/ExecutionStatsStore.ts b/packages/behavior-tree-editor/src/stores/ExecutionStatsStore.ts similarity index 100% rename from packages/behavior-tree/src/editor/stores/ExecutionStatsStore.ts rename to packages/behavior-tree-editor/src/stores/ExecutionStatsStore.ts diff --git a/packages/behavior-tree/src/editor/stores/index.ts b/packages/behavior-tree-editor/src/stores/index.ts similarity index 100% rename from packages/behavior-tree/src/editor/stores/index.ts rename to packages/behavior-tree-editor/src/stores/index.ts diff --git a/packages/behavior-tree/src/editor/stores/useUIStore.ts b/packages/behavior-tree-editor/src/stores/useUIStore.ts similarity index 100% rename from packages/behavior-tree/src/editor/stores/useUIStore.ts rename to packages/behavior-tree-editor/src/stores/useUIStore.ts diff --git a/packages/behavior-tree/src/editor/styles/BehaviorTreeNode.css b/packages/behavior-tree-editor/src/styles/BehaviorTreeNode.css similarity index 100% rename from packages/behavior-tree/src/editor/styles/BehaviorTreeNode.css rename to packages/behavior-tree-editor/src/styles/BehaviorTreeNode.css diff --git a/packages/behavior-tree/src/editor/styles/Toast.css b/packages/behavior-tree-editor/src/styles/Toast.css similarity index 100% rename from packages/behavior-tree/src/editor/styles/Toast.css rename to packages/behavior-tree-editor/src/styles/Toast.css diff --git a/packages/behavior-tree/src/editor/types/Breakpoint.ts b/packages/behavior-tree-editor/src/types/Breakpoint.ts similarity index 100% rename from packages/behavior-tree/src/editor/types/Breakpoint.ts rename to packages/behavior-tree-editor/src/types/Breakpoint.ts diff --git a/packages/behavior-tree/src/editor/types/index.ts b/packages/behavior-tree-editor/src/types/index.ts similarity index 100% rename from packages/behavior-tree/src/editor/types/index.ts rename to packages/behavior-tree-editor/src/types/index.ts diff --git a/packages/behavior-tree/src/editor/utils/BehaviorTreeExecutor.ts b/packages/behavior-tree-editor/src/utils/BehaviorTreeExecutor.ts similarity index 99% rename from packages/behavior-tree/src/editor/utils/BehaviorTreeExecutor.ts rename to packages/behavior-tree-editor/src/utils/BehaviorTreeExecutor.ts index ca7d59c7..9e73e2d6 100644 --- a/packages/behavior-tree/src/editor/utils/BehaviorTreeExecutor.ts +++ b/packages/behavior-tree-editor/src/utils/BehaviorTreeExecutor.ts @@ -1,13 +1,13 @@ import { World, Entity, Scene, createLogger, Time, Core } from '@esengine/ecs-framework'; import { - BehaviorTreeData, - BehaviorNodeData, BehaviorTreeRuntimeComponent, BehaviorTreeAssetManager, BehaviorTreeExecutionSystem, TaskStatus, - NodeType -} from '../..'; + NodeType, + type BehaviorTreeData, + type BehaviorNodeData +} from '@esengine/behavior-tree'; import type { BehaviorTreeNode } from '../stores'; import { useExecutionStatsStore } from '../stores/ExecutionStatsStore'; import type { Breakpoint } from '../types/Breakpoint'; diff --git a/packages/behavior-tree/src/editor/utils/DOMCache.ts b/packages/behavior-tree-editor/src/utils/DOMCache.ts similarity index 100% rename from packages/behavior-tree/src/editor/utils/DOMCache.ts rename to packages/behavior-tree-editor/src/utils/DOMCache.ts diff --git a/packages/behavior-tree/src/editor/utils/RuntimeLoader.ts b/packages/behavior-tree-editor/src/utils/RuntimeLoader.ts similarity index 98% rename from packages/behavior-tree/src/editor/utils/RuntimeLoader.ts rename to packages/behavior-tree-editor/src/utils/RuntimeLoader.ts index 2d096212..e4be909c 100644 --- a/packages/behavior-tree/src/editor/utils/RuntimeLoader.ts +++ b/packages/behavior-tree-editor/src/utils/RuntimeLoader.ts @@ -1,4 +1,4 @@ -import { BehaviorTreeAssetManager, BehaviorTreeData } from '../..'; +import { BehaviorTreeAssetManager, type BehaviorTreeData } from '@esengine/behavior-tree'; import { BehaviorTreeSerializer } from '../infrastructure/serialization/BehaviorTreeSerializer'; import { BehaviorTree } from '../domain/models/BehaviorTree'; diff --git a/packages/behavior-tree/src/editor/utils/portUtils.ts b/packages/behavior-tree-editor/src/utils/portUtils.ts similarity index 100% rename from packages/behavior-tree/src/editor/utils/portUtils.ts rename to packages/behavior-tree-editor/src/utils/portUtils.ts diff --git a/packages/behavior-tree-editor/tsconfig.build.json b/packages/behavior-tree-editor/tsconfig.build.json new file mode 100644 index 00000000..ba0684d9 --- /dev/null +++ b/packages/behavior-tree-editor/tsconfig.build.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ES2020", + "moduleResolution": "bundler", + "lib": ["ES2020", "DOM"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "declaration": true, + "declarationMap": true, + "jsx": "react-jsx", + "resolveJsonModule": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/behavior-tree-editor/tsconfig.json b/packages/behavior-tree-editor/tsconfig.json new file mode 100644 index 00000000..d099ddd8 --- /dev/null +++ b/packages/behavior-tree-editor/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src", + "declaration": true, + "jsx": "react-jsx" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/behavior-tree-editor/tsup.config.ts b/packages/behavior-tree-editor/tsup.config.ts new file mode 100644 index 00000000..b4f49f5d --- /dev/null +++ b/packages/behavior-tree-editor/tsup.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'tsup'; +import { editorOnlyPreset } from '../build-config/src/presets/plugin-tsup'; + +export default defineConfig({ + ...editorOnlyPreset(), + tsconfig: 'tsconfig.build.json' +}); diff --git a/packages/behavior-tree/jest.config.cjs b/packages/behavior-tree/jest.config.cjs index a4dcf800..47414c64 100644 --- a/packages/behavior-tree/jest.config.cjs +++ b/packages/behavior-tree/jest.config.cjs @@ -1,7 +1,7 @@ module.exports = { preset: 'ts-jest', testEnvironment: 'node', - roots: ['/tests'], + roots: ['/src'], testMatch: ['**/*.test.ts'], moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], collectCoverageFrom: [ @@ -11,5 +11,6 @@ module.exports = { ], coverageDirectory: 'coverage', verbose: true, - testTimeout: 10000 + testTimeout: 10000, + passWithNoTests: true }; diff --git a/packages/behavior-tree/package.json b/packages/behavior-tree/package.json index 4b5f4baf..54dfcbe8 100644 --- a/packages/behavior-tree/package.json +++ b/packages/behavior-tree/package.json @@ -1,99 +1,63 @@ { - "name": "@esengine/behavior-tree", - "version": "1.0.1", - "description": "ECS-based AI behavior tree system with visual editor and runtime execution", - "main": "dist/index.js", - "module": "dist/index.js", - "types": "dist/index.d.ts", - "type": "module", - "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.js" + "name": "@esengine/behavior-tree", + "version": "1.0.1", + "description": "ECS-based AI behavior tree system with runtime execution", + "esengine": { + "plugin": true, + "pluginExport": "BehaviorTreePlugin", + "category": "ai" }, - "./runtime": { - "types": "./dist/runtime.d.ts", - "import": "./dist/runtime.js" + "main": "dist/index.js", + "module": "dist/index.js", + "types": "dist/index.d.ts", + "type": "module", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } }, - "./editor": { - "types": "./dist/editor/index.d.ts", - "import": "./dist/editor/index.js" + "files": [ + "dist" + ], + "keywords": [ + "ecs", + "behavior-tree", + "ai", + "game-ai", + "entity-component-system" + ], + "scripts": { + "clean": "rimraf dist tsconfig.tsbuildinfo", + "build": "tsup", + "build:watch": "tsup --watch", + "type-check": "tsc --noEmit" }, - "./plugin.json": "./plugin.json" - }, - "files": [ - "dist", - "plugin.json" - ], - "keywords": [ - "ecs", - "behavior-tree", - "ai", - "game-ai", - "entity-component-system" - ], - "scripts": { - "clean": "rimraf dist tsconfig.tsbuildinfo", - "build": "vite build", - "build:watch": "vite build --watch", - "type-check": "tsc --noEmit", - "test": "jest --config jest.config.cjs", - "test:watch": "jest --watch --config jest.config.cjs" - }, - "author": "yhh", - "license": "MIT", - "peerDependencies": { - "@esengine/ecs-framework": ">=2.0.0", - "@esengine/ecs-components": "workspace:*", - "@esengine/asset-system": "workspace:*", - "@esengine/editor-runtime": "workspace:*", - "lucide-react": "^0.545.0", - "react": "^18.3.1", - "zustand": "^4.5.2" - }, - "peerDependenciesMeta": { - "@esengine/ecs-components": { - "optional": true + "author": "yhh", + "license": "MIT", + "devDependencies": { + "@esengine/ecs-framework": "workspace:*", + "@esengine/engine-core": "workspace:*", + "@esengine/asset-system": "workspace:*", + "@esengine/build-config": "workspace:*", + "@types/jest": "^29.5.14", + "@types/node": "^20.19.17", + "jest": "^29.7.0", + "rimraf": "^5.0.0", + "ts-jest": "^29.4.0", + "tsup": "^8.0.0", + "typescript": "^5.8.3" }, - "@esengine/asset-system": { - "optional": true + "dependencies": { + "tslib": "^2.8.1" }, - "@esengine/editor-runtime": { - "optional": true + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" }, - "react": { - "optional": true - }, - "lucide-react": { - "optional": true - }, - "zustand": { - "optional": true + "repository": { + "type": "git", + "url": "https://github.com/esengine/ecs-framework.git", + "directory": "packages/behavior-tree" } - }, - "devDependencies": { - "@tauri-apps/plugin-fs": "^2.4.2", - "@types/jest": "^29.5.14", - "@types/node": "^20.19.17", - "@types/react": "^18.3.12", - "@vitejs/plugin-react": "^4.7.0", - "jest": "^29.7.0", - "rimraf": "^5.0.0", - "ts-jest": "^29.4.0", - "typescript": "^5.8.3", - "vite": "^6.0.7", - "vite-plugin-dts": "^3.7.0" - }, - "dependencies": { - "tslib": "^2.8.1" - }, - "publishConfig": { - "access": "public", - "registry": "https://registry.npmjs.org/" - }, - "repository": { - "type": "git", - "url": "https://github.com/esengine/ecs-framework.git", - "directory": "packages/behavior-tree" - } } diff --git a/packages/behavior-tree/src/BehaviorTreeRuntimeModule.ts b/packages/behavior-tree/src/BehaviorTreeRuntimeModule.ts index 593b1e9c..025b8543 100644 --- a/packages/behavior-tree/src/BehaviorTreeRuntimeModule.ts +++ b/packages/behavior-tree/src/BehaviorTreeRuntimeModule.ts @@ -1,11 +1,6 @@ -/** - * Behavior Tree Runtime Module (Pure runtime, no editor dependencies) - * 行为树运行时模块(纯运行时,无编辑器依赖) - */ - import type { IScene, ServiceContainer } from '@esengine/ecs-framework'; import { ComponentRegistry, Core } from '@esengine/ecs-framework'; -import type { IRuntimeModuleLoader, SystemContext } from '@esengine/ecs-components'; +import type { IRuntimeModule, IPlugin, PluginDescriptor, SystemContext } from '@esengine/engine-core'; import type { AssetManager } from '@esengine/asset-system'; import { BehaviorTreeRuntimeComponent } from './execution/BehaviorTreeRuntimeComponent'; @@ -15,11 +10,12 @@ import { GlobalBlackboardService } from './Services/GlobalBlackboardService'; import { BehaviorTreeLoader } from './loaders/BehaviorTreeLoader'; import { BehaviorTreeAssetType } from './index'; -/** - * Behavior Tree Runtime Module - * 行为树运行时模块 - */ -export class BehaviorTreeRuntimeModule implements IRuntimeModuleLoader { +export interface BehaviorTreeSystemContext extends SystemContext { + behaviorTreeSystem?: BehaviorTreeExecutionSystem; + assetManager?: AssetManager; +} + +class BehaviorTreeRuntimeModule implements IRuntimeModule { private _loaderRegistered = false; registerComponents(registry: typeof ComponentRegistry): void { @@ -36,33 +32,41 @@ export class BehaviorTreeRuntimeModule implements IRuntimeModuleLoader { } createSystems(scene: IScene, context: SystemContext): void { - // 注册行为树加载器到 AssetManager - // Register behavior tree loader to AssetManager - const assetManager = context.assetManager as AssetManager | undefined; - console.log('[BehaviorTreeRuntimeModule] createSystems called, assetManager:', assetManager ? 'exists' : 'null'); + const btContext = context as BehaviorTreeSystemContext; - if (!this._loaderRegistered && assetManager) { - assetManager.registerLoader(BehaviorTreeAssetType, new BehaviorTreeLoader()); + if (!this._loaderRegistered && btContext.assetManager) { + btContext.assetManager.registerLoader(BehaviorTreeAssetType, new BehaviorTreeLoader()); this._loaderRegistered = true; - console.log('[BehaviorTreeRuntimeModule] Registered BehaviorTreeLoader for type:', BehaviorTreeAssetType); } const behaviorTreeSystem = new BehaviorTreeExecutionSystem(Core); - // 设置 AssetManager 引用 - // Set AssetManager reference - if (assetManager) { - behaviorTreeSystem.setAssetManager(assetManager); - console.log('[BehaviorTreeRuntimeModule] Set assetManager on behaviorTreeSystem'); - } else { - console.warn('[BehaviorTreeRuntimeModule] assetManager is null, cannot set on behaviorTreeSystem'); + if (btContext.assetManager) { + behaviorTreeSystem.setAssetManager(btContext.assetManager); } - if (context.isEditor) { + if (btContext.isEditor) { behaviorTreeSystem.enabled = false; } scene.addSystem(behaviorTreeSystem); - context.behaviorTreeSystem = behaviorTreeSystem; + btContext.behaviorTreeSystem = behaviorTreeSystem; } } + +const descriptor: PluginDescriptor = { + id: '@esengine/behavior-tree', + name: 'Behavior Tree', + version: '1.0.0', + description: 'AI behavior tree system', + category: 'ai', + enabledByDefault: false, + isEnginePlugin: true +}; + +export const BehaviorTreePlugin: IPlugin = { + descriptor, + runtimeModule: new BehaviorTreeRuntimeModule() +}; + +export { BehaviorTreeRuntimeModule }; diff --git a/packages/behavior-tree/src/constants.ts b/packages/behavior-tree/src/constants.ts new file mode 100644 index 00000000..5ea246f7 --- /dev/null +++ b/packages/behavior-tree/src/constants.ts @@ -0,0 +1,8 @@ +/** + * Behavior Tree Constants + * 行为树常量 + */ + +// Asset type constant for behavior tree +// 行为树资产类型常量 +export const BehaviorTreeAssetType = 'behaviortree' as const; diff --git a/packages/behavior-tree/src/index.ts b/packages/behavior-tree/src/index.ts index dbc2177d..4e020310 100644 --- a/packages/behavior-tree/src/index.ts +++ b/packages/behavior-tree/src/index.ts @@ -7,9 +7,8 @@ * @packageDocumentation */ -// Asset type constant for behavior tree -// 行为树资产类型常量 -export const BehaviorTreeAssetType = 'behaviortree' as const; +// Constants +export { BehaviorTreeAssetType } from './constants'; // Types export * from './Types/TaskStatus'; @@ -35,8 +34,5 @@ export * from './Services/GlobalBlackboardService'; export type { BlackboardTypeDefinition } from './Blackboard/BlackboardTypes'; export { BlackboardTypes } from './Blackboard/BlackboardTypes'; -// Runtime module (no editor dependencies) -export { BehaviorTreeRuntimeModule } from './BehaviorTreeRuntimeModule'; - -// Plugin (for PluginManager - includes editor dependencies) -export { BehaviorTreePlugin } from './editor/index'; +// Runtime module and plugin +export { BehaviorTreeRuntimeModule, BehaviorTreePlugin, type BehaviorTreeSystemContext } from './BehaviorTreeRuntimeModule'; diff --git a/packages/behavior-tree/src/loaders/BehaviorTreeLoader.ts b/packages/behavior-tree/src/loaders/BehaviorTreeLoader.ts index ee6f2f45..befd7610 100644 --- a/packages/behavior-tree/src/loaders/BehaviorTreeLoader.ts +++ b/packages/behavior-tree/src/loaders/BehaviorTreeLoader.ts @@ -15,7 +15,7 @@ import { Core } from '@esengine/ecs-framework'; import { BehaviorTreeData } from '../execution/BehaviorTreeData'; import { BehaviorTreeAssetManager } from '../execution/BehaviorTreeAssetManager'; import { EditorToBehaviorTreeDataConverter } from '../Serialization/EditorToBehaviorTreeDataConverter'; -import { BehaviorTreeAssetType } from '../index'; +import { BehaviorTreeAssetType } from '../constants'; /** * 行为树资产接口 diff --git a/packages/behavior-tree/src/runtime.ts b/packages/behavior-tree/src/runtime.ts deleted file mode 100644 index 8f89ad7f..00000000 --- a/packages/behavior-tree/src/runtime.ts +++ /dev/null @@ -1,36 +0,0 @@ -/** - * @esengine/behavior-tree Runtime Entry Point - * - * This entry point exports only runtime-related code without any editor dependencies. - * Use this for standalone game runtime builds. - * - * 此入口点仅导出运行时相关代码,不包含任何编辑器依赖。 - * 用于独立游戏运行时构建。 - */ - -// Types -export * from './Types/TaskStatus'; - -// Execution (runtime core) -export * from './execution'; - -// Utilities -export * from './BehaviorTreeStarter'; -export * from './BehaviorTreeBuilder'; - -// Serialization -export * from './Serialization/NodeTemplates'; -export * from './Serialization/BehaviorTreeAsset'; -export * from './Serialization/EditorFormatConverter'; -export * from './Serialization/BehaviorTreeAssetSerializer'; -export * from './Serialization/EditorToBehaviorTreeDataConverter'; - -// Services -export * from './Services/GlobalBlackboardService'; - -// Blackboard types (excluding BlackboardValueType which is already exported from TaskStatus) -export type { BlackboardTypeDefinition } from './Blackboard/BlackboardTypes'; -export { BlackboardTypes } from './Blackboard/BlackboardTypes'; - -// Runtime module -export { BehaviorTreeRuntimeModule } from './BehaviorTreeRuntimeModule'; diff --git a/packages/behavior-tree/tsconfig.build.json b/packages/behavior-tree/tsconfig.build.json new file mode 100644 index 00000000..ba0684d9 --- /dev/null +++ b/packages/behavior-tree/tsconfig.build.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ES2020", + "moduleResolution": "bundler", + "lib": ["ES2020", "DOM"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "declaration": true, + "declarationMap": true, + "jsx": "react-jsx", + "resolveJsonModule": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/behavior-tree/tsconfig.json b/packages/behavior-tree/tsconfig.json index cbca93a9..66b54d18 100644 --- a/packages/behavior-tree/tsconfig.json +++ b/packages/behavior-tree/tsconfig.json @@ -30,7 +30,7 @@ ], "references": [ { "path": "../core" }, - { "path": "../components" }, + { "path": "../engine-core" }, { "path": "../editor-core" }, { "path": "../ui" }, { "path": "../editor-runtime" } diff --git a/packages/behavior-tree/tsup.config.ts b/packages/behavior-tree/tsup.config.ts new file mode 100644 index 00000000..f704a430 --- /dev/null +++ b/packages/behavior-tree/tsup.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'tsup'; +import { runtimeOnlyPreset } from '../build-config/src/presets/plugin-tsup'; + +export default defineConfig({ + ...runtimeOnlyPreset(), + tsconfig: 'tsconfig.build.json' +}); diff --git a/packages/behavior-tree/vite.config.ts b/packages/behavior-tree/vite.config.ts deleted file mode 100644 index 24ab88ae..00000000 --- a/packages/behavior-tree/vite.config.ts +++ /dev/null @@ -1,112 +0,0 @@ -import { defineConfig } from 'vite'; -import { resolve } from 'path'; -import dts from 'vite-plugin-dts'; -import react from '@vitejs/plugin-react'; - -/** - * 自定义插件:将 CSS 转换为自执行的样式注入代码 - * Custom plugin: Convert CSS to self-executing style injection code - * - * 当用户写 `import './styles.css'` 时,这个插件会: - * 1. 在构建时将 CSS 内容转换为 JS 代码 - * 2. JS 代码在模块导入时自动执行,将样式注入到 DOM - * 3. 使用唯一 ID 防止重复注入 - */ -function escapeUnsafeChars(str: string): string { - const charMap: Record = { - '<': '\\u003C', - '>': '\\u003E', - '/': '\\u002F', - '\\': '\\\\', - '\u2028': '\\u2028', - '\u2029': '\\u2029' - }; - return str.replace(/[<>\\/\u2028\u2029]/g, (x) => charMap[x] || x); -} - -function injectCSSPlugin(): unknown { - const cssIdMap = new Map(); - let cssCounter = 0; - - return { - name: 'inject-css-plugin', - enforce: 'post' as const, - generateBundle(_options: unknown, bundle: Record) { - const bundleKeys = Object.keys(bundle); - - // 找到所有 CSS 文件 - const cssFiles = bundleKeys.filter(key => key.endsWith('.css')); - - for (const cssFile of cssFiles) { - const cssChunk = bundle[cssFile]; - if (!cssChunk || !cssChunk.source) continue; - - const cssContent = cssChunk.source; - const styleId = `esengine-behavior-tree-style-${cssCounter++}`; - cssIdMap.set(cssFile, styleId); - - // 生成样式注入代码 - const injectCode = `(function(){if(typeof document!=='undefined'){var s=document.createElement('style');s.id='${styleId}';if(!document.getElementById(s.id)){s.textContent=${escapeUnsafeChars(JSON.stringify(cssContent))};document.head.appendChild(s);}}})();`; - - // 注入到 editor/index.js 或共享 chunk - for (const jsKey of bundleKeys) { - if (!jsKey.endsWith('.js')) continue; - const jsChunk = bundle[jsKey]; - if (!jsChunk || jsChunk.type !== 'chunk' || !jsChunk.code) continue; - - if (jsKey === 'editor/index.js' || jsKey.match(/^index-[^/]+\.js$/)) { - jsChunk.code = injectCode + '\n' + jsChunk.code; - } - } - - // 删除独立的 CSS 文件 - delete bundle[cssFile]; - } - } - }; -} - -export default defineConfig({ - plugins: [ - react(), - dts({ - include: ['src'], - outDir: 'dist', - rollupTypes: false - }), - injectCSSPlugin() - ], - esbuild: { - jsx: 'automatic', - }, - build: { - lib: { - entry: { - index: resolve(__dirname, 'src/index.ts'), - runtime: resolve(__dirname, 'src/runtime.ts'), - 'editor/index': resolve(__dirname, 'src/editor/index.ts') - }, - formats: ['es'], - fileName: (format, entryName) => `${entryName}.js` - }, - rollupOptions: { - external: [ - '@esengine/ecs-framework', - '@esengine/editor-runtime', - 'react', - 'react/jsx-runtime', - 'lucide-react', - 'zustand', - /^@esengine\//, - /^@tauri-apps\// - ], - output: { - exports: 'named', - preserveModules: false - } - }, - target: 'es2020', - minify: false, - sourcemap: true - } -}); diff --git a/packages/blueprint-editor/package.json b/packages/blueprint-editor/package.json new file mode 100644 index 00000000..a3cfd1e0 --- /dev/null +++ b/packages/blueprint-editor/package.json @@ -0,0 +1,49 @@ +{ + "name": "@esengine/blueprint-editor", + "version": "1.0.0", + "description": "Editor support for @esengine/blueprint - visual scripting editor", + "main": "dist/index.js", + "module": "dist/index.js", + "types": "dist/index.d.ts", + "type": "module", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "files": [ + "dist" + ], + "scripts": { + "build": "tsup", + "build:watch": "tsup --watch", + "type-check": "tsc --noEmit", + "clean": "rimraf dist" + }, + "dependencies": { + "@esengine/blueprint": "workspace:*" + }, + "devDependencies": { + "@esengine/ecs-framework": "workspace:*", + "@esengine/engine-core": "workspace:*", + "@esengine/editor-core": "workspace:*", + "@esengine/node-editor": "workspace:*", + "@esengine/build-config": "workspace:*", + "lucide-react": "^0.545.0", + "react": "^18.3.1", + "zustand": "^5.0.8", + "@types/react": "^18.3.12", + "rimraf": "^5.0.5", + "tsup": "^8.0.0", + "typescript": "^5.3.3" + }, + "keywords": [ + "ecs", + "blueprint", + "editor", + "visual-scripting" + ], + "author": "", + "license": "MIT" +} diff --git a/packages/blueprint-editor/src/BlueprintPlugin.ts b/packages/blueprint-editor/src/BlueprintPlugin.ts new file mode 100644 index 00000000..8b7412cb --- /dev/null +++ b/packages/blueprint-editor/src/BlueprintPlugin.ts @@ -0,0 +1,126 @@ +/** + * Blueprint Editor Plugin + * 蓝图编辑器插件 + */ + +import { Core, type ServiceContainer } from '@esengine/ecs-framework'; +import type { IPlugin, PluginDescriptor } from '@esengine/engine-core'; +import type { IEditorModuleLoader, PanelDescriptor, FileActionHandler, FileCreationTemplate } from '@esengine/editor-core'; +import { MessageHub, PanelPosition } from '@esengine/editor-core'; + +// Re-export from @esengine/blueprint for runtime module +import { NodeRegistry, BlueprintVM, createBlueprintSystem } from '@esengine/blueprint'; + +// Store for pending file path +import { useBlueprintEditorStore } from './stores/blueprintEditorStore'; + +// Direct import of panel component (not dynamic import) +import { BlueprintEditorPanel } from './components/BlueprintEditorPanel'; + +/** + * Blueprint Editor Module Implementation + * 蓝图编辑器模块实现 + */ +class BlueprintEditorModuleImpl implements IEditorModuleLoader { + async install(_services: ServiceContainer): Promise { + // Editor module installation + } + + async uninstall(): Promise { + // Cleanup + } + + getPanels(): PanelDescriptor[] { + return [ + { + id: 'blueprint-editor', + title: 'Blueprint Editor', + position: PanelPosition.Center, + icon: 'Workflow', + closable: true, + resizable: true, + order: 50, + component: BlueprintEditorPanel, + isDynamic: true + } + ]; + } + + getFileActionHandlers(): FileActionHandler[] { + return [ + { + // 扩展名不带点号,与 FileActionRegistry.getFileExtension() 保持一致 + // Extensions without dot prefix, consistent with FileActionRegistry.getFileExtension() + extensions: ['blueprint', 'bp'], + onDoubleClick: (filePath: string) => { + // 设置待加载的文件路径到 store + // Set pending file path to store + useBlueprintEditorStore.getState().setPendingFilePath(filePath); + + // 通过 MessageHub 打开蓝图编辑器面板 + // Open blueprint editor panel via MessageHub + const messageHub = Core.services.resolve(MessageHub); + if (messageHub) { + messageHub.publish('dynamic-panel:open', { + panelId: 'blueprint-editor', + title: `Blueprint - ${filePath.split(/[\\/]/).pop()}` + }); + } + } + } + ]; + } + + getFileCreationTemplates(): FileCreationTemplate[] { + return [ + { + id: 'blueprint', + label: 'Blueprint', + // 扩展名不带点号,FileTree 会自动添加点号 + // Extension without dot, FileTree will add the dot automatically + extension: 'blueprint', + icon: 'Workflow', + getContent: (fileName: string) => { + const name = fileName.replace(/\.blueprint$/i, '') || 'NewBlueprint'; + return JSON.stringify({ + version: '1.0.0', + name, + nodes: [], + connections: [], + variables: [] + }, null, 2); + } + } + ]; + } +} + +const descriptor: PluginDescriptor = { + id: '@esengine/blueprint', + name: 'Blueprint', + version: '1.0.0', + description: 'Visual scripting system for ECS Framework', + category: 'scripting', + enabledByDefault: false, + isEnginePlugin: true, + canContainContent: true, + modules: [ + { name: 'Runtime', type: 'runtime', loadingPhase: 'default' }, + { name: 'Editor', type: 'editor', loadingPhase: 'postDefault' } + ] +}; + +/** + * Complete Blueprint plugin with both runtime and editor modules + * 完整的蓝图插件,包含运行时和编辑器模块 + */ +export const BlueprintPlugin: IPlugin = { + descriptor, + editorModule: new BlueprintEditorModuleImpl() +}; + +// Also export the editor module instance for direct use +export const BlueprintEditorModule = new BlueprintEditorModuleImpl(); + +// Re-export useful items +export { NodeRegistry, BlueprintVM, createBlueprintSystem }; diff --git a/packages/blueprint/src/editor/components/BlueprintCanvas.tsx b/packages/blueprint-editor/src/components/BlueprintCanvas.tsx similarity index 97% rename from packages/blueprint/src/editor/components/BlueprintCanvas.tsx rename to packages/blueprint-editor/src/components/BlueprintCanvas.tsx index 96b0e517..1b1c07e0 100644 --- a/packages/blueprint/src/editor/components/BlueprintCanvas.tsx +++ b/packages/blueprint-editor/src/components/BlueprintCanvas.tsx @@ -17,9 +17,8 @@ import { type PinCategory } from '@esengine/node-editor'; import { useBlueprintEditorStore } from '../stores/blueprintEditorStore'; -import { NodeRegistry } from '../../runtime/NodeRegistry'; -import type { BlueprintNode, BlueprintConnection, BlueprintNodeTemplate } from '../../types/nodes'; -import type { BlueprintPinDefinition } from '../../types/pins'; +import { NodeRegistry } from '@esengine/blueprint'; +import type { BlueprintNode, BlueprintConnection, BlueprintNodeTemplate, BlueprintPinDefinition } from '@esengine/blueprint'; interface ContextMenuState { isOpen: boolean; @@ -221,7 +220,9 @@ export const BlueprintCanvas: React.FC = () => { } } - return new Graph('blueprint', blueprint.metadata.name, graphNodes, graphConnections); + // 安全访问 metadata.name,兼容旧格式文件 + const blueprintName = blueprint.metadata?.name || (blueprint as any).name || 'Blueprint'; + return new Graph('blueprint', blueprintName, graphNodes, graphConnections); }, [blueprint]); // Handle graph changes diff --git a/packages/blueprint-editor/src/components/BlueprintEditorPanel.tsx b/packages/blueprint-editor/src/components/BlueprintEditorPanel.tsx new file mode 100644 index 00000000..64658cd0 --- /dev/null +++ b/packages/blueprint-editor/src/components/BlueprintEditorPanel.tsx @@ -0,0 +1,90 @@ +/** + * Blueprint Editor Panel - Main panel for blueprint editing + * 蓝图编辑器面板 - 蓝图编辑的主面板 + */ + +import React, { useEffect } from 'react'; +import { Core } from '@esengine/ecs-framework'; +import { IFileSystemService, type IFileSystem } from '@esengine/editor-core'; +import { BlueprintCanvas } from './BlueprintCanvas'; +import { useBlueprintEditorStore } from '../stores/blueprintEditorStore'; +import type { BlueprintAsset } from '@esengine/blueprint'; + +// Import blueprint package to register nodes +// 导入蓝图包以注册节点 +import '@esengine/blueprint'; + +/** + * Panel container styles + * 面板容器样式 + */ +const panelStyles: React.CSSProperties = { + width: '100%', + height: '100%', + display: 'flex', + flexDirection: 'column', + backgroundColor: '#1a1a2e', + color: '#fff', + overflow: 'hidden' +}; + +/** + * Blueprint Editor Panel Component + * 蓝图编辑器面板组件 + */ +export const BlueprintEditorPanel: React.FC = () => { + const { + blueprint, + pendingFilePath, + createNewBlueprint, + loadBlueprint, + setPendingFilePath + } = useBlueprintEditorStore(); + + // Load blueprint from pending file path + // 从待加载的文件路径加载蓝图 + useEffect(() => { + if (!pendingFilePath) return; + + const loadBlueprintFile = async () => { + try { + const fileSystem = Core.services.tryResolve(IFileSystemService) as IFileSystem | null; + if (!fileSystem) { + console.error('[BlueprintEditorPanel] FileSystem service not available'); + setPendingFilePath(null); + createNewBlueprint('New Blueprint'); + return; + } + + const content = await fileSystem.readFile(pendingFilePath); + const asset = JSON.parse(content) as BlueprintAsset; + + loadBlueprint(asset, pendingFilePath); + setPendingFilePath(null); + + console.log('[BlueprintEditorPanel] Loaded blueprint from file:', pendingFilePath); + } catch (error) { + console.error('[BlueprintEditorPanel] Failed to load blueprint file:', error); + setPendingFilePath(null); + // 加载失败时创建新蓝图 + createNewBlueprint('New Blueprint'); + } + }; + + loadBlueprintFile(); + }, [pendingFilePath, loadBlueprint, setPendingFilePath, createNewBlueprint]); + + // Create a default blueprint if none exists and no pending file + // 如果不存在蓝图且没有待加载文件,则创建默认蓝图 + useEffect(() => { + if (!blueprint && !pendingFilePath) { + createNewBlueprint('New Blueprint'); + } + }, [blueprint, pendingFilePath, createNewBlueprint]); + + return ( +
+ +
+ ); +}; diff --git a/packages/blueprint/src/editor/components/index.ts b/packages/blueprint-editor/src/components/index.ts similarity index 100% rename from packages/blueprint/src/editor/components/index.ts rename to packages/blueprint-editor/src/components/index.ts diff --git a/packages/blueprint/src/editor/index.ts b/packages/blueprint-editor/src/index.ts similarity index 100% rename from packages/blueprint/src/editor/index.ts rename to packages/blueprint-editor/src/index.ts diff --git a/packages/blueprint/src/editor/stores/blueprintEditorStore.ts b/packages/blueprint-editor/src/stores/blueprintEditorStore.ts similarity index 84% rename from packages/blueprint/src/editor/stores/blueprintEditorStore.ts rename to packages/blueprint-editor/src/stores/blueprintEditorStore.ts index 66d5221b..e840075e 100644 --- a/packages/blueprint/src/editor/stores/blueprintEditorStore.ts +++ b/packages/blueprint-editor/src/stores/blueprintEditorStore.ts @@ -4,8 +4,8 @@ */ import { create } from 'zustand'; -import { BlueprintAsset, createEmptyBlueprint } from '../../types/blueprint'; -import { BlueprintNode, BlueprintConnection } from '../../types/nodes'; +import { createEmptyBlueprint } from '@esengine/blueprint'; +import type { BlueprintAsset, BlueprintNode, BlueprintConnection } from '@esengine/blueprint'; /** * Blueprint editor state interface @@ -30,6 +30,9 @@ interface BlueprintEditorState { /** Whether the blueprint has unsaved changes (是否有未保存的更改) */ isDirty: boolean; + /** Pending file path to load when panel opens (面板打开时待加载的文件路径) */ + pendingFilePath: string | null; + /** Current file path if saved (当前文件路径) */ filePath: string | null; @@ -75,6 +78,9 @@ interface BlueprintEditorState { /** Mark as clean (标记为未修改) */ markClean: () => void; + + /** Set pending file path (设置待加载的文件路径) */ + setPendingFilePath: (path: string | null) => void; } /** @@ -85,6 +91,19 @@ function generateId(): string { return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; } +/** + * 安全获取或创建 metadata + * Safely get or create metadata + */ +function getUpdatedMetadata(blueprint: BlueprintAsset): BlueprintAsset['metadata'] { + const existing = blueprint.metadata || { + name: (blueprint as any).name || 'Blueprint', + createdAt: Date.now(), + modifiedAt: Date.now() + }; + return { ...existing, modifiedAt: Date.now() }; +} + /** * Blueprint editor store * 蓝图编辑器状态存储 @@ -96,6 +115,7 @@ export const useBlueprintEditorStore = create((set, get) = panOffset: { x: 0, y: 0 }, zoom: 1, isDirty: false, + pendingFilePath: null, filePath: null, createNewBlueprint: (name: string) => { @@ -130,7 +150,7 @@ export const useBlueprintEditorStore = create((set, get) = blueprint: { ...blueprint, nodes: [...blueprint.nodes, newNode], - metadata: { ...blueprint.metadata, modifiedAt: Date.now() } + metadata: getUpdatedMetadata(blueprint) }, isDirty: true }); @@ -147,7 +167,7 @@ export const useBlueprintEditorStore = create((set, get) = connections: blueprint.connections.filter( c => c.fromNodeId !== nodeId && c.toNodeId !== nodeId ), - metadata: { ...blueprint.metadata, modifiedAt: Date.now() } + metadata: getUpdatedMetadata(blueprint) }, selectedNodeIds: get().selectedNodeIds.filter(id => id !== nodeId), isDirty: true @@ -164,7 +184,7 @@ export const useBlueprintEditorStore = create((set, get) = nodes: blueprint.nodes.map(n => n.id === nodeId ? { ...n, position: { x, y } } : n ), - metadata: { ...blueprint.metadata, modifiedAt: Date.now() } + metadata: getUpdatedMetadata(blueprint) }, isDirty: true }); @@ -180,7 +200,7 @@ export const useBlueprintEditorStore = create((set, get) = nodes: blueprint.nodes.map(n => n.id === nodeId ? { ...n, data: { ...n.data, ...data } } : n ), - metadata: { ...blueprint.metadata, modifiedAt: Date.now() } + metadata: getUpdatedMetadata(blueprint) }, isDirty: true }); @@ -210,7 +230,7 @@ export const useBlueprintEditorStore = create((set, get) = blueprint: { ...blueprint, connections: newConnections, - metadata: { ...blueprint.metadata, modifiedAt: Date.now() } + metadata: getUpdatedMetadata(blueprint) }, isDirty: true }); @@ -224,7 +244,7 @@ export const useBlueprintEditorStore = create((set, get) = blueprint: { ...blueprint, connections: blueprint.connections.filter(c => c.id !== connectionId), - metadata: { ...blueprint.metadata, modifiedAt: Date.now() } + metadata: getUpdatedMetadata(blueprint) }, isDirty: true }); @@ -252,5 +272,9 @@ export const useBlueprintEditorStore = create((set, get) = markClean: () => { set({ isDirty: false }); + }, + + setPendingFilePath: (path: string | null) => { + set({ pendingFilePath: path }); } })); diff --git a/packages/blueprint/src/editor/stores/index.ts b/packages/blueprint-editor/src/stores/index.ts similarity index 100% rename from packages/blueprint/src/editor/stores/index.ts rename to packages/blueprint-editor/src/stores/index.ts diff --git a/packages/blueprint-editor/tsconfig.build.json b/packages/blueprint-editor/tsconfig.build.json new file mode 100644 index 00000000..ba0684d9 --- /dev/null +++ b/packages/blueprint-editor/tsconfig.build.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ES2020", + "moduleResolution": "bundler", + "lib": ["ES2020", "DOM"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "declaration": true, + "declarationMap": true, + "jsx": "react-jsx", + "resolveJsonModule": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/blueprint-editor/tsconfig.json b/packages/blueprint-editor/tsconfig.json new file mode 100644 index 00000000..d099ddd8 --- /dev/null +++ b/packages/blueprint-editor/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src", + "declaration": true, + "jsx": "react-jsx" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/blueprint-editor/tsup.config.ts b/packages/blueprint-editor/tsup.config.ts new file mode 100644 index 00000000..b4f49f5d --- /dev/null +++ b/packages/blueprint-editor/tsup.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'tsup'; +import { editorOnlyPreset } from '../build-config/src/presets/plugin-tsup'; + +export default defineConfig({ + ...editorOnlyPreset(), + tsconfig: 'tsconfig.build.json' +}); diff --git a/packages/blueprint/package.json b/packages/blueprint/package.json index 9ff7b437..748b7d01 100644 --- a/packages/blueprint/package.json +++ b/packages/blueprint/package.json @@ -1,85 +1,52 @@ { - "name": "@esengine/blueprint", - "version": "1.0.0", - "description": "Visual scripting system for ECS Framework", - "main": "dist/index.js", - "module": "dist/index.js", - "types": "dist/index.d.ts", - "type": "module", - "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.js" + "name": "@esengine/blueprint", + "version": "1.0.0", + "description": "Visual scripting system for ECS Framework", + "main": "dist/index.js", + "module": "dist/index.js", + "types": "dist/index.d.ts", + "type": "module", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } }, - "./editor": { - "types": "./dist/editor/index.d.ts", - "import": "./dist/editor/index.js" + "files": [ + "dist" + ], + "scripts": { + "clean": "rimraf dist tsconfig.tsbuildinfo", + "build": "tsup", + "build:watch": "tsup --watch", + "type-check": "tsc --noEmit" }, - "./plugin.json": "./plugin.json" - }, - "files": [ - "dist", - "plugin.json" - ], - "scripts": { - "clean": "rimraf dist tsconfig.tsbuildinfo", - "build": "vite build", - "build:watch": "vite build --watch", - "type-check": "tsc --noEmit" - }, - "keywords": [ - "ecs", - "blueprint", - "visual-scripting", - "game-engine", - "node-editor" - ], - "author": "yhh", - "license": "MIT", - "peerDependencies": { - "@esengine/ecs-framework": ">=2.0.0", - "@esengine/editor-runtime": "workspace:*", - "@esengine/node-editor": "workspace:*", - "lucide-react": "^0.545.0", - "react": "^18.3.1", - "zustand": "^4.5.2" - }, - "peerDependenciesMeta": { - "@esengine/editor-runtime": { - "optional": true + "keywords": [ + "ecs", + "blueprint", + "visual-scripting", + "game-engine" + ], + "author": "yhh", + "license": "MIT", + "devDependencies": { + "@esengine/ecs-framework": "workspace:*", + "@esengine/build-config": "workspace:*", + "@types/node": "^20.19.17", + "rimraf": "^5.0.0", + "tsup": "^8.0.0", + "typescript": "^5.8.3" }, - "@esengine/node-editor": { - "optional": true + "dependencies": { + "tslib": "^2.8.1" }, - "lucide-react": { - "optional": true + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" }, - "react": { - "optional": true - }, - "zustand": { - "optional": true + "repository": { + "type": "git", + "url": "https://github.com/esengine/ecs-framework.git", + "directory": "packages/blueprint" } - }, - "devDependencies": { - "@types/node": "^20.19.17", - "@types/react": "^18.3.12", - "@vitejs/plugin-react": "^4.7.0", - "rimraf": "^5.0.0", - "typescript": "^5.8.3", - "vite": "^6.0.7", - "vite-plugin-dts": "^3.7.0" - }, - "dependencies": { - "tslib": "^2.8.1" - }, - "publishConfig": { - "access": "public", - "registry": "https://registry.npmjs.org/" - }, - "repository": { - "type": "git", - "url": "https://github.com/esengine/ecs-framework.git", - "directory": "packages/blueprint" - } } diff --git a/packages/blueprint/src/editor/BlueprintPlugin.ts b/packages/blueprint/src/editor/BlueprintPlugin.ts deleted file mode 100644 index 8add4a77..00000000 --- a/packages/blueprint/src/editor/BlueprintPlugin.ts +++ /dev/null @@ -1,180 +0,0 @@ -/** - * Blueprint Editor Plugin - Integrates blueprint editor with the editor - * 蓝图编辑器插件 - 将蓝图编辑器与编辑器集成 - */ - -import { - type ServiceContainer, - type IPluginLoader, - type IEditorModuleLoader, - type PluginDescriptor, - type PanelDescriptor, - type MenuItemDescriptor, - type FileActionHandler, - type FileCreationTemplate, - PanelPosition, - FileSystem, - createLogger, - MessageHub, - IMessageHub -} from '@esengine/editor-runtime'; -import { BlueprintEditorPanel } from './components/BlueprintEditorPanel'; -import { useBlueprintEditorStore } from './stores/blueprintEditorStore'; -import { createEmptyBlueprint, validateBlueprintAsset } from '../types/blueprint'; - -const logger = createLogger('BlueprintEditorModule'); - -/** - * Blueprint 编辑器模块 - * Blueprint editor module - */ -class BlueprintEditorModule implements IEditorModuleLoader { - private services?: ServiceContainer; - - async install(services: ServiceContainer): Promise { - this.services = services; - logger.info('Blueprint editor module installed'); - } - - async uninstall(): Promise { - logger.info('Blueprint editor module uninstalled'); - } - - getPanels(): PanelDescriptor[] { - return [ - { - id: 'panel-blueprint-editor', - title: 'Blueprint Editor', - position: PanelPosition.Center, - defaultSize: 800, - resizable: true, - closable: true, - icon: 'Workflow', - order: 20, - isDynamic: true, - component: BlueprintEditorPanel - } - ]; - } - - getMenuItems(): MenuItemDescriptor[] { - return [ - { - id: 'blueprint-new', - label: 'New Blueprint', - parentId: 'file', - shortcut: 'Ctrl+Shift+B', - execute: () => { - useBlueprintEditorStore.getState().createNewBlueprint('New Blueprint'); - } - }, - { - id: 'view-blueprint-editor', - label: 'Blueprint Editor', - parentId: 'view', - shortcut: 'Ctrl+B' - } - ]; - } - - getFileActionHandlers(): FileActionHandler[] { - const services = this.services; - return [ - { - extensions: ['bp'], - onDoubleClick: async (filePath: string) => { - try { - // 使用 FileSystem API 读取文件 - const content = await FileSystem.readTextFile(filePath); - const data = JSON.parse(content); - - if (validateBlueprintAsset(data)) { - useBlueprintEditorStore.getState().loadBlueprint(data, filePath); - logger.info('Loaded blueprint:', filePath); - - // 打开蓝图编辑器面板 - if (services) { - const messageHub = services.resolve(IMessageHub); - if (messageHub) { - const fileName = filePath.split(/[\\/]/).pop() || 'Blueprint'; - messageHub.publish('dynamic-panel:open', { - panelId: 'panel-blueprint-editor', - title: `Blueprint - ${fileName}` - }); - } - } - } else { - logger.error('Invalid blueprint file:', filePath); - } - } catch (error) { - logger.error('Failed to load blueprint:', error); - } - } - } - ]; - } - - getFileCreationTemplates(): FileCreationTemplate[] { - return [ - { - id: 'create-blueprint', - label: 'Blueprint', - extension: 'bp', - icon: 'Workflow', - category: 'scripting', - getContent: (fileName: string) => { - const name = fileName.replace('.bp', ''); - const blueprint = createEmptyBlueprint(name); - return JSON.stringify(blueprint, null, 2); - } - } - ]; - } - - async onEditorReady(): Promise { - logger.info('Editor ready'); - } - - async onProjectOpen(_projectPath: string): Promise { - logger.info('Project opened'); - } - - async onProjectClose(): Promise { - useBlueprintEditorStore.getState().createNewBlueprint('New Blueprint'); - logger.info('Project closed'); - } -} - -/** - * Plugin descriptor - * 插件描述符 - */ -const descriptor: PluginDescriptor = { - id: '@esengine/blueprint', - name: 'Blueprint Visual Scripting', - version: '1.0.0', - description: 'Visual scripting system for creating game logic without code', - category: 'scripting', - icon: 'Workflow', - enabledByDefault: true, - canContainContent: true, - isEnginePlugin: true, - isCore: false, - modules: [ - { - name: 'BlueprintEditor', - type: 'editor', - loadingPhase: 'default', - panels: ['panel-blueprint-editor'] - } - ] -}; - -/** - * Blueprint Plugin Export - * 蓝图插件导出 - */ -export const BlueprintPlugin: IPluginLoader = { - descriptor, - editorModule: new BlueprintEditorModule() -}; diff --git a/packages/blueprint/src/editor/components/BlueprintEditorPanel.tsx b/packages/blueprint/src/editor/components/BlueprintEditorPanel.tsx deleted file mode 100644 index 18ef7c0c..00000000 --- a/packages/blueprint/src/editor/components/BlueprintEditorPanel.tsx +++ /dev/null @@ -1,48 +0,0 @@ -/** - * Blueprint Editor Panel - Main panel for blueprint editing - * 蓝图编辑器面板 - 蓝图编辑的主面板 - */ - -import React, { useEffect } from 'react'; -import { BlueprintCanvas } from './BlueprintCanvas'; -import { useBlueprintEditorStore } from '../stores/blueprintEditorStore'; - -// Import nodes to register them -// 导入节点以注册它们 -import '../../nodes'; - -/** - * Panel container styles - * 面板容器样式 - */ -const panelStyles: React.CSSProperties = { - width: '100%', - height: '100%', - display: 'flex', - flexDirection: 'column', - backgroundColor: '#1a1a2e', - color: '#fff', - overflow: 'hidden' -}; - -/** - * Blueprint Editor Panel Component - * 蓝图编辑器面板组件 - */ -export const BlueprintEditorPanel: React.FC = () => { - const { blueprint, createNewBlueprint } = useBlueprintEditorStore(); - - // Create a default blueprint if none exists - // 如果不存在则创建默认蓝图 - useEffect(() => { - if (!blueprint) { - createNewBlueprint('New Blueprint'); - } - }, [blueprint, createNewBlueprint]); - - return ( -
- -
- ); -}; diff --git a/packages/blueprint/tsconfig.build.json b/packages/blueprint/tsconfig.build.json new file mode 100644 index 00000000..ba0684d9 --- /dev/null +++ b/packages/blueprint/tsconfig.build.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ES2020", + "moduleResolution": "bundler", + "lib": ["ES2020", "DOM"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "declaration": true, + "declarationMap": true, + "jsx": "react-jsx", + "resolveJsonModule": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/blueprint/tsup.config.ts b/packages/blueprint/tsup.config.ts new file mode 100644 index 00000000..f704a430 --- /dev/null +++ b/packages/blueprint/tsup.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'tsup'; +import { runtimeOnlyPreset } from '../build-config/src/presets/plugin-tsup'; + +export default defineConfig({ + ...runtimeOnlyPreset(), + tsconfig: 'tsconfig.build.json' +}); diff --git a/packages/blueprint/vite.config.ts b/packages/blueprint/vite.config.ts deleted file mode 100644 index 5fb95845..00000000 --- a/packages/blueprint/vite.config.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { defineConfig } from 'vite'; -import { resolve } from 'path'; -import dts from 'vite-plugin-dts'; -import react from '@vitejs/plugin-react'; - -/** - * 自定义插件:将 CSS 转换为自执行的样式注入代码 - * Custom plugin: Convert CSS to self-executing style injection code - */ -function escapeUnsafeChars(str: string): string { - const charMap: Record = { - '<': '\\u003C', - '>': '\\u003E', - '/': '\\u002F', - '\\': '\\\\', - '\u2028': '\\u2028', - '\u2029': '\\u2029' - }; - return str.replace(/[<>\\/\u2028\u2029]/g, (x) => charMap[x] || x); -} - -function injectCSSPlugin(): unknown { - const cssIdMap = new Map(); - let cssCounter = 0; - - return { - name: 'inject-css-plugin', - enforce: 'post' as const, - generateBundle(_options: unknown, bundle: Record) { - const bundleKeys = Object.keys(bundle); - - // 找到所有 CSS 文件 - const cssFiles = bundleKeys.filter(key => key.endsWith('.css')); - - for (const cssFile of cssFiles) { - const cssChunk = bundle[cssFile]; - if (!cssChunk || !cssChunk.source) continue; - - const cssContent = cssChunk.source; - const styleId = `esengine-blueprint-style-${cssCounter++}`; - cssIdMap.set(cssFile, styleId); - - // 生成样式注入代码 - const injectCode = `(function(){if(typeof document!=='undefined'){var s=document.createElement('style');s.id='${styleId}';if(!document.getElementById(s.id)){s.textContent=${escapeUnsafeChars(JSON.stringify(cssContent))};document.head.appendChild(s);}}})();`; - - // 注入到 editor/index.js 或共享 chunk - for (const jsKey of bundleKeys) { - if (!jsKey.endsWith('.js')) continue; - const jsChunk = bundle[jsKey]; - if (!jsChunk || jsChunk.type !== 'chunk' || !jsChunk.code) continue; - - if (jsKey === 'editor/index.js' || jsKey.match(/^index-[^/]+\.js$/)) { - jsChunk.code = injectCode + '\n' + jsChunk.code; - } - } - - // 删除独立的 CSS 文件 - delete bundle[cssFile]; - } - } - }; -} - -export default defineConfig({ - plugins: [ - react(), - dts({ - include: ['src'], - outDir: 'dist', - rollupTypes: false - }), - injectCSSPlugin() - ], - esbuild: { - jsx: 'automatic', - }, - build: { - lib: { - entry: { - index: resolve(__dirname, 'src/index.ts'), - 'editor/index': resolve(__dirname, 'src/editor/index.ts') - }, - formats: ['es'], - fileName: (_format, entryName) => `${entryName}.js` - }, - rollupOptions: { - external: [ - '@esengine/ecs-framework', - '@esengine/editor-runtime', - 'react', - 'react/jsx-runtime', - 'lucide-react', - 'zustand', - /^@esengine\//, - /^@tauri-apps\// - ], - output: { - exports: 'named', - preserveModules: false - } - }, - target: 'es2020', - minify: false, - sourcemap: true - } -}); diff --git a/packages/build-config/README.md b/packages/build-config/README.md new file mode 100644 index 00000000..c71438e9 --- /dev/null +++ b/packages/build-config/README.md @@ -0,0 +1,215 @@ +# @esengine/build-config + +ES Engine 统一构建配置包,提供标准化的 Vite 配置预设和共享插件。 + +## 快速开始 + +### 创建新包 + +使用脚手架工具快速创建新包: + +```bash +# 交互式创建 +node scripts/create-package.mjs + +# 或指定参数 +node scripts/create-package.mjs my-plugin --type plugin +``` + +### 包类型 + +| 类型 | 说明 | 示例 | +|------|------|------| +| `runtime-only` | 纯运行时库,不含编辑器代码 | core, math, components | +| `plugin` | 插件包,同时有 runtime 和 editor 入口 | ui, tilemap, behavior-tree | +| `editor-only` | 纯编辑器包,仅用于编辑器 | editor-core, node-editor | + +## 使用预设 + +### 1. runtime-only(纯运行时包) + +```typescript +// vite.config.ts +import { runtimeOnlyPreset } from '@esengine/build-config/presets'; + +export default runtimeOnlyPreset({ + root: __dirname +}); +``` + +目录结构: +``` +packages/my-lib/ +├── src/ +│ └── index.ts # 主入口 +├── vite.config.ts +└── package.json +``` + +### 2. plugin(插件包) + +```typescript +// vite.config.ts +import { pluginPreset } from '@esengine/build-config/presets'; + +export default pluginPreset({ + root: __dirname, + hasCSS: true // 如果有 CSS 文件 +}); +``` + +目录结构: +``` +packages/my-plugin/ +├── src/ +│ ├── index.ts # 主入口(导出全部) +│ ├── runtime.ts # 运行时入口(不含 React!) +│ ├── MyRuntimeModule.ts +│ └── editor/ +│ ├── index.ts # 编辑器模块 +│ └── MyPlugin.ts +├── plugin.json # 插件描述文件 +├── vite.config.ts +└── package.json +``` + +生成的 exports: +```json +{ + ".": "./dist/index.js", + "./runtime": "./dist/runtime.js", + "./editor": "./dist/editor/index.js", + "./plugin.json": "./plugin.json" +} +``` + +### 3. editor-only(纯编辑器包) + +```typescript +// vite.config.ts +import { editorOnlyPreset } from '@esengine/build-config/presets'; + +export default editorOnlyPreset({ + root: __dirname, + hasReact: true, + hasCSS: true +}); +``` + +## 共享插件 + +### CSS 注入插件 + +将 CSS 内联到 JS 中,避免单独的 CSS 文件: + +```typescript +import { cssInjectPlugin } from '@esengine/build-config/plugins'; + +export default defineConfig({ + plugins: [cssInjectPlugin()] +}); +``` + +### 阻止编辑器代码泄漏 + +在运行时构建中检测并阻止编辑器代码被打包: + +```typescript +import { blockEditorPlugin } from '@esengine/build-config/plugins'; + +export default defineConfig({ + plugins: [ + blockEditorPlugin({ bIsRuntimeBuild: true }) + ] +}); +``` + +## Runtime vs Editor 分离规则 + +### ✅ runtime.ts 中可以: +- 导入 @esengine/ecs-framework +- 导入 @esengine/ecs-components +- 导入其他包的 `/runtime` 路径 + +### ❌ runtime.ts 中不能: +- 导入 `react`、`react-dom` +- 导入 `@esengine/editor-core` +- 导入 `lucide-react` 等 UI 库 +- 导入任何包的 `/editor` 路径 + +### 示例 + +```typescript +// ✅ 正确 +import { Core } from '@esengine/ecs-framework'; +import { UIRuntimeModule } from '@esengine/ui/runtime'; + +// ❌ 错误 - 会把编辑器代码打包进来 +import { UIPlugin } from '@esengine/ui'; // 主入口包含编辑器 +import { UIPlugin } from '@esengine/ui/editor'; // 直接导入编辑器 +import React from 'react'; // React 不应在运行时 +``` + +## 迁移现有包 + +### 从 Rollup 迁移到 Vite 预设 + +1. 安装依赖: +```bash +pnpm add -D @esengine/build-config vite vite-plugin-dts +``` + +2. 替换 `rollup.config.js` 为 `vite.config.ts`: +```typescript +import { pluginPreset } from '@esengine/build-config/presets'; + +export default pluginPreset({ + root: __dirname, + hasCSS: true +}); +``` + +3. 更新 `package.json` 的 scripts: +```json +{ + "scripts": { + "build": "vite build", + "build:watch": "vite build --watch" + } +} +``` + +4. 删除旧的 rollup 配置和依赖。 + +## API 参考 + +### runtimeOnlyPreset(options) + +| 选项 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| root | string | - | 包根目录(必填) | +| entry | string | 'src/index.ts' | 入口文件 | +| external | (string\|RegExp)[] | [] | 额外的外部依赖 | +| viteConfig | Partial | {} | 额外的 Vite 配置 | + +### pluginPreset(options) + +| 选项 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| root | string | - | 包根目录(必填) | +| entries.main | string | 'src/index.ts' | 主入口 | +| entries.runtime | string | 'src/runtime.ts' | 运行时入口 | +| entries.editor | string | 'src/editor/index.ts' | 编辑器入口 | +| hasCSS | boolean | false | 是否包含 CSS | +| hasPluginJson | boolean | true | 是否导出 plugin.json | +| external | (string\|RegExp)[] | [] | 额外的外部依赖 | + +### editorOnlyPreset(options) + +| 选项 | 类型 | 默认值 | 说明 | +|------|------|--------|------| +| root | string | - | 包根目录(必填) | +| entry | string | 'src/index.ts' | 入口文件 | +| hasReact | boolean | true | 是否包含 React | +| hasCSS | boolean | false | 是否包含 CSS | +| external | (string\|RegExp)[] | [] | 额外的外部依赖 | diff --git a/packages/build-config/package.json b/packages/build-config/package.json new file mode 100644 index 00000000..831a2a36 --- /dev/null +++ b/packages/build-config/package.json @@ -0,0 +1,39 @@ +{ + "name": "@esengine/build-config", + "version": "1.0.0", + "description": "Shared build configuration for ES Engine packages", + "type": "module", + "main": "src/index.ts", + "exports": { + ".": "./src/index.ts", + "./presets": "./src/presets/index.ts", + "./presets/tsup": "./src/presets/plugin-tsup.ts", + "./plugins": "./src/plugins/index.ts" + }, + "scripts": { + "type-check": "tsc --noEmit" + }, + "keywords": [ + "build", + "tsup", + "config" + ], + "author": "yhh", + "license": "MIT", + "devDependencies": { + "@vitejs/plugin-react": "^4.3.4", + "tsup": "^8.0.0", + "typescript": "^5.8.3", + "vite": "^6.3.5", + "vite-plugin-dts": "^4.5.4" + }, + "peerDependencies": { + "tsup": "^8.0.0", + "vite": "^6.0.0" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } +} diff --git a/packages/build-config/src/index.ts b/packages/build-config/src/index.ts new file mode 100644 index 00000000..1b9f9512 --- /dev/null +++ b/packages/build-config/src/index.ts @@ -0,0 +1,90 @@ +/** + * @esengine/build-config + * + * 统一构建配置包,提供标准化的 Vite 配置预设和共享插件 + * Unified build configuration with standardized Vite presets and shared plugins + * + * @example + * ```typescript + * // 1. 纯运行时包 (core, math, components) + * import { runtimeOnlyPreset } from '@esengine/build-config/presets'; + * export default runtimeOnlyPreset({ root: __dirname }); + * + * // 2. 插件包 (ui, tilemap, behavior-tree) + * import { pluginPreset } from '@esengine/build-config/presets'; + * export default pluginPreset({ + * root: __dirname, + * hasCSS: true + * }); + * + * // 3. 纯编辑器包 (editor-core, node-editor) + * import { editorOnlyPreset } from '@esengine/build-config/presets'; + * export default editorOnlyPreset({ + * root: __dirname, + * hasReact: true + * }); + * ``` + * + * ## 包类型说明 + * + * | 类型 | 说明 | 示例 | + * |------|------|------| + * | RuntimeOnly | 纯运行时库,不含编辑器代码 | core, math, components | + * | Plugin | 插件包,同时有 runtime 和 editor 入口 | ui, tilemap, behavior-tree | + * | EditorOnly | 纯编辑器包,仅用于编辑器 | editor-core, node-editor | + * + * ## 目录结构约定 + * + * ### RuntimeOnly 包 + * ``` + * packages/my-lib/ + * ├── src/ + * │ └── index.ts # 主入口 + * ├── vite.config.ts + * └── package.json + * ``` + * + * ### Plugin 包 + * ``` + * packages/my-plugin/ + * ├── src/ + * │ ├── index.ts # 主入口(编辑器环境) + * │ ├── runtime.ts # 运行时入口(不含 React) + * │ └── editor/ + * │ └── index.ts # 编辑器模块 + * ├── plugin.json # 插件描述文件 + * ├── vite.config.ts + * └── package.json + * ``` + * + * ### EditorOnly 包 + * ``` + * packages/my-editor-tool/ + * ├── src/ + * │ └── index.ts # 主入口 + * ├── vite.config.ts + * └── package.json + * ``` + */ + +// Types +export { EPackageType, STANDARD_EXTERNALS, EDITOR_ONLY_EXTERNALS } from './types'; +export type { PackageBuildConfig } from './types'; + +// Presets +export { + runtimeOnlyPreset, + pluginPreset, + standaloneRuntimeConfig, + editorOnlyPreset +} from './presets'; +export type { + RuntimeOnlyOptions, + PluginPackageOptions, + StandaloneRuntimeOptions, + EditorOnlyOptions +} from './presets'; + +// Plugins +export { cssInjectPlugin, blockEditorPlugin } from './plugins'; +export type { BlockEditorOptions } from './plugins'; diff --git a/packages/build-config/src/plugins/block-editor.ts b/packages/build-config/src/plugins/block-editor.ts new file mode 100644 index 00000000..8fac291f --- /dev/null +++ b/packages/build-config/src/plugins/block-editor.ts @@ -0,0 +1,100 @@ +/** + * Block Editor Plugin + * 阻止编辑器代码泄漏插件 + * + * 在运行时构建中检测并阻止编辑器代码被打包 + * Detects and blocks editor code from being bundled in runtime builds + */ + +import type { Plugin } from 'vite'; + +export interface BlockEditorOptions { + /** 是否为运行时构建 */ + bIsRuntimeBuild: boolean; + /** 要阻止的模块模式 */ + blockedPatterns?: (string | RegExp)[]; + /** 是否只警告而不报错 */ + bWarnOnly?: boolean; +} + +const DEFAULT_BLOCKED_PATTERNS: (string | RegExp)[] = [ + // React 相关 + /^react$/, + /^react-dom$/, + /^react\/jsx-runtime$/, + /^lucide-react$/, + + // 编辑器包 + /@esengine\/editor-core/, + /@esengine\/node-editor/, + + // 编辑器子路径 + /\/editor$/, + /\/editor\//, +]; + +/** + * 创建阻止编辑器代码泄漏的插件 + * + * @example + * ```typescript + * import { blockEditorPlugin } from '@esengine/build-config/plugins'; + * + * // 在运行时构建中使用 + * export default defineConfig({ + * plugins: [ + * blockEditorPlugin({ bIsRuntimeBuild: true }) + * ] + * }); + * ``` + */ +export function blockEditorPlugin(options: BlockEditorOptions): Plugin { + const { + bIsRuntimeBuild, + blockedPatterns = DEFAULT_BLOCKED_PATTERNS, + bWarnOnly = false + } = options; + + if (!bIsRuntimeBuild) { + // 非运行时构建不需要此插件 + return { name: 'esengine:block-editor-noop' }; + } + + const isBlocked = (source: string): boolean => { + return blockedPatterns.some(pattern => { + if (typeof pattern === 'string') { + return source === pattern || source.startsWith(pattern + '/'); + } + return pattern.test(source); + }); + }; + + return { + name: 'esengine:block-editor', + enforce: 'pre', + + resolveId(source: string, importer: string | undefined) { + if (isBlocked(source)) { + const message = `[block-editor] Editor dependency detected in runtime build:\n` + + ` Source: ${source}\n` + + ` Importer: ${importer || 'entry'}\n` + + `\n` + + ` This usually means:\n` + + ` 1. A runtime module is importing from a non-/runtime path\n` + + ` 2. An editor-only dependency leaked into the dependency chain\n` + + `\n` + + ` Fix: Change the import to use /runtime subpath, e.g.:\n` + + ` import { X } from '@esengine/ui/runtime' // ✓\n` + + ` import { X } from '@esengine/ui' // ✗`; + + if (bWarnOnly) { + console.warn('\x1b[33m' + message + '\x1b[0m'); + return { id: source, external: true }; + } else { + throw new Error(message); + } + } + return null; + } + }; +} diff --git a/packages/build-config/src/plugins/css-inject.ts b/packages/build-config/src/plugins/css-inject.ts new file mode 100644 index 00000000..38b4dde3 --- /dev/null +++ b/packages/build-config/src/plugins/css-inject.ts @@ -0,0 +1,71 @@ +/** + * CSS Inject Plugin + * CSS 注入插件 + * + * 将 CSS 内联到 JS 中,避免单独的 CSS 文件 + * Inlines CSS into JS to avoid separate CSS files + */ + +import type { Plugin } from 'vite'; +import type { OutputBundle, NormalizedOutputOptions, OutputAsset, OutputChunk } from 'rollup'; + +/** + * 创建 CSS 注入插件 + * + * @example + * ```typescript + * import { cssInjectPlugin } from '@esengine/build-config/plugins'; + * + * export default defineConfig({ + * plugins: [cssInjectPlugin()] + * }); + * ``` + */ +export function cssInjectPlugin(): Plugin { + return { + name: 'esengine:css-inject', + apply: 'build', + + generateBundle(_options: NormalizedOutputOptions, bundle: OutputBundle) { + // 收集所有 CSS 内容 + const cssChunks: string[] = []; + const cssFileNames: string[] = []; + + for (const [fileName, chunk] of Object.entries(bundle)) { + if (fileName.endsWith('.css') && chunk.type === 'asset') { + cssChunks.push(chunk.source as string); + cssFileNames.push(fileName); + } + } + + if (cssChunks.length === 0) return; + + // 合并所有 CSS + const combinedCSS = cssChunks.join('\n'); + + // 创建注入代码 + const injectCode = ` +(function() { + if (typeof document === 'undefined') return; + var style = document.createElement('style'); + style.setAttribute('data-esengine', 'true'); + style.textContent = ${JSON.stringify(combinedCSS)}; + document.head.appendChild(style); +})(); +`; + + // 找到主入口 JS 文件并注入 + for (const chunk of Object.values(bundle)) { + if (chunk.type === 'chunk' && chunk.isEntry) { + chunk.code = injectCode + chunk.code; + break; + } + } + + // 删除独立的 CSS 文件 + for (const fileName of cssFileNames) { + delete bundle[fileName]; + } + } + }; +} diff --git a/packages/build-config/src/plugins/index.ts b/packages/build-config/src/plugins/index.ts new file mode 100644 index 00000000..59f9a4da --- /dev/null +++ b/packages/build-config/src/plugins/index.ts @@ -0,0 +1,7 @@ +/** + * Shared Vite Plugins + * 共享 Vite 插件 + */ + +export { cssInjectPlugin } from './css-inject'; +export { blockEditorPlugin, type BlockEditorOptions } from './block-editor'; diff --git a/packages/build-config/src/presets/editor-only.ts b/packages/build-config/src/presets/editor-only.ts new file mode 100644 index 00000000..aa3c882f --- /dev/null +++ b/packages/build-config/src/presets/editor-only.ts @@ -0,0 +1,109 @@ +/** + * Editor-Only Package Preset + * 纯编辑器包预设 + * + * 用于仅在编辑器环境使用的包 + * For packages only used in the editor environment + * + * Examples: editor-core, node-editor + */ + +import { resolve } from 'path'; +import { defineConfig, type UserConfig } from 'vite'; +import dts from 'vite-plugin-dts'; +import react from '@vitejs/plugin-react'; +import { STANDARD_EXTERNALS } from '../types'; +import { cssInjectPlugin } from '../plugins/css-inject'; + +export interface EditorOnlyOptions { + /** 包根目录 (通常是 __dirname) */ + root: string; + + /** 入口文件 (默认: src/index.ts) */ + entry?: string; + + /** 是否包含 React 组件 (默认: true) */ + hasReact?: boolean; + + /** 是否包含 CSS (默认: false) */ + hasCSS?: boolean; + + /** 额外的外部依赖 */ + external?: (string | RegExp)[]; + + /** 额外的 Vite 配置 */ + viteConfig?: Partial; +} + +/** + * 创建纯编辑器包的 Vite 配置 + * + * @example + * ```typescript + * // vite.config.ts + * import { editorOnlyPreset } from '@esengine/build-config/presets'; + * + * export default editorOnlyPreset({ + * root: __dirname, + * hasReact: true, + * hasCSS: true + * }); + * ``` + */ +export function editorOnlyPreset(options: EditorOnlyOptions): UserConfig { + const { + root, + entry = 'src/index.ts', + hasReact = true, + hasCSS = false, + external = [], + viteConfig = {} + } = options; + + const plugins: any[] = []; + + // React 支持 + if (hasReact) { + plugins.push(react()); + } + + // DTS 生成 + plugins.push( + dts({ + include: ['src'], + outDir: 'dist', + rollupTypes: false + }) + ); + + // CSS 注入 + if (hasCSS) { + plugins.push(cssInjectPlugin()); + } + + return defineConfig({ + plugins, + esbuild: hasReact ? { jsx: 'automatic' } : undefined, + build: { + lib: { + entry: resolve(root, entry), + formats: ['es'], + fileName: () => 'index.js' + }, + rollupOptions: { + external: [ + ...STANDARD_EXTERNALS, + ...external + ], + output: { + exports: 'named', + preserveModules: false + } + }, + target: 'es2020', + minify: false, + sourcemap: true + }, + ...viteConfig + }); +} diff --git a/packages/build-config/src/presets/index.ts b/packages/build-config/src/presets/index.ts new file mode 100644 index 00000000..f2c26d4a --- /dev/null +++ b/packages/build-config/src/presets/index.ts @@ -0,0 +1,10 @@ +/** + * Build Presets + * 构建预设 + * + * 提供不同类型包的标准化 Vite 配置 + */ + +export { runtimeOnlyPreset, type RuntimeOnlyOptions } from './runtime-only'; +export { pluginPreset, standaloneRuntimeConfig, type PluginPackageOptions, type StandaloneRuntimeOptions } from './plugin'; +export { editorOnlyPreset, type EditorOnlyOptions } from './editor-only'; diff --git a/packages/build-config/src/presets/plugin-tsup.ts b/packages/build-config/src/presets/plugin-tsup.ts new file mode 100644 index 00000000..110e8eeb --- /dev/null +++ b/packages/build-config/src/presets/plugin-tsup.ts @@ -0,0 +1,157 @@ +/** + * Plugin Package Preset (tsup) + * 插件包预设 - 基于 tsup/esbuild + * + * 用于同时包含运行时和编辑器模块的插件包 + * For plugin packages with both runtime and editor modules + * + * 生成三个入口点: + * - index.js - 完整导出(编辑器环境) + * - runtime.js - 纯运行时(游戏运行时环境,不含 React) + * - editor/index.js - 编辑器模块 + * + * Examples: ui, tilemap, behavior-tree, physics-rapier2d + */ + +import type { Options } from 'tsup'; +import { STANDARD_EXTERNALS } from '../types'; + +export interface PluginPackageOptions { + /** 入口点配置 */ + entries?: { + /** 主入口 (默认: src/index.ts) */ + main?: string; + /** 运行时入口 (默认: src/runtime.ts) */ + runtime?: string; + /** 编辑器入口 (默认: src/editor/index.ts) */ + editor?: string; + }; + + /** 额外的外部依赖 */ + external?: (string | RegExp)[]; + + /** 额外的 tsup 配置 */ + tsupConfig?: Partial; +} + +/** + * 创建插件包的 tsup 配置 + * + * @example + * ```typescript + * // tsup.config.ts + * import { defineConfig } from 'tsup'; + * import { pluginPreset } from '@esengine/build-config/presets'; + * + * export default defineConfig(pluginPreset()); + * ``` + */ +export function pluginPreset(options: PluginPackageOptions = {}): Options { + const { + entries = {}, + external = [], + tsupConfig = {} + } = options; + + const mainEntry = entries.main ?? 'src/index.ts'; + const runtimeEntry = entries.runtime ?? 'src/runtime.ts'; + const editorEntry = entries.editor ?? 'src/editor/index.ts'; + + // 合并外部依赖 + const allExternal = [ + ...STANDARD_EXTERNALS, + ...external + ]; + + return { + entry: { + index: mainEntry, + runtime: runtimeEntry, + 'editor/index': editorEntry + }, + format: ['esm'], + dts: true, + splitting: false, // 禁用代码分割 + sourcemap: true, + clean: true, + external: allExternal, + esbuildOptions(options) { + options.jsx = 'automatic'; + }, + ...tsupConfig + }; +} + +/** + * 创建纯运行时包的 tsup 配置 + */ +export interface RuntimeOnlyOptions { + /** 入口文件 (默认: src/index.ts) */ + entry?: string; + /** 额外的外部依赖 */ + external?: (string | RegExp)[]; + /** 额外的 tsup 配置 */ + tsupConfig?: Partial; +} + +export function runtimeOnlyPreset(options: RuntimeOnlyOptions = {}): Options { + const { + entry = 'src/index.ts', + external = [], + tsupConfig = {} + } = options; + + return { + entry: [entry], + format: ['esm'], + dts: true, + splitting: false, + sourcemap: true, + clean: true, + external: [ + ...STANDARD_EXTERNALS, + ...external + ], + ...tsupConfig + }; +} + +/** + * 创建纯编辑器包的 tsup 配置 + */ +export interface EditorOnlyOptions { + /** 入口文件 (默认: src/index.ts) */ + entry?: string; + /** 额外的外部依赖 */ + external?: (string | RegExp)[]; + /** 额外的 tsup 配置 */ + tsupConfig?: Partial; +} + +export function editorOnlyPreset(options: EditorOnlyOptions = {}): Options { + const { + entry = 'src/index.ts', + external = [], + tsupConfig = {} + } = options; + + return { + entry: [entry], + format: ['esm'], + dts: true, + splitting: false, + sourcemap: true, + clean: true, + // 将 CSS 内联到 JS 中,运行时自动注入到 DOM + // Inline CSS into JS, auto-inject to DOM at runtime + injectStyle: true, + external: [ + ...STANDARD_EXTERNALS, + ...external + ], + esbuildOptions(options) { + options.jsx = 'automatic'; + }, + ...tsupConfig + }; +} diff --git a/packages/build-config/src/presets/plugin.ts b/packages/build-config/src/presets/plugin.ts new file mode 100644 index 00000000..699260a0 --- /dev/null +++ b/packages/build-config/src/presets/plugin.ts @@ -0,0 +1,176 @@ +/** + * Plugin Package Preset + * 插件包预设 + * + * 用于同时包含运行时和编辑器模块的插件包 + * For plugin packages with both runtime and editor modules + * + * 生成三个入口点: + * - index.js - 完整导出(编辑器环境) + * - runtime.js - 纯运行时(游戏运行时环境,不含 React) + * - editor/index.js - 编辑器模块 + * + * Examples: ui, tilemap, behavior-tree, physics-rapier2d + */ + +import { resolve } from 'path'; +import { defineConfig, type UserConfig } from 'vite'; +import dts from 'vite-plugin-dts'; +import { STANDARD_EXTERNALS, EDITOR_ONLY_EXTERNALS } from '../types'; +import { cssInjectPlugin } from '../plugins/css-inject'; + +export interface PluginPackageOptions { + /** 包根目录 (通常是 __dirname) */ + root: string; + + /** 入口点配置 */ + entries?: { + /** 主入口 (默认: src/index.ts) */ + main?: string; + /** 运行时入口 (默认: src/runtime.ts) */ + runtime?: string; + /** 编辑器入口 (默认: src/editor/index.ts) */ + editor?: string; + }; + + /** 是否包含 CSS (默认: false) */ + hasCSS?: boolean; + + /** 是否生成 plugin.json 导出 (默认: true) */ + hasPluginJson?: boolean; + + /** 额外的外部依赖 */ + external?: (string | RegExp)[]; + + /** 额外的 Vite 配置 */ + viteConfig?: Partial; +} + +/** + * 创建插件包的 Vite 配置 + * + * @example + * ```typescript + * // vite.config.ts + * import { pluginPreset } from '@esengine/build-config/presets'; + * + * export default pluginPreset({ + * root: __dirname, + * hasCSS: true + * }); + * ``` + */ +export function pluginPreset(options: PluginPackageOptions): UserConfig { + const { + root, + entries = {}, + hasCSS = false, + external = [], + viteConfig = {} + } = options; + + const mainEntry = entries.main ?? 'src/index.ts'; + const runtimeEntry = entries.runtime ?? 'src/runtime.ts'; + const editorEntry = entries.editor ?? 'src/editor/index.ts'; + + // 构建入口点映射 + const entryPoints: Record = { + index: resolve(root, mainEntry), + runtime: resolve(root, runtimeEntry), + 'editor/index': resolve(root, editorEntry) + }; + + const plugins: any[] = [ + dts({ + include: ['src'], + outDir: 'dist', + rollupTypes: false + }) + ]; + + // CSS 注入插件 + if (hasCSS) { + plugins.push(cssInjectPlugin()); + } + + return defineConfig({ + plugins, + esbuild: { + jsx: 'automatic', + }, + build: { + lib: { + entry: entryPoints, + formats: ['es'], + fileName: (_format: string, entryName: string) => `${entryName}.js` + }, + rollupOptions: { + external: [ + ...STANDARD_EXTERNALS, + ...external + ], + output: { + exports: 'named', + preserveModules: false, + // 禁用自动代码分割,所有共享代码内联到各入口 + manualChunks: () => undefined + } + }, + target: 'es2020', + minify: false, + sourcemap: true + }, + ...viteConfig + }); +} + +/** + * 创建独立运行时构建配置 + * 用于 platform-web 等需要生成独立 IIFE 运行时的场景 + * + * @example + * ```typescript + * // rollup.runtime.config.js + * import { standaloneRuntimeConfig } from '@esengine/build-config/presets'; + * + * export default standaloneRuntimeConfig({ + * root: __dirname, + * entry: 'src/runtime.ts', + * globalName: 'ECSRuntime' + * }); + * ``` + */ +export interface StandaloneRuntimeOptions { + /** 包根目录 */ + root: string; + /** 入口文件 */ + entry: string; + /** 全局变量名 (IIFE 格式) */ + globalName: string; + /** 额外的外部依赖 */ + external?: (string | RegExp)[]; +} + +export function standaloneRuntimeConfig(options: StandaloneRuntimeOptions) { + const { root, entry, globalName, external = [] } = options; + + // 返回 Rollup 配置(而非 Vite,因为需要 IIFE 格式) + return { + input: resolve(root, entry), + output: { + file: 'dist/runtime.browser.js', + format: 'iife' as const, + name: globalName, + sourcemap: true, + exports: 'default' as const + }, + external: [ + ...STANDARD_EXTERNALS, + ...EDITOR_ONLY_EXTERNALS, + ...external + ], + plugins: [ + // 需要在使用时传入 rollup 插件 + ] + }; +} diff --git a/packages/build-config/src/presets/runtime-only.ts b/packages/build-config/src/presets/runtime-only.ts new file mode 100644 index 00000000..89d3daab --- /dev/null +++ b/packages/build-config/src/presets/runtime-only.ts @@ -0,0 +1,78 @@ +/** + * Runtime-Only Package Preset + * 纯运行时包预设 + * + * 用于不包含任何编辑器代码的基础库 + * For basic libraries without any editor code + * + * Examples: core, math, components, asset-system + */ + +import { resolve } from 'path'; +import { defineConfig, type UserConfig } from 'vite'; +import dts from 'vite-plugin-dts'; +import { STANDARD_EXTERNALS } from '../types'; + +export interface RuntimeOnlyOptions { + /** 包根目录 (通常是 __dirname) */ + root: string; + /** 入口文件 (默认: src/index.ts) */ + entry?: string; + /** 额外的外部依赖 */ + external?: (string | RegExp)[]; + /** 额外的 Vite 配置 */ + viteConfig?: Partial; +} + +/** + * 创建纯运行时包的 Vite 配置 + * + * @example + * ```typescript + * // vite.config.ts + * import { runtimeOnlyPreset } from '@esengine/build-config/presets'; + * + * export default runtimeOnlyPreset({ + * root: __dirname + * }); + * ``` + */ +export function runtimeOnlyPreset(options: RuntimeOnlyOptions): UserConfig { + const { + root, + entry = 'src/index.ts', + external = [], + viteConfig = {} + } = options; + + return defineConfig({ + plugins: [ + dts({ + include: ['src'], + outDir: 'dist', + rollupTypes: false + }) + ], + build: { + lib: { + entry: resolve(root, entry), + formats: ['es'], + fileName: () => 'index.js' + }, + rollupOptions: { + external: [ + ...STANDARD_EXTERNALS, + ...external + ], + output: { + exports: 'named', + preserveModules: false + } + }, + target: 'es2020', + minify: false, + sourcemap: true + }, + ...viteConfig + }); +} diff --git a/packages/build-config/src/types.ts b/packages/build-config/src/types.ts new file mode 100644 index 00000000..9ab86776 --- /dev/null +++ b/packages/build-config/src/types.ts @@ -0,0 +1,107 @@ +/** + * Build Configuration Types + * 构建配置类型定义 + */ + +import type { UserConfig } from 'vite'; + +/** + * 包类型 + * Package types for different build configurations + */ +export const enum EPackageType { + /** + * 纯运行时库 - 不含任何编辑器代码 + * Pure runtime library - no editor dependencies + * + * Examples: core, math, components, asset-system + */ + RuntimeOnly = 'runtime-only', + + /** + * 插件包 - 同时包含运行时和编辑器模块 + * Plugin package - contains both runtime and editor modules + * + * Examples: ui, tilemap, behavior-tree, physics-rapier2d + */ + Plugin = 'plugin', + + /** + * 纯编辑器包 - 仅用于编辑器 + * Editor-only package - only used in editor + * + * Examples: editor-core, node-editor + */ + EditorOnly = 'editor-only', + + /** + * 应用包 - 最终应用(不发布到 npm) + * Application package - final app (not published) + * + * Examples: editor-app + */ + Application = 'application' +} + +/** + * 包构建配置 + */ +export interface PackageBuildConfig { + /** 包名 */ + name: string; + + /** 包类型 */ + type: EPackageType; + + /** 入口点配置 */ + entries?: { + /** 主入口 (默认: src/index.ts) */ + main?: string; + /** 运行时入口 (仅 Plugin 类型) */ + runtime?: string; + /** 编辑器入口 (Plugin 和 EditorOnly 类型) */ + editor?: string; + }; + + /** 额外的外部依赖 */ + external?: (string | RegExp)[]; + + /** 是否包含 CSS */ + hasCSS?: boolean; + + /** 是否生成 plugin.json 导出 */ + hasPluginJson?: boolean; + + /** 额外的 Vite 配置 */ + viteConfig?: Partial; +} + +/** + * 标准外部依赖列表 + * Standard external dependencies that should never be bundled + */ +export const STANDARD_EXTERNALS = [ + // React 生态 + 'react', + 'react-dom', + 'react/jsx-runtime', + 'lucide-react', + + // 状态管理 + 'zustand', + 'immer', + + // 所有 @esengine 包 + /^@esengine\//, +] as const; + +/** + * 编辑器专用依赖(运行时构建必须排除) + * Editor-only dependencies that must be excluded from runtime builds + */ +export const EDITOR_ONLY_EXTERNALS = [ + '@esengine/editor-core', + '@esengine/node-editor', + /\/editor$/, + /\/editor\//, +] as const; diff --git a/packages/build-config/templates/plugin/package.json.template b/packages/build-config/templates/plugin/package.json.template new file mode 100644 index 00000000..ad163d91 --- /dev/null +++ b/packages/build-config/templates/plugin/package.json.template @@ -0,0 +1,62 @@ +{ + "name": "{{fullName}}", + "version": "1.0.0", + "description": "{{description}}", + "type": "module", + "main": "dist/index.js", + "module": "dist/index.js", + "types": "dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + }, + "./runtime": { + "types": "./dist/runtime.d.ts", + "import": "./dist/runtime.js" + }, + "./editor": { + "types": "./dist/editor/index.d.ts", + "import": "./dist/editor/index.js" + }, + "./plugin.json": "./plugin.json" + }, + "files": [ + "dist", + "plugin.json" + ], + "scripts": { + "build": "tsup", + "build:watch": "tsup --watch", + "clean": "rimraf dist", + "type-check": "tsc --noEmit" + }, + "peerDependencies": { + "@esengine/ecs-framework": "{{depVersion}}", + "@esengine/ecs-components": "{{depVersion}}", + "@esengine/editor-core": "{{depVersion}}", + "react": "^18.3.1" + }, + "peerDependenciesMeta": { + "@esengine/editor-core": { + "optional": true + }, + "react": { + "optional": true + } + }, + "devDependencies": { + "@esengine/build-config": "{{depVersion}}", + "@types/react": "^18.3.12", + "rimraf": "^5.0.5", + "tsup": "^8.0.0", + "typescript": "^5.8.3" + }, + "keywords": [ + "ecs", + "esengine", + "plugin" + ], + "author": "", + "license": "MIT" +} diff --git a/packages/build-config/templates/plugin/plugin.json.template b/packages/build-config/templates/plugin/plugin.json.template new file mode 100644 index 00000000..a3fabdb5 --- /dev/null +++ b/packages/build-config/templates/plugin/plugin.json.template @@ -0,0 +1,22 @@ +{ + "id": "{{fullName}}", + "name": "{{displayName}}", + "version": "1.0.0", + "category": "{{category}}", + "enabledByDefault": true, + "isEnginePlugin": false, + "modules": [ + { + "name": "{{pascalName}}Runtime", + "type": "runtime", + "entry": "./src/runtime.ts" + }, + { + "name": "{{pascalName}}Editor", + "type": "editor", + "entry": "./src/editor/index.ts" + } + ], + "components": [], + "dependencies": [] +} diff --git a/packages/build-config/templates/plugin/src/RuntimeModule.ts.template b/packages/build-config/templates/plugin/src/RuntimeModule.ts.template new file mode 100644 index 00000000..475581ca --- /dev/null +++ b/packages/build-config/templates/plugin/src/RuntimeModule.ts.template @@ -0,0 +1,49 @@ +/** + * {{displayName}} Runtime Module + * + * 运行时模块 - 负责注册组件、服务和系统 + */ + +import type { + IRuntimeModuleLoader, + IComponentRegistry, + SystemContext +} from '@esengine/ecs-components'; +import type { IScene, ServiceContainer } from '@esengine/ecs-framework'; + +export class {{name}}RuntimeModule implements IRuntimeModuleLoader { + /** + * 注册组件到组件注册表 + */ + registerComponents(registry: IComponentRegistry): void { + // registry.register(MyComponent); + } + + /** + * 注册服务到服务容器 + */ + registerServices?(services: ServiceContainer): void { + // services.registerSingleton(MyService); + } + + /** + * 初始化回调 + */ + async onInitialize?(): Promise { + // 执行初始化逻辑 + } + + /** + * 为场景创建系统 + */ + createSystems?(scene: IScene, context: SystemContext): void { + // scene.addSystem(new MySystem()); + } + + /** + * 系统创建完成后的回调,用于连接跨插件依赖 + */ + onSystemsCreated?(scene: IScene, context: SystemContext): void { + // 连接跨插件依赖 + } +} diff --git a/packages/build-config/templates/plugin/src/editor/Plugin.ts.template b/packages/build-config/templates/plugin/src/editor/Plugin.ts.template new file mode 100644 index 00000000..f3975a57 --- /dev/null +++ b/packages/build-config/templates/plugin/src/editor/Plugin.ts.template @@ -0,0 +1,34 @@ +/** + * {{displayName}} Plugin + * + * 插件定义 - 注册编辑器模块(Inspector、工具等) + */ + +import type { IPluginLoader, PluginDescriptor, IEditorModuleLoader } from '@esengine/ecs-components'; +import { {{name}}RuntimeModule } from '../{{name}}RuntimeModule'; + +class {{name}}EditorModule implements IEditorModuleLoader { + registerInspectors(registry: any): void { + // 注册组件 Inspector + // registry.register('MyComponent', MyComponentInspector); + } +} + +const descriptor: PluginDescriptor = { + id: '@esengine/{{name}}', + name: '{{displayName}}', + version: '1.0.0', + category: '{{category}}', + enabledByDefault: true, + isEnginePlugin: false, + modules: [ + { name: '{{name}}Runtime', type: 'runtime', entry: './src/runtime.ts' }, + { name: '{{name}}Editor', type: 'editor', entry: './src/editor/index.ts' } + ] +}; + +export const {{name}}Plugin: IPluginLoader = { + descriptor, + runtimeModule: new {{name}}RuntimeModule(), + editorModule: new {{name}}EditorModule() +}; diff --git a/packages/build-config/templates/plugin/src/editor/index.ts.template b/packages/build-config/templates/plugin/src/editor/index.ts.template new file mode 100644 index 00000000..d5a1fd7f --- /dev/null +++ b/packages/build-config/templates/plugin/src/editor/index.ts.template @@ -0,0 +1,13 @@ +/** + * @esengine/{{name}} Editor Module + * + * 编辑器模块 - 包含 Inspector、工具等编辑器专用代码 + * Editor module - contains Inspector, tools, and other editor-specific code + * + * This module can safely import React and editor-core packages. + */ + +export { {{name}}Plugin } from './{{name}}Plugin'; + +// Inspectors +// export { MyComponentInspector } from './inspectors/MyComponentInspector'; diff --git a/packages/build-config/templates/plugin/src/index.ts.template b/packages/build-config/templates/plugin/src/index.ts.template new file mode 100644 index 00000000..dd2a6797 --- /dev/null +++ b/packages/build-config/templates/plugin/src/index.ts.template @@ -0,0 +1,14 @@ +/** + * @esengine/{{name}} + * + * {{description}} + * + * 主入口 - 导出所有内容(包括编辑器模块) + * Main entry - exports everything (including editor modules) + */ + +// Runtime exports (always available) +export * from './runtime'; + +// Editor exports (only in editor environment) +export { {{name}}Plugin } from './editor'; diff --git a/packages/build-config/templates/plugin/src/runtime.ts.template b/packages/build-config/templates/plugin/src/runtime.ts.template new file mode 100644 index 00000000..56ffb88f --- /dev/null +++ b/packages/build-config/templates/plugin/src/runtime.ts.template @@ -0,0 +1,17 @@ +/** + * @esengine/{{name}} Runtime Entry Point + * + * 运行时入口 - 仅导出运行时代码,不包含任何编辑器依赖 + * Runtime entry - exports only runtime code without any editor dependencies + * + * IMPORTANT: Do not import React or any editor packages here! + */ + +// Components +// export { MyComponent } from './components/MyComponent'; + +// Systems +// export { MySystem } from './systems/MySystem'; + +// Runtime Module +export { {{name}}RuntimeModule } from './{{name}}RuntimeModule'; diff --git a/packages/build-config/templates/plugin/tsconfig.build.json.template b/packages/build-config/templates/plugin/tsconfig.build.json.template new file mode 100644 index 00000000..ba0684d9 --- /dev/null +++ b/packages/build-config/templates/plugin/tsconfig.build.json.template @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ES2020", + "moduleResolution": "bundler", + "lib": ["ES2020", "DOM"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "declaration": true, + "declarationMap": true, + "jsx": "react-jsx", + "resolveJsonModule": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/build-config/templates/plugin/tsconfig.json.template b/packages/build-config/templates/plugin/tsconfig.json.template new file mode 100644 index 00000000..9ef4982a --- /dev/null +++ b/packages/build-config/templates/plugin/tsconfig.json.template @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": ["ES2020", "DOM"], + "strict": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "jsx": "react-jsx", + "resolveJsonModule": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/build-config/templates/plugin/tsup.config.ts.template b/packages/build-config/templates/plugin/tsup.config.ts.template new file mode 100644 index 00000000..e5589c93 --- /dev/null +++ b/packages/build-config/templates/plugin/tsup.config.ts.template @@ -0,0 +1,7 @@ +import { defineConfig } from 'tsup'; +import { pluginPreset } from '@esengine/build-config/presets/tsup'; + +export default defineConfig({ + ...pluginPreset(), + tsconfig: 'tsconfig.build.json' +}); diff --git a/packages/build-config/templates/runtime-only/package.json.template b/packages/build-config/templates/runtime-only/package.json.template new file mode 100644 index 00000000..d958ca47 --- /dev/null +++ b/packages/build-config/templates/runtime-only/package.json.template @@ -0,0 +1,38 @@ +{ + "name": "{{fullName}}", + "version": "1.0.0", + "description": "{{description}}", + "type": "module", + "main": "dist/index.js", + "module": "dist/index.js", + "types": "dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "files": [ + "dist" + ], + "scripts": { + "build": "tsup", + "build:watch": "tsup --watch", + "clean": "rimraf dist", + "type-check": "tsc --noEmit" + }, + "peerDependencies": { + "@esengine/ecs-framework": "{{depVersion}}" + }, + "devDependencies": { + "@esengine/build-config": "{{depVersion}}", + "rimraf": "^5.0.5", + "tsup": "^8.0.0", + "typescript": "^5.8.3" + }, + "keywords": [ + "ecs" + ], + "author": "", + "license": "MIT" +} diff --git a/packages/build-config/templates/runtime-only/src/index.ts.template b/packages/build-config/templates/runtime-only/src/index.ts.template new file mode 100644 index 00000000..b5c2df43 --- /dev/null +++ b/packages/build-config/templates/runtime-only/src/index.ts.template @@ -0,0 +1,8 @@ +/** + * @esengine/{{name}} + * + * {{description}} + */ + +// Export your public API here +export {}; diff --git a/packages/build-config/templates/runtime-only/tsconfig.build.json.template b/packages/build-config/templates/runtime-only/tsconfig.build.json.template new file mode 100644 index 00000000..bf8abf7b --- /dev/null +++ b/packages/build-config/templates/runtime-only/tsconfig.build.json.template @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ES2020", + "moduleResolution": "bundler", + "lib": ["ES2020", "DOM"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "declaration": true, + "declarationMap": true, + "resolveJsonModule": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/build-config/templates/runtime-only/tsconfig.json.template b/packages/build-config/templates/runtime-only/tsconfig.json.template new file mode 100644 index 00000000..6c5476dc --- /dev/null +++ b/packages/build-config/templates/runtime-only/tsconfig.json.template @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": ["ES2020", "DOM"], + "strict": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "resolveJsonModule": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/build-config/templates/runtime-only/tsup.config.ts.template b/packages/build-config/templates/runtime-only/tsup.config.ts.template new file mode 100644 index 00000000..08929a87 --- /dev/null +++ b/packages/build-config/templates/runtime-only/tsup.config.ts.template @@ -0,0 +1,7 @@ +import { defineConfig } from 'tsup'; +import { runtimeOnlyPreset } from '@esengine/build-config/presets/tsup'; + +export default defineConfig({ + ...runtimeOnlyPreset(), + tsconfig: 'tsconfig.build.json' +}); diff --git a/packages/build-config/tsconfig.json b/packages/build-config/tsconfig.json new file mode 100644 index 00000000..f59e0a3a --- /dev/null +++ b/packages/build-config/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "declaration": true, + "declarationDir": "dist", + "outDir": "dist", + "rootDir": "src", + "resolveJsonModule": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/camera-editor/package.json b/packages/camera-editor/package.json new file mode 100644 index 00000000..f3edd467 --- /dev/null +++ b/packages/camera-editor/package.json @@ -0,0 +1,43 @@ +{ + "name": "@esengine/camera-editor", + "version": "1.0.0", + "description": "Editor components for camera system", + "main": "dist/index.js", + "module": "dist/index.js", + "types": "dist/index.d.ts", + "type": "module", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "files": [ + "dist" + ], + "scripts": { + "build": "tsup", + "build:watch": "tsup --watch", + "type-check": "tsc --noEmit", + "clean": "rimraf dist" + }, + "devDependencies": { + "@esengine/ecs-framework": "workspace:*", + "@esengine/engine-core": "workspace:*", + "@esengine/camera": "workspace:*", + "@esengine/editor-core": "workspace:*", + "@esengine/build-config": "workspace:*", + "react": "^18.3.1", + "@types/react": "^18.2.0", + "rimraf": "^5.0.5", + "tsup": "^8.0.0", + "typescript": "^5.3.3" + }, + "keywords": [ + "ecs", + "camera", + "editor" + ], + "author": "yhh", + "license": "MIT" +} diff --git a/packages/camera-editor/src/index.ts b/packages/camera-editor/src/index.ts new file mode 100644 index 00000000..143f3c22 --- /dev/null +++ b/packages/camera-editor/src/index.ts @@ -0,0 +1,91 @@ +/** + * @esengine/camera-editor + * + * Editor support for @esengine/camera + * 相机编辑器支持 + */ + +import type { Entity, ServiceContainer } from '@esengine/ecs-framework'; +import { Core } from '@esengine/ecs-framework'; +import { TransformComponent } from '@esengine/engine-core'; +import type { + IEditorModuleLoader, + EntityCreationTemplate +} from '@esengine/editor-core'; +import { + EntityStoreService, + MessageHub, + ComponentRegistry +} from '@esengine/editor-core'; +import { CameraComponent } from '@esengine/camera'; + +export class CameraEditorModule implements IEditorModuleLoader { + async install(services: ServiceContainer): Promise { + const componentRegistry = services.resolve(ComponentRegistry); + if (componentRegistry) { + componentRegistry.register({ + name: 'Camera', + type: CameraComponent, + category: 'components.category.rendering', + description: 'Camera for 2D/3D rendering', + icon: 'Camera' + }); + } + } + + async uninstall(): Promise { + // Nothing to cleanup + } + + getEntityCreationTemplates(): EntityCreationTemplate[] { + return [ + { + id: 'create-camera', + label: 'Camera', + icon: 'Camera', + category: 'rendering', + order: 50, + create: (): number => { + return this.createCameraEntity('Camera'); + } + }, + ]; + } + + private createCameraEntity(baseName: string): number { + const scene = Core.scene; + if (!scene) { + throw new Error('Scene not available'); + } + + const entityStore = Core.services.resolve(EntityStoreService); + const messageHub = Core.services.resolve(MessageHub); + + if (!entityStore || !messageHub) { + throw new Error('EntityStoreService or MessageHub not available'); + } + + const existingCount = entityStore.getAllEntities() + .filter((e: Entity) => e.name.startsWith(baseName)).length; + const entityName = existingCount > 0 ? `${baseName} ${existingCount + 1}` : baseName; + + const entity = scene.createEntity(entityName); + + const transform = new TransformComponent(); + entity.addComponent(transform); + + const camera = new CameraComponent(); + entity.addComponent(camera); + + entityStore.addEntity(entity); + messageHub.publish('entity:added', { entity }); + messageHub.publish('scene:modified', {}); + entityStore.selectEntity(entity); + + return entity.id; + } +} + +export const cameraEditorModule = new CameraEditorModule(); + +export default cameraEditorModule; diff --git a/packages/camera-editor/tsconfig.build.json b/packages/camera-editor/tsconfig.build.json new file mode 100644 index 00000000..29e209e8 --- /dev/null +++ b/packages/camera-editor/tsconfig.build.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "composite": false, + "declaration": true, + "declarationMap": true, + "outDir": "./dist", + "rootDir": "./src", + "jsx": "react-jsx" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "**/*.test.ts"] +} diff --git a/packages/camera-editor/tsconfig.json b/packages/camera-editor/tsconfig.json new file mode 100644 index 00000000..39e66801 --- /dev/null +++ b/packages/camera-editor/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./dist", + "rootDir": "./src", + "jsx": "react-jsx" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"], + "references": [ + { "path": "../core" }, + { "path": "../camera" }, + { "path": "../editor-core" } + ] +} diff --git a/packages/camera-editor/tsup.config.ts b/packages/camera-editor/tsup.config.ts new file mode 100644 index 00000000..b4f49f5d --- /dev/null +++ b/packages/camera-editor/tsup.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'tsup'; +import { editorOnlyPreset } from '../build-config/src/presets/plugin-tsup'; + +export default defineConfig({ + ...editorOnlyPreset(), + tsconfig: 'tsconfig.build.json' +}); diff --git a/packages/camera/package.json b/packages/camera/package.json new file mode 100644 index 00000000..916248fd --- /dev/null +++ b/packages/camera/package.json @@ -0,0 +1,47 @@ +{ + "name": "@esengine/camera", + "version": "1.0.0", + "description": "Camera component and systems for 2D/3D rendering", + "esengine": { + "plugin": true, + "pluginExport": "CameraPlugin", + "category": "core", + "isEnginePlugin": true + }, + "main": "dist/index.js", + "module": "dist/index.js", + "types": "dist/index.d.ts", + "type": "module", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "files": [ + "dist" + ], + "scripts": { + "build": "tsup", + "build:watch": "tsup --watch", + "type-check": "tsc --noEmit", + "clean": "rimraf dist" + }, + "devDependencies": { + "@esengine/ecs-framework": "workspace:*", + "@esengine/engine-core": "workspace:*", + "@esengine/build-config": "workspace:*", + "rimraf": "^5.0.5", + "tsup": "^8.0.0", + "typescript": "^5.3.3" + }, + "keywords": [ + "ecs", + "camera", + "2d", + "3d", + "rendering" + ], + "author": "yhh", + "license": "MIT" +} diff --git a/packages/components/src/CameraComponent.ts b/packages/camera/src/CameraComponent.ts similarity index 73% rename from packages/components/src/CameraComponent.ts rename to packages/camera/src/CameraComponent.ts index 16ec3175..e3c02ac9 100644 --- a/packages/components/src/CameraComponent.ts +++ b/packages/camera/src/CameraComponent.ts @@ -1,77 +1,64 @@ import { Component, ECSComponent, Serializable, Serialize, Property } from '@esengine/ecs-framework'; -/** - * 相机投影类型 - */ -export enum CameraProjection { +export enum ECameraProjection { Perspective = 'perspective', Orthographic = 'orthographic' } -/** - * 相机组件 - 管理视图和投影 - */ @ECSComponent('Camera') @Serializable({ version: 1, typeId: 'Camera' }) export class CameraComponent extends Component { - /** 投影类型 */ @Serialize() @Property({ type: 'enum', label: 'Projection', options: [ - { label: 'Orthographic', value: CameraProjection.Orthographic }, - { label: 'Perspective', value: CameraProjection.Perspective } + { label: 'Orthographic', value: ECameraProjection.Orthographic }, + { label: 'Perspective', value: ECameraProjection.Perspective } ] }) - public projection: CameraProjection = CameraProjection.Orthographic; + public projection: ECameraProjection = ECameraProjection.Orthographic; - /** 视野角度(透视模式) */ + /** 透视模式下的视野角度,范围 [1, 179] 度 */ @Serialize() @Property({ type: 'number', label: 'Field of View', min: 1, max: 179 }) public fieldOfView: number = 60; - /** 正交尺寸(正交模式) */ + /** 正交模式下的可见区域半高度(世界单位) */ @Serialize() @Property({ type: 'number', label: 'Orthographic Size', min: 0.1, step: 0.1 }) public orthographicSize: number = 5; - /** 近裁剪面 */ @Serialize() @Property({ type: 'number', label: 'Near Clip', min: 0.01, step: 0.1 }) public nearClipPlane: number = 0.1; - /** 远裁剪面 */ @Serialize() @Property({ type: 'number', label: 'Far Clip', min: 1, step: 10 }) public farClipPlane: number = 1000; - /** 视口X */ + /** 视口归一化坐标,范围 [0, 1] */ @Serialize() @Property({ type: 'number', label: 'Viewport X', min: 0, max: 1, step: 0.01 }) public viewportX: number = 0; - /** 视口Y */ @Serialize() @Property({ type: 'number', label: 'Viewport Y', min: 0, max: 1, step: 0.01 }) public viewportY: number = 0; - /** 视口宽度 */ @Serialize() @Property({ type: 'number', label: 'Viewport Width', min: 0, max: 1, step: 0.01 }) public viewportWidth: number = 1; - /** 视口高度 */ @Serialize() @Property({ type: 'number', label: 'Viewport Height', min: 0, max: 1, step: 0.01 }) public viewportHeight: number = 1; - /** 渲染优先级 */ + /** 渲染优先级,值越大越后渲染(覆盖在上层) */ @Serialize() @Property({ type: 'integer', label: 'Depth' }) public depth: number = 0; - /** 背景颜色 */ @Serialize() @Property({ type: 'color', label: 'Background Color' }) public backgroundColor: string = '#000000'; @@ -80,3 +67,6 @@ export class CameraComponent extends Component { super(); } } + +/** @deprecated 使用 ECameraProjection 代替 */ +export const CameraProjection = ECameraProjection; diff --git a/packages/camera/src/CameraPlugin.ts b/packages/camera/src/CameraPlugin.ts new file mode 100644 index 00000000..457b541b --- /dev/null +++ b/packages/camera/src/CameraPlugin.ts @@ -0,0 +1,24 @@ +import type { ComponentRegistry as ComponentRegistryType } from '@esengine/ecs-framework'; +import type { IRuntimeModule, IPlugin, PluginDescriptor } from '@esengine/engine-core'; +import { CameraComponent } from './CameraComponent'; + +class CameraRuntimeModule implements IRuntimeModule { + registerComponents(registry: typeof ComponentRegistryType): void { + registry.register(CameraComponent); + } +} + +const descriptor: PluginDescriptor = { + id: '@esengine/camera', + name: 'Camera', + version: '1.0.0', + description: '2D/3D 相机组件', + category: 'core', + enabledByDefault: true, + isEnginePlugin: true +}; + +export const CameraPlugin: IPlugin = { + descriptor, + runtimeModule: new CameraRuntimeModule() +}; diff --git a/packages/camera/src/index.ts b/packages/camera/src/index.ts new file mode 100644 index 00000000..2fa92d1c --- /dev/null +++ b/packages/camera/src/index.ts @@ -0,0 +1,2 @@ +export { CameraComponent, ECameraProjection, CameraProjection } from './CameraComponent'; +export { CameraPlugin } from './CameraPlugin'; diff --git a/packages/camera/tsconfig.build.json b/packages/camera/tsconfig.build.json new file mode 100644 index 00000000..f39a0594 --- /dev/null +++ b/packages/camera/tsconfig.build.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "composite": false, + "declaration": true, + "declarationMap": true, + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "**/*.test.ts"] +} diff --git a/packages/camera/tsconfig.json b/packages/camera/tsconfig.json new file mode 100644 index 00000000..02f5f187 --- /dev/null +++ b/packages/camera/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"], + "references": [ + { "path": "../core" } + ] +} diff --git a/packages/camera/tsup.config.ts b/packages/camera/tsup.config.ts new file mode 100644 index 00000000..f704a430 --- /dev/null +++ b/packages/camera/tsup.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'tsup'; +import { runtimeOnlyPreset } from '../build-config/src/presets/plugin-tsup'; + +export default defineConfig({ + ...runtimeOnlyPreset(), + tsconfig: 'tsconfig.build.json' +}); diff --git a/packages/components/package.json b/packages/components/package.json deleted file mode 100644 index b8736c16..00000000 --- a/packages/components/package.json +++ /dev/null @@ -1,54 +0,0 @@ -{ - "name": "@esengine/ecs-components", - "version": "1.0.0", - "description": "Standard component library for ECS Framework", - "main": "bin/index.js", - "types": "bin/index.d.ts", - "exports": { - ".": { - "types": "./bin/index.d.ts", - "import": "./bin/index.js", - "development": { - "types": "./src/index.ts", - "import": "./src/index.ts" - } - }, - "./plugin.json": "./plugin.json" - }, - "files": [ - "bin/**/*", - "plugin.json" - ], - "keywords": [ - "ecs", - "components", - "game-engine", - "typescript" - ], - "scripts": { - "clean": "rimraf bin dist tsconfig.tsbuildinfo", - "build:ts": "tsc", - "prebuild": "npm run clean", - "build": "npm run build:ts", - "build:watch": "tsc --watch", - "rebuild": "npm run clean && npm run build" - }, - "author": "yhh", - "license": "MIT", - "devDependencies": { - "rimraf": "^5.0.0", - "typescript": "^5.8.3" - }, - "peerDependencies": { - "@esengine/ecs-framework": "^2.2.8", - "@esengine/asset-system": "workspace:*" - }, - "dependencies": { - "tslib": "^2.8.1" - }, - "repository": { - "type": "git", - "url": "https://github.com/esengine/ecs-framework.git", - "directory": "packages/components" - } -} diff --git a/packages/components/plugin.json b/packages/components/plugin.json deleted file mode 100644 index cb4b79d5..00000000 --- a/packages/components/plugin.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "id": "@esengine/ecs-components", - "name": "Core Components", - "version": "1.0.0", - "description": "Transform, Sprite, Camera 等核心组件", - "category": "core", - "loadingPhase": "preDefault", - "enabledByDefault": true, - "canContainContent": false, - "isEnginePlugin": true, - "modules": [ - { - "name": "CoreRuntime", - "type": "runtime", - "entry": "./src/index.ts" - } - ], - "dependencies": [], - "icon": "Settings" -} diff --git a/packages/components/src/AudioSourceComponent.ts b/packages/components/src/AudioSourceComponent.ts deleted file mode 100644 index fd7ed525..00000000 --- a/packages/components/src/AudioSourceComponent.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { Component, ECSComponent, Serializable, Serialize } from '@esengine/ecs-framework'; - -/** - * 音频源组件 - 管理音频播放 - */ -@ECSComponent('AudioSource') -@Serializable({ version: 1, typeId: 'AudioSource' }) -export class AudioSourceComponent extends Component { - /** 音频资源路径 */ - @Serialize() public clip: string = ''; - - /** 音量 (0-1) */ - @Serialize() public volume: number = 1; - - /** 音调 */ - @Serialize() public pitch: number = 1; - - /** 是否循环 */ - @Serialize() public loop: boolean = false; - - /** 是否启动时播放 */ - @Serialize() public playOnAwake: boolean = false; - - /** 是否静音 */ - @Serialize() public mute: boolean = false; - - /** 空间混合 (0=2D, 1=3D) */ - @Serialize() public spatialBlend: number = 0; - - /** 最小距离(3D音效) */ - @Serialize() public minDistance: number = 1; - - /** 最大距离(3D音效) */ - @Serialize() public maxDistance: number = 500; - - constructor() { - super(); - } -} diff --git a/packages/components/src/CorePlugin.ts b/packages/components/src/CorePlugin.ts deleted file mode 100644 index f0c61bfc..00000000 --- a/packages/components/src/CorePlugin.ts +++ /dev/null @@ -1,136 +0,0 @@ -/** - * Core Components Plugin - * 核心组件插件 - * - * 提供基础的 Transform、Sprite、Camera 等核心组件 - * 这是一个核心插件,不可禁用 - */ - -import type { ComponentRegistry as ComponentRegistryType, IScene, ServiceContainer } from '@esengine/ecs-framework'; - -// Components -import { TransformComponent } from './TransformComponent'; -import { SpriteComponent } from './SpriteComponent'; -import { SpriteAnimatorComponent } from './SpriteAnimatorComponent'; -import { CameraComponent } from './CameraComponent'; - -// Systems -import { SpriteAnimatorSystem } from './systems/SpriteAnimatorSystem'; - - -/** - * 系统创建上下文 - */ -export interface SystemContext { - isEditor: boolean; - engineBridge?: any; - renderSystem?: any; - [key: string]: any; -} - -/** - * 插件描述符类型 - */ -export interface PluginDescriptor { - id: string; - name: string; - version: string; - description?: string; - category?: string; - loadingPhase?: string; - enabledByDefault?: boolean; - canContainContent?: boolean; - isEnginePlugin?: boolean; - modules?: Array<{ - name: string; - type: string; - entry: string; - }>; - dependencies?: Array<{ - id: string; - version?: string; - }>; - icon?: string; -} - -/** - * 运行时模块加载器接口 - */ -export interface IRuntimeModuleLoader { - registerComponents(registry: typeof ComponentRegistryType): void; - registerServices?(services: ServiceContainer): void; - createSystems?(scene: IScene, context: SystemContext): void; - /** - * 所有系统创建完成后调用 - * 用于处理跨插件的系统依赖关系 - * Called after all systems are created, used for cross-plugin system dependencies - */ - onSystemsCreated?(scene: IScene, context: SystemContext): void; - onInitialize?(): Promise; - onDestroy?(): void; -} - -/** - * 插件加载器接口 - */ -export interface IPluginLoader { - readonly descriptor: PluginDescriptor; - readonly runtimeModule?: IRuntimeModuleLoader; - readonly editorModule?: any; -} - -/** - * 核心组件运行时模块 - */ -export class CoreRuntimeModule implements IRuntimeModuleLoader { - registerComponents(registry: typeof ComponentRegistryType): void { - registry.register(TransformComponent); - registry.register(SpriteComponent); - registry.register(SpriteAnimatorComponent); - registry.register(CameraComponent); - } - - createSystems(scene: IScene, context: SystemContext): void { - const animatorSystem = new SpriteAnimatorSystem(); - - if (context.isEditor) { - animatorSystem.enabled = false; - } - - scene.addSystem(animatorSystem); - context.animatorSystem = animatorSystem; - } -} - -/** - * 插件描述符 - */ -const descriptor: PluginDescriptor = { - id: '@esengine/ecs-components', - name: 'Core Components', - version: '1.0.0', - description: 'Transform, Sprite, Camera 等核心组件', - category: 'core', - loadingPhase: 'preDefault', - enabledByDefault: true, - canContainContent: false, - isEnginePlugin: true, - modules: [ - { - name: 'CoreRuntime', - type: 'runtime', - entry: './src/index.ts' - } - ], - icon: 'Settings' -}; - -/** - * 核心组件插件 - */ -export const CorePlugin: IPluginLoader = { - descriptor, - runtimeModule: new CoreRuntimeModule(), -}; - -export default CorePlugin; diff --git a/packages/components/src/TextComponent.ts b/packages/components/src/TextComponent.ts deleted file mode 100644 index 5f075229..00000000 --- a/packages/components/src/TextComponent.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { Component, ECSComponent, Serializable, Serialize } from '@esengine/ecs-framework'; - -/** - * 文本对齐方式 - */ -export enum TextAlignment { - Left = 'left', - Center = 'center', - Right = 'right' -} - -/** - * 文本组件 - 管理文本渲染 - */ -@ECSComponent('Text') -@Serializable({ version: 1, typeId: 'Text' }) -export class TextComponent extends Component { - /** 文本内容 */ - @Serialize() public text: string = ''; - - /** 字体 */ - @Serialize() public font: string = 'Arial'; - - /** 字体大小 */ - @Serialize() public fontSize: number = 16; - - /** 颜色 */ - @Serialize() public color: string = '#ffffff'; - - /** 对齐方式 */ - @Serialize() public alignment: TextAlignment = TextAlignment.Left; - - /** 行高 */ - @Serialize() public lineHeight: number = 1.2; - - /** 是否加粗 */ - @Serialize() public bold: boolean = false; - - /** 是否斜体 */ - @Serialize() public italic: boolean = false; - - constructor(text: string = '') { - super(); - this.text = text; - } -} diff --git a/packages/components/src/TransformComponent.ts b/packages/components/src/TransformComponent.ts deleted file mode 100644 index 9a01161e..00000000 --- a/packages/components/src/TransformComponent.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { Component, ECSComponent, Serializable, Serialize, Property } from '@esengine/ecs-framework'; - -/** - * 3D向量 - */ -export interface Vector3 { - x: number; - y: number; - z: number; -} - -/** - * 变换组件 - 管理实体的位置、旋转和缩放 - */ -@ECSComponent('Transform') -@Serializable({ version: 1, typeId: 'Transform' }) -export class TransformComponent extends Component { - /** 位置 */ - @Serialize() - @Property({ type: 'vector3', label: 'Position' }) - public position: Vector3 = { x: 0, y: 0, z: 0 }; - - /** 旋转(欧拉角,度) */ - @Serialize() - @Property({ type: 'vector3', label: 'Rotation' }) - public rotation: Vector3 = { x: 0, y: 0, z: 0 }; - - /** 缩放 */ - @Serialize() - @Property({ type: 'vector3', label: 'Scale' }) - public scale: Vector3 = { x: 1, y: 1, z: 1 }; - - constructor(x: number = 0, y: number = 0, z: number = 0) { - super(); - this.position = { x, y, z }; - } - - /** - * 设置位置 - */ - public setPosition(x: number, y: number, z: number = 0): this { - this.position = { x, y, z }; - return this; - } - - /** - * 设置旋转 - */ - public setRotation(x: number, y: number, z: number): this { - this.rotation = { x, y, z }; - return this; - } - - /** - * 设置缩放 - */ - public setScale(x: number, y: number, z: number = 1): this { - this.scale = { x, y, z }; - return this; - } -} diff --git a/packages/components/src/index.ts b/packages/components/src/index.ts deleted file mode 100644 index adf75bf4..00000000 --- a/packages/components/src/index.ts +++ /dev/null @@ -1,21 +0,0 @@ -// 变换 -export { TransformComponent, Vector3 } from './TransformComponent'; - -// 渲染 -export { SpriteComponent } from './SpriteComponent'; -export { SpriteAnimatorComponent, AnimationFrame, AnimationClip } from './SpriteAnimatorComponent'; -export { TextComponent, TextAlignment } from './TextComponent'; -export { CameraComponent, CameraProjection } from './CameraComponent'; - -// 系统 -export { SpriteAnimatorSystem } from './systems/SpriteAnimatorSystem'; - -// 物理组件已移至 @esengine/physics-rapier2d 包 -// Physics components have been moved to @esengine/physics-rapier2d package - -// 音频 -export { AudioSourceComponent } from './AudioSourceComponent'; - -// Plugin (unified plugin system) -export { CorePlugin, CoreRuntimeModule } from './CorePlugin'; -export type { SystemContext, PluginDescriptor, IRuntimeModuleLoader, IPluginLoader } from './CorePlugin'; \ No newline at end of file diff --git a/packages/components/tsconfig.json b/packages/components/tsconfig.json deleted file mode 100644 index 27a8ccf8..00000000 --- a/packages/components/tsconfig.json +++ /dev/null @@ -1,52 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2020", - "module": "ES2020", - "moduleResolution": "node", - "allowImportingTsExtensions": false, - "lib": ["ES2020", "DOM"], - "outDir": "./bin", - "rootDir": "./src", - "strict": true, - "composite": true, - "esModuleInterop": true, - "allowSyntheticDefaultImports": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "declaration": true, - "declarationMap": true, - "sourceMap": true, - "removeComments": false, - "noImplicitAny": true, - "noImplicitReturns": true, - "noImplicitThis": true, - "noUnusedLocals": false, - "noUnusedParameters": false, - "exactOptionalPropertyTypes": false, - "noImplicitOverride": true, - "noPropertyAccessFromIndexSignature": false, - "noUncheckedIndexedAccess": false, - "experimentalDecorators": true, - "emitDecoratorMetadata": true, - "importHelpers": true, - "downlevelIteration": true, - "isolatedModules": false, - "allowJs": true, - "resolveJsonModule": true - }, - "include": [ - "src/**/*", - "plugin.json" - ], - "exclude": [ - "node_modules", - "bin", - "**/*.test.ts", - "**/*.spec.ts" - ], - "references": [ - { - "path": "../core" - } - ] -} diff --git a/packages/core/src/Core.ts b/packages/core/src/Core.ts index b0ae2e04..8c6dd263 100644 --- a/packages/core/src/Core.ts +++ b/packages/core/src/Core.ts @@ -69,13 +69,6 @@ export class Core { */ private static _logger = createLogger('Core'); - /** - * 实体系统启用状态 - * - * 控制是否启用ECS实体系统功能。 - */ - public static entitySystemsEnabled: boolean; - /** * 调试模式标志 * @@ -132,7 +125,6 @@ export class Core { // 保存配置 this._config = { debug: true, - enableEntitySystems: true, ...config }; @@ -176,7 +168,6 @@ export class Core { this._pluginManager.initialize(this, this._serviceContainer); this._serviceContainer.registerInstance(PluginManager, this._pluginManager); - Core.entitySystemsEnabled = this._config.enableEntitySystems ?? true; this.debug = this._config.debug ?? true; // 初始化调试管理器 @@ -266,7 +257,6 @@ export class Core { * // 方式1:使用配置对象 * Core.create({ * debug: true, - * enableEntitySystems: true, * debugConfig: { * enabled: true, * websocketUrl: 'ws://localhost:9229' @@ -281,7 +271,7 @@ export class Core { if (this._instance == null) { // 向后兼容:如果传入boolean,转换为配置对象 const coreConfig: ICoreConfig = typeof config === 'boolean' - ? { debug: config, enableEntitySystems: true } + ? { debug: config } : config; this._instance = new Core(coreConfig); } else { @@ -614,7 +604,6 @@ export class Core { // 核心系统初始化 Core._logger.info('Core initialized', { debug: this.debug, - entitySystemsEnabled: Core.entitySystemsEnabled, debugEnabled: this._config.debugConfig?.enabled || false }); } diff --git a/packages/core/src/ECS/Components/HierarchyComponent.ts b/packages/core/src/ECS/Components/HierarchyComponent.ts new file mode 100644 index 00000000..1895a9cc --- /dev/null +++ b/packages/core/src/ECS/Components/HierarchyComponent.ts @@ -0,0 +1,56 @@ +import { Component } from '../Component'; +import { ECSComponent } from '../Decorators'; +import { Serializable, Serialize } from '../Serialization/SerializationDecorators'; + +/** + * 层级关系组件 - 用于建立实体间的父子关系 + * + * 只有需要层级关系的实体才添加此组件,遵循 ECS 组合原则。 + * 层级操作应通过 HierarchySystem 进行,而非直接修改此组件。 + * + * @example + * ```typescript + * // 通过 HierarchySystem 设置父子关系 + * const hierarchySystem = scene.getSystem(HierarchySystem); + * hierarchySystem.setParent(childEntity, parentEntity); + * + * // 查询层级信息 + * const parent = hierarchySystem.getParent(entity); + * const children = hierarchySystem.getChildren(entity); + * ``` + */ +@ECSComponent('Hierarchy') +@Serializable({ version: 1, typeId: 'Hierarchy' }) +export class HierarchyComponent extends Component { + /** + * 父实体 ID + * null 表示根实体(无父级) + */ + @Serialize() + public parentId: number | null = null; + + /** + * 子实体 ID 列表 + * 顺序即为子级的排列顺序 + */ + @Serialize() + public childIds: number[] = []; + + /** + * 在层级中的深度 + * 根实体深度为 0,由 HierarchySystem 维护 + */ + public depth: number = 0; + + /** + * 层级中是否激活 + * 考虑所有祖先的激活状态,由 HierarchySystem 维护 + */ + public bActiveInHierarchy: boolean = true; + + /** + * 层级缓存是否脏 + * 用于优化缓存更新 + */ + public bCacheDirty: boolean = true; +} diff --git a/packages/core/src/ECS/Components/index.ts b/packages/core/src/ECS/Components/index.ts new file mode 100644 index 00000000..826cbc8a --- /dev/null +++ b/packages/core/src/ECS/Components/index.ts @@ -0,0 +1 @@ +export { HierarchyComponent } from './HierarchyComponent'; diff --git a/packages/core/src/ECS/Core/FluentAPI/EntityBuilder.ts b/packages/core/src/ECS/Core/FluentAPI/EntityBuilder.ts index 8f198a94..dc74e8c5 100644 --- a/packages/core/src/ECS/Core/FluentAPI/EntityBuilder.ts +++ b/packages/core/src/ECS/Core/FluentAPI/EntityBuilder.ts @@ -2,6 +2,7 @@ import { Entity } from '../../Entity'; import { Component } from '../../Component'; import { IScene } from '../../IScene'; import { ComponentType, ComponentStorageManager } from '../ComponentStorage'; +import { HierarchySystem } from '../../Systems/HierarchySystem'; /** * 实体构建器 - 提供流式API创建和配置实体 @@ -129,7 +130,8 @@ export class EntityBuilder { */ public withChild(childBuilder: EntityBuilder): EntityBuilder { const child = childBuilder.build(); - this.entity.addChild(child); + const hierarchySystem = this.scene.getSystem(HierarchySystem); + hierarchySystem?.setParent(child, this.entity); return this; } @@ -139,9 +141,10 @@ export class EntityBuilder { * @returns 实体构建器 */ public withChildren(...childBuilders: EntityBuilder[]): EntityBuilder { + const hierarchySystem = this.scene.getSystem(HierarchySystem); for (const childBuilder of childBuilders) { const child = childBuilder.build(); - this.entity.addChild(child); + hierarchySystem?.setParent(child, this.entity); } return this; } @@ -154,7 +157,8 @@ export class EntityBuilder { public withChildFactory(childFactory: (parent: Entity) => EntityBuilder): EntityBuilder { const childBuilder = childFactory(this.entity); const child = childBuilder.build(); - this.entity.addChild(child); + const hierarchySystem = this.scene.getSystem(HierarchySystem); + hierarchySystem?.setParent(child, this.entity); return this; } @@ -167,7 +171,8 @@ export class EntityBuilder { public withChildIf(condition: boolean, childBuilder: EntityBuilder): EntityBuilder { if (condition) { const child = childBuilder.build(); - this.entity.addChild(child); + const hierarchySystem = this.scene.getSystem(HierarchySystem); + hierarchySystem?.setParent(child, this.entity); } return this; } diff --git a/packages/core/src/ECS/Entity.ts b/packages/core/src/ECS/Entity.ts index 23fb4c1e..6f15f4f6 100644 --- a/packages/core/src/ECS/Entity.ts +++ b/packages/core/src/ECS/Entity.ts @@ -37,12 +37,14 @@ export class EntityComparer { * * ECS架构中的实体(Entity),作为组件的容器。 * 实体本身不包含游戏逻辑,所有功能都通过组件来实现。 - * 支持父子关系,可以构建实体层次结构。 + * + * 层级关系通过 HierarchyComponent 和 HierarchySystem 管理, + * 而非 Entity 内置属性,符合 ECS 组合原则。 * * @example * ```typescript * // 创建实体 - * const entity = new Entity("Player", 1); + * const entity = scene.createEntity("Player"); * * // 添加组件 * const healthComponent = entity.addComponent(new HealthComponent(100)); @@ -50,12 +52,9 @@ export class EntityComparer { * // 获取组件 * const health = entity.getComponent(HealthComponent); * - * // 添加位置组件 - * entity.addComponent(new PositionComponent(100, 200)); - * - * // 添加子实体 - * const weapon = new Entity("Weapon", 2); - * entity.addChild(weapon); + * // 层级关系使用 HierarchySystem + * const hierarchySystem = scene.getSystem(HierarchySystem); + * hierarchySystem.setParent(childEntity, parentEntity); * ``` */ export class Entity { @@ -89,16 +88,6 @@ export class Entity { */ private _isDestroyed: boolean = false; - /** - * 父实体引用 - */ - private _parent: Entity | null = null; - - /** - * 子实体集合 - */ - private _children: Entity[] = []; - /** * 激活状态 */ @@ -201,32 +190,6 @@ export class Entity { this._componentCache = components; } - /** - * 获取父实体 - * @returns 父实体,如果没有父实体则返回null - */ - public get parent(): Entity | null { - return this._parent; - } - - /** - * 获取子实体数组的只读副本 - * - * @returns 子实体数组的副本 - */ - public get children(): readonly Entity[] { - return [...this._children]; - } - - /** - * 获取子实体数量 - * - * @returns 子实体的数量 - */ - public get childCount(): number { - return this._children.length; - } - /** * 获取活跃状态 * @@ -239,8 +202,6 @@ export class Entity { /** * 设置活跃状态 * - * 设置实体的活跃状态,会影响子实体的有效活跃状态。 - * * @param value - 新的活跃状态 */ public set active(value: boolean) { @@ -250,19 +211,6 @@ export class Entity { } } - /** - * 获取实体的有效活跃状态 - * - * 考虑父实体的活跃状态,只有当实体本身和所有父实体都处于活跃状态时才返回true。 - * - * @returns 有效的活跃状态 - */ - public get activeInHierarchy(): boolean { - if (!this._active) return false; - if (this._parent) return this._parent.activeInHierarchy; - return true; - } - /** * 获取实体标签 * @@ -690,185 +638,6 @@ export class Entity { return null; } - /** - * 添加子实体 - * - * @param child - 要添加的子实体 - * @returns 添加的子实体 - */ - public addChild(child: Entity): Entity { - if (child === this) { - throw new Error('Entity cannot be its own child'); - } - - if (child._parent === this) { - return child; // 已经是子实体 - } - - if (child._parent) { - child._parent.removeChild(child); - } - - child._parent = this; - this._children.push(child); - - if (!child.scene && this.scene) { - child.scene = this.scene; - this.scene.addEntity(child); - } - - return child; - } - - /** - * 移除子实体 - * - * @param child - 要移除的子实体 - * @returns 是否成功移除 - */ - public removeChild(child: Entity): boolean { - const index = this._children.indexOf(child); - if (index === -1) { - return false; - } - - this._children.splice(index, 1); - child._parent = null; - - return true; - } - - /** - * 移除所有子实体 - */ - public removeAllChildren(): void { - const childrenToRemove = [...this._children]; - - for (const child of childrenToRemove) { - this.removeChild(child); - } - } - - /** - * 根据名称查找子实体 - * - * @param name - 子实体名称 - * @param recursive - 是否递归查找 - * @returns 找到的子实体或null - */ - public findChild(name: string, recursive: boolean = false): Entity | null { - for (const child of this._children) { - if (child.name === name) { - return child; - } - } - - if (recursive) { - for (const child of this._children) { - const found = child.findChild(name, true); - if (found) { - return found; - } - } - } - - return null; - } - - /** - * 根据标签查找子实体 - * - * @param tag - 标签 - * @param recursive - 是否递归查找 - * @returns 找到的子实体数组 - */ - public findChildrenByTag(tag: number, recursive: boolean = false): Entity[] { - const result: Entity[] = []; - - for (const child of this._children) { - if (child.tag === tag) { - result.push(child); - } - } - - if (recursive) { - for (const child of this._children) { - result.push(...child.findChildrenByTag(tag, true)); - } - } - - return result; - } - - /** - * 获取根实体 - * - * @returns 层次结构的根实体 - */ - public getRoot(): Entity { - if (!this._parent) { - return this; - } - return this._parent.getRoot(); - } - - /** - * 检查是否是指定实体的祖先 - * - * @param entity - 要检查的实体 - * @returns 如果是祖先则返回true - */ - public isAncestorOf(entity: Entity): boolean { - let current = entity._parent; - while (current) { - if (current === this) { - return true; - } - current = current._parent; - } - return false; - } - - /** - * 检查是否是指定实体的后代 - * - * @param entity - 要检查的实体 - * @returns 如果是后代则返回true - */ - public isDescendantOf(entity: Entity): boolean { - return entity.isAncestorOf(this); - } - - /** - * 获取层次深度 - * - * @returns 在层次结构中的深度(根实体为0) - */ - public getDepth(): number { - let depth = 0; - let current = this._parent; - while (current) { - depth++; - current = current._parent; - } - return depth; - } - - /** - * 遍历所有子实体(深度优先) - * - * @param callback - 对每个子实体执行的回调函数 - * @param recursive - 是否递归遍历 - */ - public forEachChild(callback: (child: Entity, index: number) => void, recursive: boolean = false): void { - this._children.forEach((child, index) => { - callback(child, index); - if (recursive) { - child.forEachChild(callback, true); - } - }); - } - /** * 活跃状态改变时的回调 */ @@ -882,8 +651,7 @@ export class Entity { if (this.scene && this.scene.eventSystem) { this.scene.eventSystem.emitSync('entity:activeChanged', { entity: this, - active: this._active, - activeInHierarchy: this.activeInHierarchy + active: this._active }); } } @@ -891,7 +659,8 @@ export class Entity { /** * 销毁实体 * - * 移除所有组件、子实体并标记为已销毁 + * 移除所有组件并标记为已销毁。 + * 层级关系的清理由 HierarchySystem 处理。 */ public destroy(): void { if (this._isDestroyed) { @@ -905,15 +674,6 @@ export class Entity { this.scene.referenceTracker.unregisterEntityScene(this.id); } - const childrenToDestroy = [...this._children]; - for (const child of childrenToDestroy) { - child.destroy(); - } - - if (this._parent) { - this._parent.removeChild(this); - } - this.removeAllComponents(); if (this.scene) { @@ -927,43 +687,6 @@ export class Entity { } } - /** - * 批量销毁所有子实体 - */ - public destroyAllChildren(): void { - if (this._children.length === 0) return; - - const scene = this.scene; - const toDestroy: Entity[] = []; - - const collectChildren = (entity: Entity) => { - for (const child of entity._children) { - toDestroy.push(child); - collectChildren(child); - } - }; - collectChildren(this); - - for (const entity of toDestroy) { - entity.setDestroyedState(true); - } - - for (const entity of toDestroy) { - entity.removeAllComponents(); - } - - if (scene) { - for (const entity of toDestroy) { - scene.entities.remove(entity); - scene.querySystem.removeEntity(entity); - } - - scene.clearSystemEntityCaches(); - } - - this._children.length = 0; - } - /** * 比较实体 * @@ -993,31 +716,21 @@ export class Entity { id: number; enabled: boolean; active: boolean; - activeInHierarchy: boolean; destroyed: boolean; componentCount: number; componentTypes: string[]; componentMask: string; - parentId: number | null; - childCount: number; - childIds: number[]; - depth: number; cacheBuilt: boolean; - } { + } { return { name: this.name, id: this.id, enabled: this._enabled, active: this._active, - activeInHierarchy: this.activeInHierarchy, destroyed: this._isDestroyed, componentCount: this.components.length, componentTypes: this.components.map((c) => getComponentInstanceTypeName(c)), - componentMask: BitMask64Utils.toString(this._componentMask, 2), // 二进制表示 - parentId: this._parent?.id || null, - childCount: this._children.length, - childIds: this._children.map((c) => c.id), - depth: this.getDepth(), + componentMask: BitMask64Utils.toString(this._componentMask, 2), cacheBuilt: this._componentCache !== null }; } diff --git a/packages/core/src/ECS/EntityTags.ts b/packages/core/src/ECS/EntityTags.ts new file mode 100644 index 00000000..64179c59 --- /dev/null +++ b/packages/core/src/ECS/EntityTags.ts @@ -0,0 +1,95 @@ +/** + * 实体标签常量 + * + * 用于标识特殊类型的实体,如文件夹、摄像机等。 + * 使用位掩码实现,支持多标签组合。 + * + * @example + * ```typescript + * // 创建文件夹实体 + * entity.tag = EntityTags.FOLDER; + * + * // 检查是否是文件夹 + * if ((entity.tag & EntityTags.FOLDER) !== 0) { + * // 是文件夹 + * } + * + * // 组合多个标签 + * entity.tag = EntityTags.FOLDER | EntityTags.HIDDEN; + * ``` + */ +export const EntityTags = { + /** 无标签 */ + NONE: 0x0000, + + /** 文件夹实体 - 用于场景层级中的组织分类 */ + FOLDER: 0x1000, + + /** 隐藏实体 - 在编辑器层级中不显示 */ + HIDDEN: 0x2000, + + /** 锁定实体 - 在编辑器中不可选择/编辑 */ + LOCKED: 0x4000, + + /** 编辑器专用实体 - 仅在编辑器中存在,不导出到运行时 */ + EDITOR_ONLY: 0x8000, + + /** 预制件实例 */ + PREFAB_INSTANCE: 0x0100, + + /** 预制件根节点 */ + PREFAB_ROOT: 0x0200 +} as const; + +export type EntityTagValue = (typeof EntityTags)[keyof typeof EntityTags]; + +/** + * 检查实体是否具有指定标签 + * + * @param entityTag - 实体的 tag 值 + * @param tag - 要检查的标签 + */ +export function hasEntityTag(entityTag: number, tag: EntityTagValue): boolean { + return (entityTag & tag) !== 0; +} + +/** + * 添加标签到实体 + * + * @param entityTag - 当前 tag 值 + * @param tag - 要添加的标签 + */ +export function addEntityTag(entityTag: number, tag: EntityTagValue): number { + return entityTag | tag; +} + +/** + * 从实体移除标签 + * + * @param entityTag - 当前 tag 值 + * @param tag - 要移除的标签 + */ +export function removeEntityTag(entityTag: number, tag: EntityTagValue): number { + return entityTag & ~tag; +} + +/** + * 检查实体是否是文件夹 + */ +export function isFolder(entityTag: number): boolean { + return hasEntityTag(entityTag, EntityTags.FOLDER); +} + +/** + * 检查实体是否隐藏 + */ +export function isHidden(entityTag: number): boolean { + return hasEntityTag(entityTag, EntityTags.HIDDEN); +} + +/** + * 检查实体是否锁定 + */ +export function isLocked(entityTag: number): boolean { + return hasEntityTag(entityTag, EntityTags.LOCKED); +} diff --git a/packages/core/src/ECS/Scene.ts b/packages/core/src/ECS/Scene.ts index 8921f76c..8980eab9 100644 --- a/packages/core/src/ECS/Scene.ts +++ b/packages/core/src/ECS/Scene.ts @@ -21,6 +21,9 @@ import { } from './Serialization/IncrementalSerializer'; import { ComponentPoolManager } from './Core/ComponentPool'; import { PerformanceMonitor } from '../Utils/PerformanceMonitor'; +import { ProfilerSDK } from '../Utils/Profiler/ProfilerSDK'; +import { ProfileCategory } from '../Utils/Profiler/ProfilerTypes'; +import { AutoProfiler } from '../Utils/Profiler/AutoProfiler'; import { ServiceContainer, type ServiceType, type IService } from '../Core/ServiceContainer'; import { createInstance, isInjectable, injectProperties } from '../Core/DI'; import { createLogger } from '../Utils/Logger'; @@ -334,30 +337,58 @@ export class Scene implements IScene { * 更新场景 */ public update() { - ComponentPoolManager.getInstance().update(); + // 开始性能采样帧 + ProfilerSDK.beginFrame(); + const frameHandle = ProfilerSDK.beginSample('Scene.update', ProfileCategory.ECS); - this.entities.updateLists(); + try { + ComponentPoolManager.getInstance().update(); - const systems = this.systems; + this.entities.updateLists(); - for (const system of systems) { - if (system.enabled) { - try { - system.update(); - } catch (error) { - this._handleSystemError(system, 'update', error); + const systems = this.systems; + + // Update 阶段 + const updateHandle = ProfilerSDK.beginSample('Systems.update', ProfileCategory.ECS); + try { + for (const system of systems) { + if (system.enabled) { + const systemHandle = ProfilerSDK.beginSample(system.systemName, ProfileCategory.ECS); + try { + system.update(); + } catch (error) { + this._handleSystemError(system, 'update', error); + } finally { + ProfilerSDK.endSample(systemHandle); + } + } } + } finally { + ProfilerSDK.endSample(updateHandle); } - } - for (const system of systems) { - if (system.enabled) { - try { - system.lateUpdate(); - } catch (error) { - this._handleSystemError(system, 'lateUpdate', error); + // LateUpdate 阶段 + const lateUpdateHandle = ProfilerSDK.beginSample('Systems.lateUpdate', ProfileCategory.ECS); + try { + for (const system of systems) { + if (system.enabled) { + const systemHandle = ProfilerSDK.beginSample(`${system.systemName}.late`, ProfileCategory.ECS); + try { + system.lateUpdate(); + } catch (error) { + this._handleSystemError(system, 'lateUpdate', error); + } finally { + ProfilerSDK.endSample(systemHandle); + } + } } + } finally { + ProfilerSDK.endSample(lateUpdateHandle); } + } finally { + ProfilerSDK.endSample(frameHandle); + // 结束性能采样帧 + ProfilerSDK.endFrame(); } } @@ -693,6 +724,11 @@ export class Scene implements IScene { injectProperties(system, this._services); + // 调试模式下自动包装系统方法以收集性能数据(ProfilerSDK 启用时表示调试模式) + if (ProfilerSDK.isEnabled()) { + AutoProfiler.wrapInstance(system, system.systemName, ProfileCategory.ECS); + } + system.initialize(); this.logger.debug(`System ${constructor.name} registered and initialized`); diff --git a/packages/core/src/ECS/Serialization/EntitySerializer.ts b/packages/core/src/ECS/Serialization/EntitySerializer.ts index 46aafb5f..782993fd 100644 --- a/packages/core/src/ECS/Serialization/EntitySerializer.ts +++ b/packages/core/src/ECS/Serialization/EntitySerializer.ts @@ -8,6 +8,8 @@ import { Entity } from '../Entity'; import { ComponentType } from '../Core/ComponentStorage'; import { ComponentSerializer, SerializedComponent } from './ComponentSerializer'; import { IScene } from '../IScene'; +import { HierarchyComponent } from '../Components/HierarchyComponent'; +import { HierarchySystem } from '../Systems/HierarchySystem'; /** * 序列化后的实体数据 @@ -68,9 +70,14 @@ export class EntitySerializer { * * @param entity 要序列化的实体 * @param includeChildren 是否包含子实体(默认true) + * @param hierarchySystem 层级系统(可选,用于获取层级信息) * @returns 序列化后的实体数据 */ - public static serialize(entity: Entity, includeChildren: boolean = true): SerializedEntity { + public static serialize( + entity: Entity, + includeChildren: boolean = true, + hierarchySystem?: HierarchySystem + ): SerializedEntity { const serializedComponents = ComponentSerializer.serializeComponents( Array.from(entity.components) ); @@ -86,15 +93,24 @@ export class EntitySerializer { children: [] }; - // 序列化父实体引用 - if (entity.parent) { - serializedEntity.parentId = entity.parent.id; + // 通过 HierarchyComponent 获取层级信息 + const hierarchy = entity.getComponent(HierarchyComponent); + if (hierarchy?.parentId !== null && hierarchy?.parentId !== undefined) { + serializedEntity.parentId = hierarchy.parentId; } // 序列化子实体 - if (includeChildren) { - for (const child of entity.children) { - serializedEntity.children.push(this.serialize(child, true)); + // 直接使用 HierarchyComponent.childIds 获取子实体 + if (includeChildren && hierarchy && hierarchy.childIds.length > 0) { + // 获取场景引用:优先从 hierarchySystem,否则从 entity.scene + const scene = hierarchySystem?.scene ?? entity.scene; + if (scene) { + for (const childId of hierarchy.childIds) { + const child = scene.findEntityById(childId); + if (child) { + serializedEntity.children.push(this.serialize(child, true, hierarchySystem)); + } + } } } @@ -109,6 +125,7 @@ export class EntitySerializer { * @param idGenerator 实体ID生成器(用于生成新ID或保持原ID) * @param preserveIds 是否保持原始ID(默认false) * @param scene 目标场景(可选,用于设置entity.scene以支持添加组件) + * @param hierarchySystem 层级系统(可选,用于建立层级关系) * @returns 反序列化后的实体 */ public static deserialize( @@ -116,12 +133,17 @@ export class EntitySerializer { componentRegistry: Map, idGenerator: () => number, preserveIds: boolean = false, - scene?: IScene + scene?: IScene, + hierarchySystem?: HierarchySystem | null, + allEntities?: Map ): Entity { // 创建实体(使用原始ID或新生成的ID) const entityId = preserveIds ? serializedEntity.id : idGenerator(); const entity = new Entity(serializedEntity.name, entityId); + // 将实体添加到收集 Map 中(用于后续添加到场景) + allEntities?.set(entity.id, entity); + // 如果提供了scene,先设置entity.scene以支持添加组件 if (scene) { entity.scene = scene; @@ -143,16 +165,28 @@ export class EntitySerializer { entity.addComponent(component); } - // 反序列化子实体 + // 重要:清除 HierarchyComponent 中的旧 ID + // 当 preserveIds=false 时,序列化的 parentId 和 childIds 是旧 ID,需要重新建立 + // 通过 hierarchySystem.setParent 会正确设置新的 ID + const hierarchy = entity.getComponent(HierarchyComponent); + if (hierarchy) { + hierarchy.parentId = null; + hierarchy.childIds = []; + } + + // 反序列化子实体并建立层级关系 for (const childData of serializedEntity.children) { const childEntity = this.deserialize( childData, componentRegistry, idGenerator, preserveIds, - scene + scene, + hierarchySystem, + allEntities ); - entity.addChild(childEntity); + // 使用 HierarchySystem 建立层级关系 + hierarchySystem?.setParent(childEntity, entity); } return entity; @@ -163,19 +197,23 @@ export class EntitySerializer { * * @param entities 实体数组 * @param includeChildren 是否包含子实体 + * @param hierarchySystem 层级系统(可选,用于获取层级信息) * @returns 序列化后的实体数据数组 */ public static serializeEntities( entities: Entity[], - includeChildren: boolean = true + includeChildren: boolean = true, + hierarchySystem?: HierarchySystem ): SerializedEntity[] { const result: SerializedEntity[] = []; for (const entity of entities) { // 只序列化顶层实体(没有父实体的实体) // 子实体会在父实体序列化时一并处理 - if (!entity.parent || !includeChildren) { - result.push(this.serialize(entity, includeChildren)); + const hierarchy = entity.getComponent(HierarchyComponent); + const bHasParent = hierarchy?.parentId !== null && hierarchy?.parentId !== undefined; + if (!bHasParent || !includeChildren) { + result.push(this.serialize(entity, includeChildren, hierarchySystem)); } } @@ -190,6 +228,7 @@ export class EntitySerializer { * @param idGenerator 实体ID生成器 * @param preserveIds 是否保持原始ID * @param scene 目标场景(可选,用于设置entity.scene以支持添加组件) + * @param hierarchySystem 层级系统(可选,用于建立层级关系) * @returns 反序列化后的实体数组 */ public static deserializeEntities( @@ -197,9 +236,11 @@ export class EntitySerializer { componentRegistry: Map, idGenerator: () => number, preserveIds: boolean = false, - scene?: IScene - ): Entity[] { - const result: Entity[] = []; + scene?: IScene, + hierarchySystem?: HierarchySystem | null + ): { rootEntities: Entity[]; allEntities: Map } { + const rootEntities: Entity[] = []; + const allEntities = new Map(); for (const serialized of serializedEntities) { const entity = this.deserialize( @@ -207,12 +248,14 @@ export class EntitySerializer { componentRegistry, idGenerator, preserveIds, - scene + scene, + hierarchySystem, + allEntities ); - result.push(entity); + rootEntities.push(entity); } - return result; + return { rootEntities, allEntities }; } /** diff --git a/packages/core/src/ECS/Serialization/IncrementalSerializer.ts b/packages/core/src/ECS/Serialization/IncrementalSerializer.ts index b00d2ef7..2e6a21ba 100644 --- a/packages/core/src/ECS/Serialization/IncrementalSerializer.ts +++ b/packages/core/src/ECS/Serialization/IncrementalSerializer.ts @@ -11,6 +11,8 @@ import { ComponentSerializer, SerializedComponent } from './ComponentSerializer' import { SerializedEntity } from './EntitySerializer'; import { ComponentType } from '../Core/ComponentStorage'; import { BinarySerializer } from '../../Utils/BinarySerializer'; +import { HierarchyComponent } from '../Components/HierarchyComponent'; +import { HierarchySystem } from '../Systems/HierarchySystem'; /** * 变更操作类型 @@ -196,6 +198,10 @@ export class IncrementalSerializer { for (const entity of scene.entities.buffer) { snapshot.entityIds.add(entity.id); + // 获取层级信息 + const hierarchy = entity.getComponent(HierarchyComponent); + const parentId = hierarchy?.parentId; + // 存储实体基本信息 snapshot.entities.set(entity.id, { name: entity.name, @@ -203,7 +209,7 @@ export class IncrementalSerializer { active: entity.active, enabled: entity.enabled, updateOrder: entity.updateOrder, - ...(entity.parent && { parentId: entity.parent.id }) + ...(parentId !== null && parentId !== undefined && { parentId }) }); // 快照组件 @@ -272,6 +278,10 @@ export class IncrementalSerializer { for (const entity of scene.entities.buffer) { currentEntityIds.add(entity.id); + // 获取层级信息 + const hierarchy = entity.getComponent(HierarchyComponent); + const parentId = hierarchy?.parentId; + if (!baseSnapshot.entityIds.has(entity.id)) { // 新增实体 incremental.entityChanges.push({ @@ -285,7 +295,7 @@ export class IncrementalSerializer { active: entity.active, enabled: entity.enabled, updateOrder: entity.updateOrder, - ...(entity.parent && { parentId: entity.parent.id }), + ...(parentId !== null && parentId !== undefined && { parentId }), components: [], children: [] } @@ -312,7 +322,7 @@ export class IncrementalSerializer { oldData.active !== entity.active || oldData.enabled !== entity.enabled || oldData.updateOrder !== entity.updateOrder || - oldData.parentId !== entity.parent?.id; + oldData.parentId !== parentId; if (entityChanged) { incremental.entityChanges.push({ @@ -324,7 +334,7 @@ export class IncrementalSerializer { active: entity.active, enabled: entity.enabled, updateOrder: entity.updateOrder, - ...(entity.parent && { parentId: entity.parent.id }) + ...(parentId !== null && parentId !== undefined && { parentId }) } }); } @@ -539,16 +549,20 @@ export class IncrementalSerializer { if (change.entityData.enabled !== undefined) entity.enabled = change.entityData.enabled; if (change.entityData.updateOrder !== undefined) entity.updateOrder = change.entityData.updateOrder; - if (change.entityData.parentId !== undefined) { - const newParent = scene.entities.findEntityById(change.entityData.parentId); - if (newParent && entity.parent !== newParent) { - if (entity.parent) { - entity.parent.removeChild(entity); + // 使用 HierarchySystem 更新层级关系 + const hierarchySystem = scene.getSystem(HierarchySystem); + if (hierarchySystem) { + const hierarchy = entity.getComponent(HierarchyComponent); + const currentParentId = hierarchy?.parentId; + + if (change.entityData.parentId !== undefined) { + const newParent = scene.entities.findEntityById(change.entityData.parentId); + if (newParent && currentParentId !== change.entityData.parentId) { + hierarchySystem.setParent(entity, newParent); } - newParent.addChild(entity); + } else if (currentParentId !== null && currentParentId !== undefined) { + hierarchySystem.setParent(entity, null); } - } else if (entity.parent) { - entity.parent.removeChild(entity); } } diff --git a/packages/core/src/ECS/Serialization/SceneSerializer.ts b/packages/core/src/ECS/Serialization/SceneSerializer.ts index b476b130..2991ab5f 100644 --- a/packages/core/src/ECS/Serialization/SceneSerializer.ts +++ b/packages/core/src/ECS/Serialization/SceneSerializer.ts @@ -11,6 +11,8 @@ import { EntitySerializer, SerializedEntity } from './EntitySerializer'; import { getComponentTypeName } from '../Decorators'; import { getSerializationMetadata } from './SerializationDecorators'; import { BinarySerializer } from '../../Utils/BinarySerializer'; +import { HierarchySystem } from '../Systems/HierarchySystem'; +import { HierarchyComponent } from '../Components/HierarchyComponent'; /** * 场景序列化格式 @@ -167,8 +169,11 @@ export class SceneSerializer { // 过滤实体和组件 const entities = this.filterEntities(scene, opts); - // 序列化实体 - const serializedEntities = EntitySerializer.serializeEntities(entities, true); + // 获取层级系统用于序列化子实体 + const hierarchySystem = scene.getSystem(HierarchySystem); + + // 序列化实体(传入 hierarchySystem 以正确序列化子实体) + const serializedEntities = EntitySerializer.serializeEntities(entities, true, hierarchySystem ?? undefined); // 收集组件类型信息 const componentTypeRegistry = this.buildComponentTypeRegistry(entities); @@ -258,19 +263,24 @@ export class SceneSerializer { // ID生成器 const idGenerator = () => scene.identifierPool.checkOut(); + // 获取层级系统 + const hierarchySystem = scene.getSystem(HierarchySystem); + // 反序列化实体 - const entities = EntitySerializer.deserializeEntities( + const { rootEntities, allEntities } = EntitySerializer.deserializeEntities( serializedScene.entities, componentRegistry, idGenerator, opts.preserveIds || false, - scene + scene, + hierarchySystem ); - // 将实体添加到场景 - for (const entity of entities) { + // 将所有实体添加到场景(包括子实体) + // 先添加根实体,再递归添加子实体 + for (const entity of rootEntities) { scene.addEntity(entity, true); - this.addChildrenRecursively(entity, scene); + this.addChildrenRecursively(entity, scene, hierarchySystem, allEntities); } // 统一清理缓存(批量操作完成后) @@ -285,8 +295,8 @@ export class SceneSerializer { // 调用所有组件的 onDeserialized 生命周期方法 // Call onDeserialized lifecycle method on all components const deserializedPromises: Promise[] = []; - for (const entity of entities) { - this.callOnDeserializedRecursively(entity, deserializedPromises); + for (const entity of allEntities.values()) { + this.callOnDeserializedForEntity(entity, deserializedPromises); } // 如果有异步的 onDeserialized,在后台执行 @@ -298,9 +308,12 @@ export class SceneSerializer { } /** - * 递归调用实体及其子实体所有组件的 onDeserialized 方法 + * 调用实体所有组件的 onDeserialized 方法(不递归) */ - private static callOnDeserializedRecursively(entity: Entity, promises: Promise[]): void { + private static callOnDeserializedForEntity( + entity: Entity, + promises: Promise[] + ): void { for (const component of entity.components) { try { const result = component.onDeserialized(); @@ -311,10 +324,6 @@ export class SceneSerializer { console.error(`Error calling onDeserialized on component ${component.constructor.name}:`, error); } } - - for (const child of entity.children) { - this.callOnDeserializedRecursively(child, promises); - } } /** @@ -327,11 +336,27 @@ export class SceneSerializer { * * @param entity 父实体 * @param scene 目标场景 + * @param hierarchySystem 层级系统 */ - private static addChildrenRecursively(entity: Entity, scene: IScene): void { - for (const child of entity.children) { - scene.addEntity(child, true); // 延迟缓存清理 - this.addChildrenRecursively(child, scene); // 递归处理子实体的子实体 + private static addChildrenRecursively( + entity: Entity, + scene: IScene, + hierarchySystem?: HierarchySystem | null, + childEntitiesMap?: Map + ): void { + const hierarchy = entity.getComponent(HierarchyComponent); + if (!hierarchy || hierarchy.childIds.length === 0) return; + + // 获取子实体 + // 注意:此时子实体还没有被添加到场景,所以不能用 scene.findEntityById + // 需要从 childEntitiesMap 中查找(如果提供了的话) + for (const childId of hierarchy.childIds) { + // 尝试从 map 中获取,否则从场景获取(用于已添加的情况) + const child = childEntitiesMap?.get(childId) ?? scene.findEntityById(childId); + if (child) { + scene.addEntity(child, true); // 延迟缓存清理 + this.addChildrenRecursively(child, scene, hierarchySystem, childEntitiesMap); + } } } diff --git a/packages/core/src/ECS/Systems/HierarchySystem.ts b/packages/core/src/ECS/Systems/HierarchySystem.ts new file mode 100644 index 00000000..e10cb872 --- /dev/null +++ b/packages/core/src/ECS/Systems/HierarchySystem.ts @@ -0,0 +1,549 @@ +import { Entity } from '../Entity'; +import { EntitySystem } from './EntitySystem'; +import { Matcher } from '../Utils/Matcher'; +import { HierarchyComponent } from '../Components/HierarchyComponent'; + +/** + * 层级关系系统 - 管理实体间的父子关系 + * + * 提供层级操作的统一 API,维护层级缓存(depth、activeInHierarchy)。 + * 所有层级操作应通过此系统进行,而非直接修改 HierarchyComponent。 + * + * @example + * ```typescript + * const hierarchySystem = scene.getSystem(HierarchySystem); + * + * // 设置父子关系 + * hierarchySystem.setParent(child, parent); + * + * // 查询层级 + * const parent = hierarchySystem.getParent(entity); + * const children = hierarchySystem.getChildren(entity); + * const depth = hierarchySystem.getDepth(entity); + * ``` + */ +export class HierarchySystem extends EntitySystem { + private static readonly MAX_DEPTH = 32; + + constructor() { + super(Matcher.empty().all(HierarchyComponent)); + } + + /** + * 系统优先级,确保在其他系统之前更新层级缓存 + */ + public override get updateOrder(): number { + return -1000; + } + + protected override process(): void { + // 更新所有脏缓存 + for (const entity of this.entities) { + const hierarchy = entity.getComponent(HierarchyComponent); + if (hierarchy?.bCacheDirty) { + this.updateHierarchyCache(entity); + } + } + } + + /** + * 设置实体的父级 + * + * @param child - 子实体 + * @param parent - 父实体,null 表示移动到根级 + */ + public setParent(child: Entity, parent: Entity | null): void { + let childHierarchy = child.getComponent(HierarchyComponent); + + // 如果子实体没有 HierarchyComponent,自动添加 + if (!childHierarchy) { + childHierarchy = new HierarchyComponent(); + child.addComponent(childHierarchy); + } + + // 检查是否需要变更 + const currentParentId = childHierarchy.parentId; + const newParentId = parent?.id ?? null; + if (currentParentId === newParentId) { + return; + } + + // 防止循环引用 + if (parent && this.isAncestorOf(child, parent)) { + throw new Error('Cannot set parent: would create circular reference'); + } + + // 从旧父级移除 + if (currentParentId !== null) { + const oldParent = this.scene?.findEntityById(currentParentId); + if (oldParent) { + const oldParentHierarchy = oldParent.getComponent(HierarchyComponent); + if (oldParentHierarchy) { + const idx = oldParentHierarchy.childIds.indexOf(child.id); + if (idx !== -1) { + oldParentHierarchy.childIds.splice(idx, 1); + } + } + } + } + + // 添加到新父级 + if (parent) { + let parentHierarchy = parent.getComponent(HierarchyComponent); + if (!parentHierarchy) { + parentHierarchy = new HierarchyComponent(); + parent.addComponent(parentHierarchy); + } + childHierarchy.parentId = parent.id; + parentHierarchy.childIds.push(child.id); + } else { + childHierarchy.parentId = null; + } + + // 标记缓存脏 + this.markCacheDirty(child); + } + + /** + * 在指定位置插入子实体 + * + * @param parent - 父实体 + * @param child - 子实体 + * @param index - 插入位置索引,-1 表示追加到末尾 + */ + public insertChildAt(parent: Entity, child: Entity, index: number): void { + let childHierarchy = child.getComponent(HierarchyComponent); + let parentHierarchy = parent.getComponent(HierarchyComponent); + + // 自动添加 HierarchyComponent + if (!childHierarchy) { + childHierarchy = new HierarchyComponent(); + child.addComponent(childHierarchy); + } + if (!parentHierarchy) { + parentHierarchy = new HierarchyComponent(); + parent.addComponent(parentHierarchy); + } + + // 防止循环引用 + if (this.isAncestorOf(child, parent)) { + throw new Error('Cannot set parent: would create circular reference'); + } + + // 从旧父级移除 + if (childHierarchy.parentId !== null && childHierarchy.parentId !== parent.id) { + const oldParent = this.scene?.findEntityById(childHierarchy.parentId); + if (oldParent) { + const oldParentHierarchy = oldParent.getComponent(HierarchyComponent); + if (oldParentHierarchy) { + const idx = oldParentHierarchy.childIds.indexOf(child.id); + if (idx !== -1) { + oldParentHierarchy.childIds.splice(idx, 1); + } + } + } + } + + // 设置新父级 + childHierarchy.parentId = parent.id; + + // 从当前父级的子列表中移除(如果已存在) + const existingIdx = parentHierarchy.childIds.indexOf(child.id); + if (existingIdx !== -1) { + parentHierarchy.childIds.splice(existingIdx, 1); + } + + // 插入到指定位置 + if (index < 0 || index >= parentHierarchy.childIds.length) { + parentHierarchy.childIds.push(child.id); + } else { + parentHierarchy.childIds.splice(index, 0, child.id); + } + + // 标记缓存脏 + this.markCacheDirty(child); + } + + /** + * 移除子实体(将其移动到根级) + */ + public removeChild(parent: Entity, child: Entity): boolean { + const parentHierarchy = parent.getComponent(HierarchyComponent); + const childHierarchy = child.getComponent(HierarchyComponent); + + if (!parentHierarchy || !childHierarchy) { + return false; + } + + if (childHierarchy.parentId !== parent.id) { + return false; + } + + const idx = parentHierarchy.childIds.indexOf(child.id); + if (idx !== -1) { + parentHierarchy.childIds.splice(idx, 1); + } + + childHierarchy.parentId = null; + this.markCacheDirty(child); + + return true; + } + + /** + * 移除所有子实体 + */ + public removeAllChildren(parent: Entity): void { + const parentHierarchy = parent.getComponent(HierarchyComponent); + if (!parentHierarchy) return; + + const childIds = [...parentHierarchy.childIds]; + for (const childId of childIds) { + const child = this.scene?.findEntityById(childId); + if (child) { + this.removeChild(parent, child); + } + } + } + + /** + * 获取实体的父级 + */ + public getParent(entity: Entity): Entity | null { + const hierarchy = entity.getComponent(HierarchyComponent); + if (!hierarchy || hierarchy.parentId === null) { + return null; + } + return this.scene?.findEntityById(hierarchy.parentId) ?? null; + } + + /** + * 获取实体的子级列表 + */ + public getChildren(entity: Entity): Entity[] { + const hierarchy = entity.getComponent(HierarchyComponent); + if (!hierarchy) return []; + + const children: Entity[] = []; + for (const childId of hierarchy.childIds) { + const child = this.scene?.findEntityById(childId); + if (child) { + children.push(child); + } + } + return children; + } + + /** + * 获取子级数量 + */ + public getChildCount(entity: Entity): number { + const hierarchy = entity.getComponent(HierarchyComponent); + return hierarchy?.childIds.length ?? 0; + } + + /** + * 检查实体是否有子级 + */ + public hasChildren(entity: Entity): boolean { + return this.getChildCount(entity) > 0; + } + + /** + * 检查 ancestor 是否是 entity 的祖先 + */ + public isAncestorOf(ancestor: Entity, entity: Entity): boolean { + let current = this.getParent(entity); + let depth = 0; + + while (current && depth < HierarchySystem.MAX_DEPTH) { + if (current.id === ancestor.id) { + return true; + } + current = this.getParent(current); + depth++; + } + + return false; + } + + /** + * 检查 descendant 是否是 entity 的后代 + */ + public isDescendantOf(descendant: Entity, entity: Entity): boolean { + return this.isAncestorOf(entity, descendant); + } + + /** + * 获取根实体 + */ + public getRoot(entity: Entity): Entity { + let current = entity; + let parent = this.getParent(current); + let depth = 0; + + while (parent && depth < HierarchySystem.MAX_DEPTH) { + current = parent; + parent = this.getParent(current); + depth++; + } + + return current; + } + + /** + * 获取实体在层级中的深度 + */ + public getDepth(entity: Entity): number { + const hierarchy = entity.getComponent(HierarchyComponent); + if (!hierarchy) return 0; + + // 如果缓存有效,直接返回 + if (!hierarchy.bCacheDirty) { + return hierarchy.depth; + } + + // 重新计算 + let depth = 0; + let current = this.getParent(entity); + while (current && depth < HierarchySystem.MAX_DEPTH) { + depth++; + current = this.getParent(current); + } + + hierarchy.depth = depth; + return depth; + } + + /** + * 检查实体在层级中是否激活 + */ + public isActiveInHierarchy(entity: Entity): boolean { + if (!entity.active) return false; + + const hierarchy = entity.getComponent(HierarchyComponent); + if (!hierarchy) return entity.active; + + // 如果缓存有效,直接返回 + if (!hierarchy.bCacheDirty) { + return hierarchy.bActiveInHierarchy; + } + + // 重新计算 + const parent = this.getParent(entity); + if (!parent) { + hierarchy.bActiveInHierarchy = entity.active; + } else { + hierarchy.bActiveInHierarchy = entity.active && this.isActiveInHierarchy(parent); + } + + return hierarchy.bActiveInHierarchy; + } + + /** + * 获取所有根实体(没有父级的实体) + */ + public getRootEntities(): Entity[] { + const roots: Entity[] = []; + for (const entity of this.entities) { + const hierarchy = entity.getComponent(HierarchyComponent); + if (hierarchy && hierarchy.parentId === null) { + roots.push(entity); + } + } + return roots; + } + + /** + * 根据名称查找子实体 + * + * @param entity - 父实体 + * @param name - 子实体名称 + * @param bRecursive - 是否递归查找 + */ + public findChild(entity: Entity, name: string, bRecursive: boolean = false): Entity | null { + const children = this.getChildren(entity); + + for (const child of children) { + if (child.name === name) { + return child; + } + } + + if (bRecursive) { + for (const child of children) { + const found = this.findChild(child, name, true); + if (found) { + return found; + } + } + } + + return null; + } + + /** + * 根据标签查找子实体 + * + * @param entity - 父实体 + * @param tag - 标签值 + * @param bRecursive - 是否递归查找 + */ + public findChildrenByTag(entity: Entity, tag: number, bRecursive: boolean = false): Entity[] { + const result: Entity[] = []; + const children = this.getChildren(entity); + + for (const child of children) { + if ((child.tag & tag) !== 0) { + result.push(child); + } + + if (bRecursive) { + result.push(...this.findChildrenByTag(child, tag, true)); + } + } + + return result; + } + + /** + * 遍历所有子级 + * + * @param entity - 父实体 + * @param callback - 回调函数 + * @param bRecursive - 是否递归遍历 + */ + public forEachChild( + entity: Entity, + callback: (child: Entity) => void, + bRecursive: boolean = false + ): void { + const children = this.getChildren(entity); + + for (const child of children) { + callback(child); + + if (bRecursive) { + this.forEachChild(child, callback, true); + } + } + } + + /** + * 扁平化层级树(用于虚拟化渲染) + * + * @param expandedIds - 展开的实体 ID 集合 + * @returns 扁平化的节点列表 + */ + public flattenHierarchy(expandedIds: Set): Array<{ + entity: Entity; + depth: number; + bHasChildren: boolean; + bIsExpanded: boolean; + }> { + const result: Array<{ + entity: Entity; + depth: number; + bHasChildren: boolean; + bIsExpanded: boolean; + }> = []; + + const traverse = (entity: Entity, depth: number): void => { + const bHasChildren = this.hasChildren(entity); + const bIsExpanded = expandedIds.has(entity.id); + + result.push({ + entity, + depth, + bHasChildren, + bIsExpanded + }); + + if (bHasChildren && bIsExpanded) { + for (const child of this.getChildren(entity)) { + traverse(child, depth + 1); + } + } + }; + + for (const root of this.getRootEntities()) { + traverse(root, 0); + } + + return result; + } + + /** + * 标记缓存为脏 + */ + private markCacheDirty(entity: Entity): void { + const hierarchy = entity.getComponent(HierarchyComponent); + if (!hierarchy) return; + + hierarchy.bCacheDirty = true; + + // 递归标记所有子级 + for (const childId of hierarchy.childIds) { + const child = this.scene?.findEntityById(childId); + if (child) { + this.markCacheDirty(child); + } + } + } + + /** + * 更新层级缓存 + */ + private updateHierarchyCache(entity: Entity): void { + const hierarchy = entity.getComponent(HierarchyComponent); + if (!hierarchy) return; + + // 计算深度 + hierarchy.depth = this.getDepth(entity); + + // 计算激活状态 + hierarchy.bActiveInHierarchy = this.isActiveInHierarchy(entity); + + // 标记缓存有效 + hierarchy.bCacheDirty = false; + } + + /** + * 当实体被移除时清理层级关系 + */ + protected onEntityRemoved(entity: Entity): void { + const hierarchy = entity.getComponent(HierarchyComponent); + if (!hierarchy) return; + + // 从父级移除 + if (hierarchy.parentId !== null) { + const parent = this.scene?.findEntityById(hierarchy.parentId); + if (parent) { + const parentHierarchy = parent.getComponent(HierarchyComponent); + if (parentHierarchy) { + const idx = parentHierarchy.childIds.indexOf(entity.id); + if (idx !== -1) { + parentHierarchy.childIds.splice(idx, 1); + } + } + } + } + + // 处理子级:可选择销毁或移动到根级 + // 默认将子级移动到根级 + for (const childId of hierarchy.childIds) { + const child = this.scene?.findEntityById(childId); + if (child) { + const childHierarchy = child.getComponent(HierarchyComponent); + if (childHierarchy) { + childHierarchy.parentId = null; + this.markCacheDirty(child); + } + } + } + } + + public override dispose(): void { + // 清理资源 + } +} diff --git a/packages/core/src/ECS/Systems/index.ts b/packages/core/src/ECS/Systems/index.ts index 3548a3e3..dfa1cbce 100644 --- a/packages/core/src/ECS/Systems/index.ts +++ b/packages/core/src/ECS/Systems/index.ts @@ -4,6 +4,7 @@ export { ProcessingSystem } from './ProcessingSystem'; export { PassiveSystem } from './PassiveSystem'; export { IntervalSystem } from './IntervalSystem'; export { WorkerEntitySystem } from './WorkerEntitySystem'; +export { HierarchySystem } from './HierarchySystem'; // Worker系统相关类型导出 export type { diff --git a/packages/core/src/ECS/TypedEntity.ts b/packages/core/src/ECS/TypedEntity.ts index a61d3763..8913acc7 100644 --- a/packages/core/src/ECS/TypedEntity.ts +++ b/packages/core/src/ECS/TypedEntity.ts @@ -8,6 +8,7 @@ import { Entity } from './Entity'; import type { Component } from './Component'; import type { ComponentType } from './Core/ComponentStorage'; import type { ComponentConstructor, ComponentInstance } from '../Types/TypeHelpers'; +import { HierarchySystem } from './Systems/HierarchySystem'; /** * 获取组件,如果不存在则抛出错误 @@ -277,7 +278,8 @@ export class TypedEntityBuilder { * 添加子实体 */ withChild(child: Entity): this { - this._entity.addChild(child); + const hierarchySystem = this._entity.scene?.getSystem(HierarchySystem); + hierarchySystem?.setParent(child, this._entity); return this; } diff --git a/packages/core/src/ECS/index.ts b/packages/core/src/ECS/index.ts index 4c37f576..f5f8f31d 100644 --- a/packages/core/src/ECS/index.ts +++ b/packages/core/src/ECS/index.ts @@ -4,6 +4,7 @@ export { ECSEventType, EventPriority, EVENT_TYPES, EventTypeValidator } from './ export * from './Systems'; export * from './Utils'; export * from './Decorators'; +export * from './Components'; export { Scene } from './Scene'; export { IScene, ISceneFactory, ISceneConfig } from './IScene'; export { SceneManager } from './SceneManager'; @@ -18,3 +19,4 @@ export { ReferenceTracker, getSceneByEntityId } from './Core/ReferenceTracker'; export type { EntityRefRecord } from './Core/ReferenceTracker'; export { ReactiveQuery, ReactiveQueryChangeType } from './Core/ReactiveQuery'; export type { ReactiveQueryChange, ReactiveQueryListener, ReactiveQueryConfig } from './Core/ReactiveQuery'; +export * from './EntityTags'; diff --git a/packages/core/src/Types/index.ts b/packages/core/src/Types/index.ts index f196a6e1..d51a9338 100644 --- a/packages/core/src/Types/index.ts +++ b/packages/core/src/Types/index.ts @@ -264,8 +264,6 @@ export interface IECSDebugConfig { export interface ICoreConfig { /** 是否启用调试模式 */ debug?: boolean; - /** 是否启用实体系统 */ - enableEntitySystems?: boolean; /** 调试配置 */ debugConfig?: IECSDebugConfig; /** WorldManager配置 */ diff --git a/packages/core/src/Utils/Debug/AdvancedProfilerCollector.ts b/packages/core/src/Utils/Debug/AdvancedProfilerCollector.ts index cade8938..2eea6430 100644 --- a/packages/core/src/Utils/Debug/AdvancedProfilerCollector.ts +++ b/packages/core/src/Utils/Debug/AdvancedProfilerCollector.ts @@ -30,6 +30,24 @@ export interface ILegacyPerformanceMonitor { }>; } +/** + * 热点函数项(支持递归层级) + */ +export interface IHotspotItem { + name: string; + category: string; + inclusiveTime: number; + inclusiveTimePercent: number; + exclusiveTime: number; + exclusiveTimePercent: number; + callCount: number; + avgCallTime: number; + /** 层级深度 */ + depth: number; + /** 子函数 */ + children?: IHotspotItem[] | undefined; +} + /** * 高级性能数据接口 */ @@ -63,17 +81,8 @@ export interface IAdvancedProfilerData { percentOfFrame: number; }>; }>; - /** 热点函数列表 */ - hotspots: Array<{ - name: string; - category: string; - inclusiveTime: number; - inclusiveTimePercent: number; - exclusiveTime: number; - exclusiveTimePercent: number; - callCount: number; - avgCallTime: number; - }>; + /** 热点函数列表(支持层级) */ + hotspots: IHotspotItem[]; /** 调用关系数据 */ callGraph: { /** 当前选中的函数 */ @@ -332,16 +341,91 @@ export class AdvancedProfilerCollector { private buildHotspots(report: ProfileReport): IAdvancedProfilerData['hotspots'] { const totalTime = report.hotspots.reduce((sum, h) => sum + h.inclusiveTime, 0) || 1; - return report.hotspots.slice(0, 50).map(h => ({ - name: h.name, - category: h.category, - inclusiveTime: h.inclusiveTime, - inclusiveTimePercent: (h.inclusiveTime / totalTime) * 100, - exclusiveTime: h.exclusiveTime, - exclusiveTimePercent: (h.exclusiveTime / totalTime) * 100, - callCount: h.callCount, - avgCallTime: h.averageTime - })); + // 使用 callGraph 构建层级结构 + // 找出所有根节点(没有被任何函数调用的,或者是顶层函数) + const rootFunctions = new Set(); + const childFunctions = new Set(); + + for (const [name, node] of report.callGraph) { + // 如果没有调用者,或者调用者不在 hotspots 中,则是根节点 + if (node.callers.size === 0) { + rootFunctions.add(name); + } else { + // 检查是否所有调用者都在 hotspots 之外 + let hasParentInHotspots = false; + for (const callerName of node.callers.keys()) { + if (report.callGraph.has(callerName)) { + hasParentInHotspots = true; + childFunctions.add(name); + break; + } + } + if (!hasParentInHotspots) { + rootFunctions.add(name); + } + } + } + + // 递归构建层级热点数据 + const buildHotspotItem = ( + name: string, + depth: number, + visited: Set + ): IHotspotItem | null => { + if (visited.has(name)) return null; // 避免循环 + visited.add(name); + + const stats = report.hotspots.find(h => h.name === name); + const node = report.callGraph.get(name); + + if (!stats && !node) return null; + + const inclusiveTime = stats?.inclusiveTime || node?.totalTime || 0; + const exclusiveTime = stats?.exclusiveTime || inclusiveTime; + const callCount = stats?.callCount || node?.callCount || 1; + + // 构建子节点 + const children: IHotspotItem[] = []; + if (node && depth < 5) { // 限制深度避免过深 + for (const [calleeName] of node.callees) { + const child = buildHotspotItem(calleeName, depth + 1, visited); + if (child) { + children.push(child); + } + } + // 按耗时排序 + children.sort((a, b) => b.inclusiveTime - a.inclusiveTime); + } + + return { + name, + category: stats?.category || node?.category || ProfileCategory.Custom, + inclusiveTime, + inclusiveTimePercent: (inclusiveTime / totalTime) * 100, + exclusiveTime, + exclusiveTimePercent: (exclusiveTime / totalTime) * 100, + callCount, + avgCallTime: callCount > 0 ? inclusiveTime / callCount : 0, + depth, + children: children.length > 0 ? children : undefined + }; + }; + + // 构建根节点列表 + const result: IAdvancedProfilerData['hotspots'] = []; + const visited = new Set(); + + for (const rootName of rootFunctions) { + const item = buildHotspotItem(rootName, 0, visited); + if (item) { + result.push(item); + } + } + + // 按耗时排序 + result.sort((a, b) => b.inclusiveTime - a.inclusiveTime); + + return result.slice(0, 50); } private buildHotspotsFromLegacy( @@ -363,7 +447,8 @@ export class AdvancedProfilerCollector { exclusiveTime: execTime, exclusiveTimePercent: frameTime > 0 ? (execTime / frameTime) * 100 : 0, callCount: stats?.executionCount || 1, - avgCallTime: stats?.averageTime || execTime + avgCallTime: stats?.averageTime || execTime, + depth: 0 }); } @@ -388,15 +473,27 @@ export class AdvancedProfilerCollector { }; } + // 计算所有调用者的总调用次数(用于计算调用者的百分比) + let totalCallerCount = 0; + for (const data of node.callers.values()) { + totalCallerCount += data.count; + } + + // Calling Functions(谁调用了我) + // - totalTime: 该调用者调用当前函数时的平均耗时 + // - percentOfCurrent: 该调用者的调用次数占总调用次数的百分比 const callers = Array.from(node.callers.entries()) .map(([name, data]) => ({ name, callCount: data.count, totalTime: data.totalTime, - percentOfCurrent: node.totalTime > 0 ? (data.totalTime / node.totalTime) * 100 : 0 + percentOfCurrent: totalCallerCount > 0 ? (data.count / totalCallerCount) * 100 : 0 })) - .sort((a, b) => b.totalTime - a.totalTime); + .sort((a, b) => b.callCount - a.callCount); + // Called Functions(我调用了谁) + // - totalTime: 当前函数调用该被调用者时的平均耗时 + // - percentOfCurrent: 该被调用者的耗时占当前函数耗时的百分比 const callees = Array.from(node.callees.entries()) .map(([name, data]) => ({ name, diff --git a/packages/core/src/Utils/Debug/DebugManager.ts b/packages/core/src/Utils/Debug/DebugManager.ts index 70148320..afb0f25c 100644 --- a/packages/core/src/Utils/Debug/DebugManager.ts +++ b/packages/core/src/Utils/Debug/DebugManager.ts @@ -486,6 +486,9 @@ export class DebugManager implements IService, IUpdatable { const { functionName, requestId } = message; this.advancedProfilerCollector.setSelectedFunction(functionName || null); + // 立即发送更新的数据,无需等待下一帧 + this.sendDebugData(); + this.webSocketManager.send({ type: 'set_profiler_selected_function_response', requestId, @@ -995,10 +998,19 @@ export class DebugManager implements IService, IUpdatable { try { const debugData = this.getDebugData(); + + // 收集高级性能数据(包含 callGraph) + const isProfilerEnabled = ProfilerSDK.isEnabled(); + + const advancedProfilerData = isProfilerEnabled + ? this.advancedProfilerCollector.collectAdvancedData(this.performanceMonitor) + : null; + // 包装成调试面板期望的消息格式 const message = { type: 'debug_data', - data: debugData + data: debugData, + advancedProfiler: advancedProfilerData }; this.webSocketManager.send(message); } catch (error) { diff --git a/packages/core/src/Utils/Debug/EntityDataCollector.ts b/packages/core/src/Utils/Debug/EntityDataCollector.ts index 2b8d033a..75262d62 100644 --- a/packages/core/src/Utils/Debug/EntityDataCollector.ts +++ b/packages/core/src/Utils/Debug/EntityDataCollector.ts @@ -3,6 +3,8 @@ import { Entity } from '../../ECS/Entity'; import { Component } from '../../ECS/Component'; import { getComponentInstanceTypeName } from '../../ECS/Decorators'; import { IScene } from '../../ECS/IScene'; +import { HierarchyComponent } from '../../ECS/Components/HierarchyComponent'; +import { HierarchySystem } from '../../ECS/Systems/HierarchySystem'; /** * 实体数据收集器 @@ -75,20 +77,28 @@ export class EntityDataCollector { const entityList = (scene as any).entities; if (!entityList?.buffer) return []; - return entityList.buffer.map((entity: Entity) => ({ - id: entity.id, - name: entity.name || `Entity_${entity.id}`, - active: entity.active !== false, - enabled: entity.enabled !== false, - activeInHierarchy: entity.activeInHierarchy !== false, - componentCount: entity.components.length, - componentTypes: entity.components.map((component: Component) => getComponentInstanceTypeName(component)), - parentId: entity.parent?.id || null, - childIds: entity.children?.map((child: Entity) => child.id) || [], - depth: entity.getDepth ? entity.getDepth() : 0, - tag: entity.tag || 0, - updateOrder: entity.updateOrder || 0 - })); + const hierarchySystem = scene.getSystem(HierarchySystem); + + return entityList.buffer.map((entity: Entity) => { + const hierarchy = entity.getComponent(HierarchyComponent); + const bActiveInHierarchy = hierarchySystem?.isActiveInHierarchy(entity) ?? entity.active; + const depth = hierarchySystem?.getDepth(entity) ?? 0; + + return { + id: entity.id, + name: entity.name || `Entity_${entity.id}`, + active: entity.active !== false, + enabled: entity.enabled !== false, + activeInHierarchy: bActiveInHierarchy, + componentCount: entity.components.length, + componentTypes: entity.components.map((component: Component) => getComponentInstanceTypeName(component)), + parentId: hierarchy?.parentId ?? null, + childIds: hierarchy?.childIds ?? [], + depth, + tag: entity.tag || 0, + updateOrder: entity.updateOrder || 0 + }; + }); } /** @@ -200,7 +210,7 @@ export class EntityDataCollector { pendingRemove: stats.pendingRemove || 0, entitiesPerArchetype: archetypeData.distribution, topEntitiesByComponents: archetypeData.topEntities, - entityHierarchy: this.buildEntityHierarchyTree(entityList), + entityHierarchy: this.buildEntityHierarchyTree(entityList, scene), entityDetailsMap: this.buildEntityDetailsMap(entityList, scene) }; } @@ -534,7 +544,10 @@ export class EntityDataCollector { } } - private buildEntityHierarchyTree(entityList: { buffer?: Entity[] }): Array<{ + private buildEntityHierarchyTree( + entityList: { buffer?: Entity[] }, + scene?: IScene | null + ): Array<{ id: number; name: string; active: boolean; @@ -550,11 +563,14 @@ export class EntityDataCollector { }> { if (!entityList?.buffer) return []; + const hierarchySystem = scene?.getSystem(HierarchySystem); const rootEntities: any[] = []; entityList.buffer.forEach((entity: Entity) => { - if (!entity.parent) { - const hierarchyNode = this.buildEntityHierarchyNode(entity); + const hierarchy = entity.getComponent(HierarchyComponent); + const bHasNoParent = hierarchy?.parentId === null || hierarchy?.parentId === undefined; + if (bHasNoParent) { + const hierarchyNode = this.buildEntityHierarchyNode(entity, hierarchySystem); rootEntities.push(hierarchyNode); } }); @@ -572,25 +588,32 @@ export class EntityDataCollector { /** * 构建实体层次结构节点 */ - private buildEntityHierarchyNode(entity: Entity): any { + private buildEntityHierarchyNode(entity: Entity, hierarchySystem?: HierarchySystem | null): any { + const hierarchy = entity.getComponent(HierarchyComponent); + const bActiveInHierarchy = hierarchySystem?.isActiveInHierarchy(entity) ?? entity.active; + const depth = hierarchySystem?.getDepth(entity) ?? 0; + let node = { id: entity.id, name: entity.name || `Entity_${entity.id}`, active: entity.active !== false, enabled: entity.enabled !== false, - activeInHierarchy: entity.activeInHierarchy !== false, + activeInHierarchy: bActiveInHierarchy, componentCount: entity.components.length, componentTypes: entity.components.map((component: Component) => getComponentInstanceTypeName(component)), - parentId: entity.parent?.id || null, + parentId: hierarchy?.parentId ?? null, children: [] as any[], - depth: entity.getDepth ? entity.getDepth() : 0, + depth, tag: entity.tag || 0, updateOrder: entity.updateOrder || 0 }; // 递归构建子实体节点 - if (entity.children && entity.children.length > 0) { - node.children = entity.children.map((child: Entity) => this.buildEntityHierarchyNode(child)); + if (hierarchySystem) { + const children = hierarchySystem.getChildren(entity); + if (children.length > 0) { + node.children = children.map((child: Entity) => this.buildEntityHierarchyNode(child, hierarchySystem)); + } } // 优先使用Entity的getDebugInfo方法 @@ -616,6 +639,7 @@ export class EntityDataCollector { private buildEntityDetailsMap(entityList: { buffer?: Entity[] }, scene?: IScene | null): Record { if (!entityList?.buffer) return {}; + const hierarchySystem = scene?.getSystem(HierarchySystem); const entityDetailsMap: Record = {}; const entities = entityList.buffer; const batchSize = 100; @@ -626,7 +650,7 @@ export class EntityDataCollector { batch.forEach((entity: Entity) => { const baseDebugInfo = entity.getDebugInfo ? entity.getDebugInfo() - : this.buildFallbackEntityInfo(entity, scene); + : this.buildFallbackEntityInfo(entity, scene, hierarchySystem); const componentCacheStats = (entity as any).getComponentCacheStats ? (entity as any).getComponentCacheStats() @@ -634,9 +658,13 @@ export class EntityDataCollector { const componentDetails = this.extractComponentDetails(entity.components); + // 获取父实体名称 + const parent = hierarchySystem?.getParent(entity); + const parentName = parent?.name ?? null; + entityDetailsMap[entity.id] = { ...baseDebugInfo, - parentName: entity.parent?.name || null, + parentName, components: componentDetails, componentTypes: baseDebugInfo.componentTypes || componentDetails.map((comp) => comp.typeName), cachePerformance: componentCacheStats @@ -656,15 +684,22 @@ export class EntityDataCollector { /** * 构建实体基础信息 */ - private buildFallbackEntityInfo(entity: Entity, scene?: IScene | null): any { + private buildFallbackEntityInfo( + entity: Entity, + scene?: IScene | null, + hierarchySystem?: HierarchySystem | null + ): any { const sceneInfo = this.getSceneInfo(scene); + const hierarchy = entity.getComponent(HierarchyComponent); + const bActiveInHierarchy = hierarchySystem?.isActiveInHierarchy(entity) ?? entity.active; + const depth = hierarchySystem?.getDepth(entity) ?? 0; return { name: entity.name || `Entity_${entity.id}`, id: entity.id, enabled: entity.enabled !== false, active: entity.active !== false, - activeInHierarchy: entity.activeInHierarchy !== false, + activeInHierarchy: bActiveInHierarchy, destroyed: entity.isDestroyed || false, scene: sceneInfo.name, sceneName: sceneInfo.name, @@ -672,10 +707,10 @@ export class EntityDataCollector { componentCount: entity.components.length, componentTypes: entity.components.map((component: Component) => getComponentInstanceTypeName(component)), componentMask: entity.componentMask?.toString() || '0', - parentId: entity.parent?.id || null, - childCount: entity.children?.length || 0, - childIds: entity.children.map((child: Entity) => child.id) || [], - depth: entity.getDepth ? entity.getDepth() : 0, + parentId: hierarchy?.parentId ?? null, + childCount: hierarchy?.childIds?.length ?? 0, + childIds: hierarchy?.childIds ?? [], + depth, tag: entity.tag || 0, updateOrder: entity.updateOrder || 0 }; diff --git a/packages/core/src/Utils/Profiler/AutoProfiler.ts b/packages/core/src/Utils/Profiler/AutoProfiler.ts new file mode 100644 index 00000000..ee9cbcdc --- /dev/null +++ b/packages/core/src/Utils/Profiler/AutoProfiler.ts @@ -0,0 +1,649 @@ +/** + * 自动性能分析器 + * + * 提供自动函数包装和采样分析功能,无需手动埋点即可收集性能数据。 + * + * 支持三种分析模式: + * 1. 自动包装模式 - 使用 Proxy 自动包装类的所有方法 + * 2. 采样分析模式 - 定时采样调用栈(需要浏览器支持) + * 3. 装饰器模式 - 使用 @Profile() 装饰器手动标记方法 + */ + +import { ProfilerSDK } from './ProfilerSDK'; +import { ProfileCategory } from './ProfilerTypes'; + +/** + * 自动分析配置 + */ +export interface AutoProfilerConfig { + /** 是否启用自动包装 */ + enabled: boolean; + /** 采样间隔(毫秒),用于采样分析器 */ + sampleInterval: number; + /** 最小记录耗时(毫秒),低于此值的调用不记录 */ + minDuration: number; + /** 是否追踪异步方法 */ + trackAsync: boolean; + /** 排除的方法名模式 */ + excludePatterns: RegExp[]; + /** 最大采样缓冲区大小 */ + maxBufferSize: number; +} + +const DEFAULT_CONFIG: AutoProfilerConfig = { + enabled: true, + sampleInterval: 10, + minDuration: 0.1, // 0.1ms + trackAsync: true, + excludePatterns: [ + /^_/, // 私有方法 + /^get[A-Z]/, // getter 方法 + /^set[A-Z]/, // setter 方法 + /^is[A-Z]/, // 布尔检查方法 + /^has[A-Z]/, // 存在检查方法 + ], + maxBufferSize: 10000 +}; + +/** + * 采样数据 + */ +interface SampleData { + timestamp: number; + stack: string[]; + duration?: number; +} + +/** + * 包装信息 + */ +interface WrapInfo { + className: string; + methodName: string; + category: ProfileCategory; + original: Function; +} + +/** + * 自动性能分析器 + */ +export class AutoProfiler { + private static instance: AutoProfiler | null = null; + private config: AutoProfilerConfig; + private wrappedObjects: WeakMap> = new WeakMap(); + private samplingProfiler: SamplingProfiler | null = null; + private registeredClasses: Map = new Map(); + + private constructor(config?: Partial) { + this.config = { ...DEFAULT_CONFIG, ...config }; + } + + /** + * 获取单例实例 + */ + public static getInstance(config?: Partial): AutoProfiler { + if (!AutoProfiler.instance) { + AutoProfiler.instance = new AutoProfiler(config); + } + return AutoProfiler.instance; + } + + /** + * 重置实例 + */ + public static resetInstance(): void { + if (AutoProfiler.instance) { + AutoProfiler.instance.dispose(); + AutoProfiler.instance = null; + } + } + + /** + * 启用/禁用自动分析 + */ + public static setEnabled(enabled: boolean): void { + AutoProfiler.getInstance().setEnabled(enabled); + } + + /** + * 注册类以进行自动分析 + * 该类的所有实例方法都会被自动包装 + */ + public static registerClass any>( + constructor: T, + category: ProfileCategory = ProfileCategory.Custom, + className?: string + ): T { + return AutoProfiler.getInstance().registerClass(constructor, category, className); + } + + /** + * 包装对象实例的所有方法 + */ + public static wrapInstance( + instance: T, + className: string, + category: ProfileCategory = ProfileCategory.Custom + ): T { + return AutoProfiler.getInstance().wrapInstance(instance, className, category); + } + + /** + * 包装单个函数 + */ + public static wrapFunction any>( + fn: T, + name: string, + category: ProfileCategory = ProfileCategory.Custom + ): T { + return AutoProfiler.getInstance().wrapFunction(fn, name, category); + } + + /** + * 启动采样分析器 + */ + public static startSampling(): void { + AutoProfiler.getInstance().startSampling(); + } + + /** + * 停止采样分析器 + */ + public static stopSampling(): SampleData[] { + return AutoProfiler.getInstance().stopSampling(); + } + + /** + * 设置启用状态 + */ + public setEnabled(enabled: boolean): void { + this.config.enabled = enabled; + if (!enabled && this.samplingProfiler) { + this.samplingProfiler.stop(); + } + } + + /** + * 注册类以进行自动分析 + */ + public registerClass any>( + constructor: T, + category: ProfileCategory = ProfileCategory.Custom, + className?: string + ): T { + const name = className || constructor.name; + this.registeredClasses.set(name, { constructor, category }); + + // eslint-disable-next-line @typescript-eslint/no-this-alias -- Required for Proxy construct handler + const self = this; + + // 创建代理类 + const ProxiedClass = new Proxy(constructor, { + construct(target, args, newTarget) { + const instance = Reflect.construct(target, args, newTarget); + if (self.config.enabled) { + self.wrapInstance(instance, name, category); + } + return instance; + } + }); + + return ProxiedClass; + } + + /** + * 包装对象实例的所有方法 + */ + public wrapInstance( + instance: T, + className: string, + category: ProfileCategory = ProfileCategory.Custom + ): T { + if (!this.config.enabled) { + return instance; + } + + // 检查是否已经包装过 + if (this.wrappedObjects.has(instance)) { + return instance; + } + + const wrapInfoMap = new Map(); + this.wrappedObjects.set(instance, wrapInfoMap); + + // 获取所有方法(包括原型链上的) + const methodNames = this.getAllMethodNames(instance); + + for (const methodName of methodNames) { + if (this.shouldExcludeMethod(methodName)) { + continue; + } + + const descriptor = this.getPropertyDescriptor(instance, methodName); + if (!descriptor || typeof descriptor.value !== 'function') { + continue; + } + + const original = descriptor.value as Function; + const wrapped = this.createWrappedMethod(original, className, methodName, category); + + wrapInfoMap.set(methodName, { + className, + methodName, + category, + original + }); + + try { + (instance as any)[methodName] = wrapped; + } catch { + // 某些属性可能是只读的 + } + } + + return instance; + } + + /** + * 包装单个函数 + */ + public wrapFunction any>( + fn: T, + name: string, + category: ProfileCategory = ProfileCategory.Custom + ): T { + if (!this.config.enabled) return fn; + + // eslint-disable-next-line @typescript-eslint/no-this-alias -- Required for wrapped function closure + const self = this; + + const wrapped = function(this: any, ...args: any[]): any { + const handle = ProfilerSDK.beginSample(name, category); + try { + const result = fn.apply(this, args); + + // 处理 Promise + if (self.config.trackAsync && result instanceof Promise) { + return result.finally(() => { + ProfilerSDK.endSample(handle); + }); + } + + // 同步函数,立即结束采样 + ProfilerSDK.endSample(handle); + return result; + } catch (error) { + // 发生错误时也要结束采样 + ProfilerSDK.endSample(handle); + throw error; + } + } as T; + + // 保留原函数的属性 + Object.defineProperty(wrapped, 'name', { value: fn.name || name }); + Object.defineProperty(wrapped, 'length', { value: fn.length }); + + return wrapped; + } + + /** + * 启动采样分析器 + */ + public startSampling(): void { + if (!this.samplingProfiler) { + this.samplingProfiler = new SamplingProfiler(this.config); + } + this.samplingProfiler.start(); + } + + /** + * 停止采样分析器 + */ + public stopSampling(): SampleData[] { + if (!this.samplingProfiler) { + return []; + } + return this.samplingProfiler.stop(); + } + + /** + * 释放资源 + */ + public dispose(): void { + if (this.samplingProfiler) { + this.samplingProfiler.stop(); + this.samplingProfiler = null; + } + this.registeredClasses.clear(); + } + + /** + * 创建包装后的方法 + */ + private createWrappedMethod( + original: Function, + className: string, + methodName: string, + category: ProfileCategory + ): Function { + // eslint-disable-next-line @typescript-eslint/no-this-alias -- Required for wrapped method closure + const self = this; + const fullName = `${className}.${methodName}`; + const minDuration = this.config.minDuration; + + return function(this: any, ...args: any[]): any { + if (!self.config.enabled || !ProfilerSDK.isEnabled()) { + return original.apply(this, args); + } + + const startTime = performance.now(); + const handle = ProfilerSDK.beginSample(fullName, category); + + try { + const result = original.apply(this, args); + + // 处理异步方法 + if (self.config.trackAsync && result instanceof Promise) { + return result.then( + (value) => { + const duration = performance.now() - startTime; + if (duration >= minDuration) { + ProfilerSDK.endSample(handle); + } + return value; + }, + (error) => { + ProfilerSDK.endSample(handle); + throw error; + } + ); + } + + // 同步方法,检查最小耗时后结束采样 + const duration = performance.now() - startTime; + if (duration >= minDuration) { + ProfilerSDK.endSample(handle); + } + return result; + } catch (error) { + // 发生错误时也要结束采样 + ProfilerSDK.endSample(handle); + throw error; + } + }; + } + + /** + * 获取对象的所有方法名 + */ + private getAllMethodNames(obj: object): string[] { + const methods = new Set(); + let current = obj; + + while (current && current !== Object.prototype) { + for (const name of Object.getOwnPropertyNames(current)) { + if (name !== 'constructor') { + methods.add(name); + } + } + current = Object.getPrototypeOf(current); + } + + return Array.from(methods); + } + + /** + * 获取属性描述符 + */ + private getPropertyDescriptor(obj: object, name: string): PropertyDescriptor | undefined { + let current = obj; + while (current && current !== Object.prototype) { + const descriptor = Object.getOwnPropertyDescriptor(current, name); + if (descriptor) return descriptor; + current = Object.getPrototypeOf(current); + } + return undefined; + } + + /** + * 判断是否应该排除该方法 + */ + private shouldExcludeMethod(methodName: string): boolean { + // 排除构造函数和内置方法 + if (methodName === 'constructor' || methodName.startsWith('__')) { + return true; + } + + // 检查排除模式 + for (const pattern of this.config.excludePatterns) { + if (pattern.test(methodName)) { + return true; + } + } + + return false; + } +} + +/** + * 采样分析器 + * 使用定时器定期采样调用栈信息 + */ +class SamplingProfiler { + private config: AutoProfilerConfig; + private samples: SampleData[] = []; + private intervalId: number | null = null; + private isRunning = false; + + constructor(config: AutoProfilerConfig) { + this.config = config; + } + + /** + * 开始采样 + */ + public start(): void { + if (this.isRunning) return; + + this.isRunning = true; + this.samples = []; + + // 使用 requestAnimationFrame 或 setInterval 进行采样 + const sample = () => { + if (!this.isRunning) return; + + const stack = this.captureStack(); + if (stack.length > 0) { + this.samples.push({ + timestamp: performance.now(), + stack + }); + + // 限制缓冲区大小 + if (this.samples.length > this.config.maxBufferSize) { + this.samples.shift(); + } + } + + // 继续采样 + if (this.config.sampleInterval < 16) { + // 高频采样使用 setTimeout + this.intervalId = setTimeout(sample, this.config.sampleInterval) as any; + } else { + this.intervalId = setTimeout(sample, this.config.sampleInterval) as any; + } + }; + + sample(); + } + + /** + * 停止采样并返回数据 + */ + public stop(): SampleData[] { + this.isRunning = false; + if (this.intervalId !== null) { + clearTimeout(this.intervalId); + this.intervalId = null; + } + return [...this.samples]; + } + + /** + * 捕获当前调用栈 + */ + private captureStack(): string[] { + try { + // 创建 Error 对象获取调用栈 + const error = new Error(); + const stack = error.stack || ''; + + // 解析调用栈 + const lines = stack.split('\n').slice(3); // 跳过 Error 和 captureStack/sample + const frames: string[] = []; + + for (const line of lines) { + const frame = this.parseStackFrame(line); + if (frame && !this.isInternalFrame(frame)) { + frames.push(frame); + } + } + + return frames; + } catch { + return []; + } + } + + /** + * 解析调用栈帧 + */ + private parseStackFrame(line: string): string | null { + // Chrome/Edge 格式: " at functionName (file:line:col)" + // Firefox 格式: "functionName@file:line:col" + // Safari 格式: "functionName@file:line:col" + + line = line.trim(); + + // Chrome 格式 + let match = line.match(/at\s+(.+?)\s+\(/); + if (match && match[1]) { + return match[1]; + } + + // Chrome 匿名函数格式 + match = line.match(/at\s+(.+)/); + if (match && match[1]) { + const name = match[1]; + if (!name.includes('(')) { + return name; + } + } + + // Firefox/Safari 格式 + match = line.match(/^(.+?)@/); + if (match && match[1]) { + return match[1]; + } + + return null; + } + + /** + * 判断是否是内部帧(应该过滤掉) + */ + private isInternalFrame(frame: string): boolean { + const internalPatterns = [ + 'SamplingProfiler', + 'AutoProfiler', + 'ProfilerSDK', + 'setTimeout', + 'setInterval', + 'requestAnimationFrame', + '', + 'eval' + ]; + + return internalPatterns.some(pattern => frame.includes(pattern)); + } +} + +/** + * @Profile 装饰器 + * 用于标记需要性能分析的方法 + * + * @example + * ```typescript + * class MySystem extends System { + * @Profile() + * update() { + * // 方法执行时间会被自动记录 + * } + * + * @Profile('customName', ProfileCategory.Physics) + * calculatePhysics() { + * // 使用自定义名称和分类 + * } + * } + * ``` + */ +export function Profile( + name?: string, + category: ProfileCategory = ProfileCategory.Custom +): MethodDecorator { + return function( + target: object, + propertyKey: string | symbol, + descriptor: PropertyDescriptor + ): PropertyDescriptor { + const original = descriptor.value; + const methodName = name || `${target.constructor.name}.${String(propertyKey)}`; + + descriptor.value = function(this: any, ...args: any[]): any { + if (!ProfilerSDK.isEnabled()) { + return original.apply(this, args); + } + + const handle = ProfilerSDK.beginSample(methodName, category); + try { + const result = original.apply(this, args); + + // 处理异步方法 + if (result instanceof Promise) { + return result.finally(() => { + ProfilerSDK.endSample(handle); + }); + } + + // 同步方法,立即结束采样 + ProfilerSDK.endSample(handle); + return result; + } catch (error) { + // 发生错误时也要结束采样 + ProfilerSDK.endSample(handle); + throw error; + } + }; + + return descriptor; + }; +} + +/** + * @ProfileClass 装饰器 + * 用于自动包装类的所有方法 + * + * @example + * ```typescript + * @ProfileClass(ProfileCategory.Physics) + * class PhysicsSystem extends System { + * update() { ... } // 自动被包装 + * calculate() { ... } // 自动被包装 + * } + * ``` + */ +export function ProfileClass(category: ProfileCategory = ProfileCategory.Custom): ClassDecorator { + return function(constructor: T): T { + return AutoProfiler.registerClass(constructor as any, category) as any; + }; +} diff --git a/packages/core/src/Utils/Profiler/ProfilerSDK.ts b/packages/core/src/Utils/Profiler/ProfilerSDK.ts index fd14e226..e528ada3 100644 --- a/packages/core/src/Utils/Profiler/ProfilerSDK.ts +++ b/packages/core/src/Utils/Profiler/ProfilerSDK.ts @@ -228,6 +228,9 @@ export class ProfilerSDK { const endTime = performance.now(); const duration = endTime - handle.startTime; + // 获取父级 handle(在删除当前 handle 之前) + const parentHandle = handle.parentId ? this.activeSamples.get(handle.parentId) : undefined; + const sample: ProfileSample = { id: handle.id, name: handle.name, @@ -237,6 +240,7 @@ export class ProfilerSDK { duration, selfTime: duration, parentId: handle.parentId, + parentName: parentHandle?.name, depth: handle.depth, callCount: 1 }; @@ -245,7 +249,7 @@ export class ProfilerSDK { this.currentFrame.samples.push(sample); } - this.updateCallGraph(handle.name, handle.category, duration, handle.parentId); + this.updateCallGraph(handle.name, handle.category, duration, parentHandle); this.activeSamples.delete(handle.id); const stackIndex = this.sampleStack.indexOf(handle); @@ -437,6 +441,9 @@ export class ProfilerSDK { const categoryBreakdown = this.aggregateCategoryStats(frames); + // 根据帧历史重新计算 callGraph(不使用全局累积的数据) + const callGraph = this.buildCallGraphFromFrames(frames); + const firstFrame = frames[0]; const lastFrame = frames[frames.length - 1]; @@ -450,13 +457,106 @@ export class ProfilerSDK { p95FrameTime: sortedTimes[Math.floor(sortedTimes.length * 0.95)] || 0, p99FrameTime: sortedTimes[Math.floor(sortedTimes.length * 0.99)] || 0, hotspots, - callGraph: new Map(this.callGraph), + callGraph, categoryBreakdown, memoryTrend: frames.map((f) => f.memory), longTasks: [...this.longTasks] }; } + /** + * 从帧历史构建调用图 + * 注意:totalTime 存储的是平均耗时(总耗时/调用次数),而不是累计总耗时 + */ + private buildCallGraphFromFrames(frames: ProfileFrame[]): Map { + // 临时存储累计数据 + const tempData = new Map; + callees: Map; + }>(); + + for (const frame of frames) { + for (const sample of frame.samples) { + // 获取或创建当前函数的节点 + let node = tempData.get(sample.name); + if (!node) { + node = { + category: sample.category, + callCount: 0, + totalTime: 0, + callers: new Map(), + callees: new Map() + }; + tempData.set(sample.name, node); + } + + node.callCount++; + node.totalTime += sample.duration; + + // 如果有父级,建立调用关系 + if (sample.parentName) { + // 记录当前函数被谁调用 + const callerData = node.callers.get(sample.parentName) || { count: 0, totalTime: 0 }; + callerData.count++; + callerData.totalTime += sample.duration; + node.callers.set(sample.parentName, callerData); + + // 确保父节点存在并记录它调用了谁 + let parentNode = tempData.get(sample.parentName); + if (!parentNode) { + parentNode = { + category: sample.category, + callCount: 0, + totalTime: 0, + callers: new Map(), + callees: new Map() + }; + tempData.set(sample.parentName, parentNode); + } + + const calleeData = parentNode.callees.get(sample.name) || { count: 0, totalTime: 0 }; + calleeData.count++; + calleeData.totalTime += sample.duration; + parentNode.callees.set(sample.name, calleeData); + } + } + } + + // 转换为最终结果,将 totalTime 改为平均耗时 + const callGraph = new Map(); + for (const [name, data] of tempData) { + const avgCallers = new Map(); + for (const [callerName, callerData] of data.callers) { + avgCallers.set(callerName, { + count: callerData.count, + totalTime: callerData.count > 0 ? callerData.totalTime / callerData.count : 0 + }); + } + + const avgCallees = new Map(); + for (const [calleeName, calleeData] of data.callees) { + avgCallees.set(calleeName, { + count: calleeData.count, + totalTime: calleeData.count > 0 ? calleeData.totalTime / calleeData.count : 0 + }); + } + + callGraph.set(name, { + name, + category: data.category, + callCount: data.callCount, + totalTime: data.callCount > 0 ? data.totalTime / data.callCount : 0, + callers: avgCallers, + callees: avgCallees + }); + } + + return callGraph; + } + /** * 获取调用图数据 */ @@ -631,7 +731,7 @@ export class ProfilerSDK { name: string, category: ProfileCategory, duration: number, - parentId?: string + parentHandle?: SampleHandle ): void { let node = this.callGraph.get(name); if (!node) { @@ -649,22 +749,33 @@ export class ProfilerSDK { node.callCount++; node.totalTime += duration; - if (parentId) { - const parentHandle = this.activeSamples.get(parentId); - if (parentHandle) { - const callerData = node.callers.get(parentHandle.name) || { count: 0, totalTime: 0 }; - callerData.count++; - callerData.totalTime += duration; - node.callers.set(parentHandle.name, callerData); + // 如果有父级,建立调用关系 + if (parentHandle) { + // 记录当前函数被谁调用 + const callerData = node.callers.get(parentHandle.name) || { count: 0, totalTime: 0 }; + callerData.count++; + callerData.totalTime += duration; + node.callers.set(parentHandle.name, callerData); - const parentNode = this.callGraph.get(parentHandle.name); - if (parentNode) { - const calleeData = parentNode.callees.get(name) || { count: 0, totalTime: 0 }; - calleeData.count++; - calleeData.totalTime += duration; - parentNode.callees.set(name, calleeData); - } + // 确保父节点存在 + let parentNode = this.callGraph.get(parentHandle.name); + if (!parentNode) { + parentNode = { + name: parentHandle.name, + category: parentHandle.category, + callCount: 0, + totalTime: 0, + callers: new Map(), + callees: new Map() + }; + this.callGraph.set(parentHandle.name, parentNode); } + + // 记录父函数调用了谁 + const calleeData = parentNode.callees.get(name) || { count: 0, totalTime: 0 }; + calleeData.count++; + calleeData.totalTime += duration; + parentNode.callees.set(name, calleeData); } } diff --git a/packages/core/src/Utils/Profiler/ProfilerTypes.ts b/packages/core/src/Utils/Profiler/ProfilerTypes.ts index 1397de76..d2b26773 100644 --- a/packages/core/src/Utils/Profiler/ProfilerTypes.ts +++ b/packages/core/src/Utils/Profiler/ProfilerTypes.ts @@ -56,6 +56,8 @@ export interface ProfileSample { duration: number; selfTime: number; parentId?: string | undefined; + /** 父级采样的名称(用于构建调用图) */ + parentName?: string | undefined; depth: number; callCount: number; metadata?: Record; diff --git a/packages/core/src/Utils/Profiler/index.ts b/packages/core/src/Utils/Profiler/index.ts index db530c1d..003d568c 100644 --- a/packages/core/src/Utils/Profiler/index.ts +++ b/packages/core/src/Utils/Profiler/index.ts @@ -4,3 +4,5 @@ export * from './ProfilerTypes'; export { ProfilerSDK } from './ProfilerSDK'; +export { AutoProfiler, Profile, ProfileClass } from './AutoProfiler'; +export type { AutoProfilerConfig } from './AutoProfiler'; diff --git a/packages/core/tests/Core.test.ts b/packages/core/tests/Core.test.ts deleted file mode 100644 index 1234e7df..00000000 --- a/packages/core/tests/Core.test.ts +++ /dev/null @@ -1,465 +0,0 @@ -import { Core } from '../src/Core'; -import { Scene } from '../src/ECS/Scene'; -import { SceneManager } from '../src/ECS/SceneManager'; -import { Entity } from '../src/ECS/Entity'; -import { Component } from '../src/ECS/Component'; -import { ITimer } from '../src/Utils/Timers/ITimer'; -import { Updatable } from '../src/Core/DI'; -import type { IService } from '../src/Core/ServiceContainer'; -import type { IUpdatable } from '../src/Types/IUpdatable'; - -// 测试组件 -class TestComponent extends Component { - public value: number; - - constructor(...args: unknown[]) { - super(); - const [value = 0] = args as [number?]; - this.value = value; - } -} - -// 测试场景 -class TestScene extends Scene { - public initializeCalled = false; - public beginCalled = false; - public endCalled = false; - public updateCallCount = 0; - - public override initialize(): void { - this.initializeCalled = true; - } - - public override begin(): void { - this.beginCalled = true; - } - - public override end(): void { - this.endCalled = true; - } - - public override update(): void { - this.updateCallCount++; - } -} - -// 测试可更新服务 -@Updatable() -class TestUpdatableService implements IService, IUpdatable { - public updateCallCount = 0; - - public update(): void { - this.updateCallCount++; - } - - public dispose(): void { - // 清理资源 - } -} - -describe('Core - 核心管理系统测试', () => { - let originalConsoleWarn: typeof console.warn; - - beforeEach(() => { - // 清除之前的实例 - (Core as any)._instance = null; - - // 注意:WorldManager不再是单例,无需reset - - // 模拟console.warn以避免测试输出 - originalConsoleWarn = console.warn; - console.warn = jest.fn(); - }); - - afterEach(() => { - // 恢复console.warn - console.warn = originalConsoleWarn; - - // 清理Core实例 - if (Core.Instance) { - Core.destroy(); - } - }); - - describe('实例创建和管理', () => { - test('应该能够创建Core实例', () => { - const core = Core.create(true); - - expect(core).toBeDefined(); - expect(core).toBeInstanceOf(Core); - expect(core.debug).toBe(true); - expect(Core.entitySystemsEnabled).toBe(true); - }); - - test('应该能够通过配置对象创建Core实例', () => { - const config = { - debug: false, - enableEntitySystems: false - }; - - const core = Core.create(config); - - expect(core.debug).toBe(false); - expect(Core.entitySystemsEnabled).toBe(false); - }); - - test('重复调用create应该返回同一个实例', () => { - const core1 = Core.create(true); - const core2 = Core.create(false); // 不同参数 - - expect(core1).toBe(core2); - }); - - test('应该能够获取Core实例', () => { - const core = Core.create(true); - const instance = Core.Instance; - - expect(instance).toBe(core); - }); - - test('在未创建实例时调用update应该显示警告', () => { - Core.update(0.016); - - expect(console.warn).toHaveBeenCalledWith(expect.stringContaining("Core实例未创建,请先调用Core.create()")); - }); - }); - - // 注意:场景管理功能已移至SceneManager - // 相关测试请查看 SceneManager.test.ts - - describe('更新循环 - 可更新服务', () => { - let core: Core; - let updatableService: TestUpdatableService; - - beforeEach(() => { - core = Core.create(true); - updatableService = new TestUpdatableService(); - Core.services.registerInstance(TestUpdatableService, updatableService); - }); - - test('应该能够更新可更新服务', () => { - Core.update(0.016); - expect(updatableService.updateCallCount).toBe(1); - }); - - test('暂停状态下不应该执行更新', () => { - Core.paused = true; - Core.update(0.016); - expect(updatableService.updateCallCount).toBe(0); - - // 恢复状态 - Core.paused = false; - }); - - test('多次更新应该累积调用', () => { - Core.update(0.016); - Core.update(0.016); - Core.update(0.016); - expect(updatableService.updateCallCount).toBe(3); - }); - }); - - describe('服务容器集成', () => { - let core: Core; - let service1: TestUpdatableService; - - beforeEach(() => { - core = Core.create(true); - service1 = new TestUpdatableService(); - }); - - test('应该能够通过ServiceContainer注册可更新服务', () => { - Core.services.registerInstance(TestUpdatableService, service1); - - // 测试更新是否被调用 - Core.update(0.016); - expect(service1.updateCallCount).toBe(1); - }); - - test('应该能够注销服务', () => { - Core.services.registerInstance(TestUpdatableService, service1); - Core.services.unregister(TestUpdatableService); - - // 测试更新不应该被调用 - Core.update(0.016); - expect(service1.updateCallCount).toBe(0); - }); - - test('应该能够通过ServiceContainer解析服务', () => { - Core.services.registerInstance(TestUpdatableService, service1); - - const retrieved = Core.services.resolve(TestUpdatableService); - expect(retrieved).toBe(service1); - }); - - test('解析不存在的服务应该抛出错误', () => { - expect(() => { - Core.services.resolve(TestUpdatableService); - }).toThrow(); - }); - }); - - describe('定时器系统', () => { - let core: Core; - - beforeEach(() => { - core = Core.create(true); - }); - - test('应该能够调度定时器', () => { - let callbackExecuted = false; - let timerInstance: ITimer | null = null; - - const timer = Core.schedule(0.1, false, null, (timer) => { - callbackExecuted = true; - timerInstance = timer; - }); - - expect(timer).toBeDefined(); - - // 模拟时间推进 - for (let i = 0; i < 10; i++) { - Core.update(0.016); // 约160ms总计 - } - - expect(callbackExecuted).toBe(true); - expect(timerInstance).toBe(timer); - }); - - test('应该能够调度重复定时器', () => { - let callbackCount = 0; - - Core.schedule(0.05, true, null, () => { - callbackCount++; - }); - - // 模拟足够长的时间以触发多次回调 - for (let i = 0; i < 15; i++) { - Core.update(0.016); // 约240ms总计,应该触发4-5次 - } - - expect(callbackCount).toBeGreaterThan(1); - }); - - test('应该支持带上下文的定时器', () => { - const context = { value: 42 }; - let receivedContext: any = null; - - Core.schedule(0.05, false, context, function(this: any, timer) { - receivedContext = this; - }); - - // 模拟时间推进 - for (let i = 0; i < 5; i++) { - Core.update(0.016); - } - - expect(receivedContext).toBe(context); - }); - }); - - describe('调试功能', () => { - test('应该能够启用调试功能', () => { - const core = Core.create(true); - const debugConfig = { - enabled: true, - websocketUrl: 'ws://localhost:8080', - autoReconnect: true, - updateInterval: 1000, - channels: { - entities: true, - systems: true, - performance: true, - components: true, - scenes: true - } - }; - - Core.enableDebug(debugConfig); - - expect(Core.isDebugEnabled).toBe(true); - }); - - test('应该能够禁用调试功能', () => { - const core = Core.create(true); - const debugConfig = { - enabled: true, - websocketUrl: 'ws://localhost:8080', - autoReconnect: true, - updateInterval: 1000, - channels: { - entities: true, - systems: true, - performance: true, - components: true, - scenes: true - } - }; - - Core.enableDebug(debugConfig); - Core.disableDebug(); - - expect(Core.isDebugEnabled).toBe(false); - }); - - test('在未创建实例时启用调试应该显示警告', () => { - const debugConfig = { - enabled: true, - websocketUrl: 'ws://localhost:8080', - autoReconnect: true, - updateInterval: 1000, - channels: { - entities: true, - systems: true, - performance: true, - components: true, - scenes: true - } - }; - - Core.enableDebug(debugConfig); - - expect(console.warn).toHaveBeenCalledWith(expect.stringContaining("Core实例未创建,请先调用Core.create()")); - }); - }); - - // ECS API 现在由 SceneManager 管理 - // 相关测试请查看 SceneManager.test.ts - - describe('性能监控集成', () => { - let core: Core; - - beforeEach(() => { - core = Core.create(true); - }); - - test('调试模式下应该启用性能监控', () => { - const performanceMonitor = (core as any)._performanceMonitor; - - expect(performanceMonitor).toBeDefined(); - // 性能监控器应该在调试模式下被启用 - expect(performanceMonitor.isEnabled).toBe(true); - }); - - test('更新循环应该包含性能监控', () => { - const performanceMonitor = (core as any)._performanceMonitor; - const startMonitoringSpy = jest.spyOn(performanceMonitor, 'startMonitoring'); - const endMonitoringSpy = jest.spyOn(performanceMonitor, 'endMonitoring'); - - Core.update(0.016); - - expect(startMonitoringSpy).toHaveBeenCalled(); - expect(endMonitoringSpy).toHaveBeenCalled(); - }); - }); - - describe('Core.destroy() 生命周期', () => { - // 测试服务类 - class TestGameService implements IService { - public disposed = false; - public value = 'test-value'; - - dispose(): void { - this.disposed = true; - } - } - - test('destroy 后应该清理所有服务', () => { - // 创建 Core 并注册服务 - Core.create({ debug: true }); - const service = new TestGameService(); - Core.services.registerInstance(TestGameService, service); - - // 验证服务已注册 - expect(Core.services.isRegistered(TestGameService)).toBe(true); - - // 销毁 Core - Core.destroy(); - - // 验证服务的 dispose 被调用 - expect(service.disposed).toBe(true); - - // 验证 Core 实例已清空 - expect(Core.Instance).toBeNull(); - }); - - test('destroy 后重新 create 应该能够成功注册服务', () => { - // 第一次:创建 Core 并注册服务 - Core.create({ debug: true }); - Core.services.registerSingleton(TestGameService); - - // 验证服务已注册 - expect(Core.services.isRegistered(TestGameService)).toBe(true); - const firstService = Core.services.resolve(TestGameService); - expect(firstService).toBeDefined(); - expect(firstService.value).toBe('test-value'); - - // 销毁 Core - Core.destroy(); - - // 第二次:重新创建 Core - Core.create({ debug: true }); - - // 应该能够重新注册相同的服务(不应该报错或 warn) - expect(() => { - Core.services.registerSingleton(TestGameService); - }).not.toThrow(); - - // 验证服务重新注册成功 - expect(Core.services.isRegistered(TestGameService)).toBe(true); - const secondService = Core.services.resolve(TestGameService); - expect(secondService).toBeDefined(); - expect(secondService.value).toBe('test-value'); - - // 两次获取的应该是不同的实例 - expect(secondService).not.toBe(firstService); - - // 第一个实例应该已经被 dispose - expect(firstService.disposed).toBe(true); - }); - - test('destroy 后旧的服务引用不应该影响新的 Core 实例', () => { - // 第一次:创建 Core 并注册服务 - Core.create({ debug: true }); - const firstService = new TestGameService(); - Core.services.registerInstance(TestGameService, firstService); - - // 销毁 Core - Core.destroy(); - - // 验证旧服务被 dispose - expect(firstService.disposed).toBe(true); - - // 第二次:重新创建 Core 并注册新的服务实例 - Core.create({ debug: true }); - const secondService = new TestGameService(); - Core.services.registerInstance(TestGameService, secondService); - - // 验证新服务注册成功 - const resolved = Core.services.resolve(TestGameService); - expect(resolved).toBe(secondService); - expect(resolved).not.toBe(firstService); - - // 新服务应该未被 dispose - expect(secondService.disposed).toBe(false); - }); - - test('多次调用 destroy 应该安全', () => { - Core.create({ debug: true }); - const service = new TestGameService(); - Core.services.registerInstance(TestGameService, service); - - // 第一次 destroy - Core.destroy(); - expect(service.disposed).toBe(true); - expect(Core.Instance).toBeNull(); - - // 第二次 destroy(应该安全,不抛出错误) - expect(() => { - Core.destroy(); - }).not.toThrow(); - - expect(Core.Instance).toBeNull(); - }); - }); -}); \ No newline at end of file diff --git a/packages/core/tests/ECS/Core/FluentAPI.test.ts b/packages/core/tests/ECS/Core/FluentAPI.test.ts deleted file mode 100644 index 2edef8f0..00000000 --- a/packages/core/tests/ECS/Core/FluentAPI.test.ts +++ /dev/null @@ -1,691 +0,0 @@ -import { - EntityBuilder, - SceneBuilder, - ComponentBuilder, - ECSFluentAPI, - EntityBatchOperator, - createECSAPI, - initializeECS, - ECS -} from '../../../src/ECS/Core/FluentAPI'; -import { Scene } from '../../../src/ECS/Scene'; -import { Entity } from '../../../src/ECS/Entity'; -import { Component } from '../../../src/ECS/Component'; -import { QuerySystem } from '../../../src/ECS/Core/QuerySystem'; -import { TypeSafeEventSystem } from '../../../src/ECS/Core/EventSystem'; -import { EntitySystem } from '../../../src/ECS/Systems/EntitySystem'; -import { Matcher } from '../../../src/ECS/Utils/Matcher'; - -// 测试组件 -class TestComponent extends Component { - public value: number; - - constructor(...args: unknown[]) { - super(); - const [value = 0] = args as [number?]; - this.value = value; - } -} - -class PositionComponent extends Component { - public x: number; - public y: number; - - constructor(...args: unknown[]) { - super(); - const [x = 0, y = 0] = args as [number?, number?]; - this.x = x; - this.y = y; - } -} - -class VelocityComponent extends Component { - public vx: number; - public vy: number; - - constructor(...args: unknown[]) { - super(); - const [vx = 0, vy = 0] = args as [number?, number?]; - this.vx = vx; - this.vy = vy; - } -} - -// 测试系统 -class TestSystem extends EntitySystem { - constructor() { - super(Matcher.empty().all(TestComponent)); - } - - protected override process(entities: Entity[]): void { - // 测试系统 - } -} - -describe('FluentAPI - 流式API测试', () => { - let scene: Scene; - let querySystem: QuerySystem; - let eventSystem: TypeSafeEventSystem; - - beforeEach(() => { - scene = new Scene(); - querySystem = new QuerySystem(); - eventSystem = new TypeSafeEventSystem(); - }); - - describe('EntityBuilder - 实体构建器', () => { - let builder: EntityBuilder; - - beforeEach(() => { - builder = new EntityBuilder(scene, scene.componentStorageManager); - }); - - test('应该能够创建实体构建器', () => { - expect(builder).toBeInstanceOf(EntityBuilder); - }); - - test('应该能够设置实体名称', () => { - const entity = builder.named('TestEntity').build(); - expect(entity.name).toBe('TestEntity'); - }); - - test('应该能够设置实体标签', () => { - const entity = builder.tagged(42).build(); - expect(entity.tag).toBe(42); - }); - - test('应该能够添加组件', () => { - const component = new TestComponent(100); - const entity = builder.with(component).build(); - - expect(entity.hasComponent(TestComponent)).toBe(true); - expect(entity.getComponent(TestComponent)).toBe(component); - }); - - test('应该能够添加多个组件', () => { - const comp1 = new TestComponent(100); - const comp2 = new PositionComponent(10, 20); - const comp3 = new VelocityComponent(1, 2); - - const entity = builder.withComponents(comp1, comp2, comp3).build(); - - expect(entity.hasComponent(TestComponent)).toBe(true); - expect(entity.hasComponent(PositionComponent)).toBe(true); - expect(entity.hasComponent(VelocityComponent)).toBe(true); - }); - - test('应该能够条件性添加组件', () => { - const comp1 = new TestComponent(100); - const comp2 = new PositionComponent(10, 20); - - const entity = builder - .withIf(true, comp1) - .withIf(false, comp2) - .build(); - - expect(entity.hasComponent(TestComponent)).toBe(true); - expect(entity.hasComponent(PositionComponent)).toBe(false); - }); - - test('应该能够使用工厂函数创建组件', () => { - const entity = builder - .withFactory(() => new TestComponent(200)) - .build(); - - expect(entity.hasComponent(TestComponent)).toBe(true); - expect(entity.getComponent(TestComponent)!.value).toBe(200); - }); - - test('应该能够配置组件属性', () => { - const entity = builder - .with(new TestComponent(100)) - .configure(TestComponent, (comp) => { - comp.value = 300; - }) - .build(); - - expect(entity.getComponent(TestComponent)!.value).toBe(300); - }); - - test('配置不存在的组件应该安全处理', () => { - expect(() => { - builder.configure(TestComponent, (comp) => { - comp.value = 300; - }).build(); - }).not.toThrow(); - }); - - test('应该能够设置实体启用状态', () => { - const entity1 = builder.enabled(true).build(); - const entity2 = new EntityBuilder(scene, scene.componentStorageManager).enabled(false).build(); - - expect(entity1.enabled).toBe(true); - expect(entity2.enabled).toBe(false); - }); - - test('应该能够设置实体活跃状态', () => { - const entity1 = builder.active(true).build(); - const entity2 = new EntityBuilder(scene, scene.componentStorageManager).active(false).build(); - - expect(entity1.active).toBe(true); - expect(entity2.active).toBe(false); - }); - - test('应该能够添加子实体', () => { - const childBuilder = new EntityBuilder(scene, scene.componentStorageManager) - .named('Child') - .with(new TestComponent(50)); - - const parent = builder - .named('Parent') - .withChild(childBuilder) - .build(); - - expect(parent.children.length).toBe(1); - expect(parent.children[0].name).toBe('Child'); - }); - - test('应该能够添加多个子实体', () => { - const child1 = new EntityBuilder(scene, scene.componentStorageManager).named('Child1'); - const child2 = new EntityBuilder(scene, scene.componentStorageManager).named('Child2'); - const child3 = new EntityBuilder(scene, scene.componentStorageManager).named('Child3'); - - const parent = builder - .named('Parent') - .withChildren(child1, child2, child3) - .build(); - - expect(parent.children.length).toBe(3); - expect(parent.children[0].name).toBe('Child1'); - expect(parent.children[1].name).toBe('Child2'); - expect(parent.children[2].name).toBe('Child3'); - }); - - test('应该能够使用工厂函数创建子实体', () => { - const parent = builder - .named('Parent') - .withChildFactory((parentEntity) => { - return new EntityBuilder(scene, scene.componentStorageManager) - .named(`Child_of_${parentEntity.name}`) - .with(new TestComponent(100)); - }) - .build(); - - expect(parent.children.length).toBe(1); - expect(parent.children[0].name).toBe('Child_of_Parent'); - }); - - test('应该能够条件性添加子实体', () => { - const child1 = new EntityBuilder(scene, scene.componentStorageManager).named('Child1'); - const child2 = new EntityBuilder(scene, scene.componentStorageManager).named('Child2'); - - const parent = builder - .named('Parent') - .withChildIf(true, child1) - .withChildIf(false, child2) - .build(); - - expect(parent.children.length).toBe(1); - expect(parent.children[0].name).toBe('Child1'); - }); - - test('应该能够构建实体并添加到场景', () => { - const entity = builder - .named('SpawnedEntity') - .with(new TestComponent(100)) - .spawn(); - - expect(entity.name).toBe('SpawnedEntity'); - expect(entity.scene).toBe(scene); - }); - - test('应该能够克隆构建器', () => { - const originalBuilder = builder.named('Original').with(new TestComponent(100)); - const clonedBuilder = originalBuilder.clone(); - - expect(clonedBuilder).toBeInstanceOf(EntityBuilder); - expect(clonedBuilder).not.toBe(originalBuilder); - }); - - test('流式调用应该工作正常', () => { - const entity = builder - .named('ComplexEntity') - .tagged(42) - .with(new TestComponent(100)) - .with(new PositionComponent(10, 20)) - .enabled(true) - .active(true) - .configure(TestComponent, (comp) => { - comp.value = 200; - }) - .build(); - - expect(entity.name).toBe('ComplexEntity'); - expect(entity.tag).toBe(42); - expect(entity.enabled).toBe(true); - expect(entity.active).toBe(true); - expect(entity.hasComponent(TestComponent)).toBe(true); - expect(entity.hasComponent(PositionComponent)).toBe(true); - expect(entity.getComponent(TestComponent)!.value).toBe(200); - }); - }); - - describe('SceneBuilder - 场景构建器', () => { - let builder: SceneBuilder; - - beforeEach(() => { - builder = new SceneBuilder(); - }); - - test('应该能够创建场景构建器', () => { - expect(builder).toBeInstanceOf(SceneBuilder); - }); - - test('应该能够设置场景名称', () => { - const scene = builder.named('TestScene').build(); - expect(scene.name).toBe('TestScene'); - }); - - test('应该能够添加实体', () => { - const entity = new Entity('TestEntity', 1); - const scene = builder.withEntity(entity).build(); - - expect(scene.entities.count).toBe(1); - expect(scene.findEntity('TestEntity')).toBe(entity); - }); - - test('应该能够使用实体构建器添加实体', () => { - const scene = builder - .withEntityBuilder((builder) => { - return builder - .named('BuilderEntity') - .with(new TestComponent(100)); - }) - .build(); - - expect(scene.entities.count).toBe(1); - expect(scene.findEntity('BuilderEntity')).not.toBeNull(); - }); - - test('应该能够批量添加实体', () => { - const entity1 = new Entity('Entity1', 1); - const entity2 = new Entity('Entity2', 2); - const entity3 = new Entity('Entity3', 3); - - const scene = builder - .withEntities(entity1, entity2, entity3) - .build(); - - expect(scene.entities.count).toBe(3); - }); - - test('应该能够添加系统', () => { - const system = new TestSystem(); - const scene = builder.withSystem(system).build(); - - expect(scene.systems.length).toBe(1); - expect(scene.systems[0]).toBe(system); - }); - - test('应该能够批量添加系统', () => { - const system1 = new TestSystem(); - const system2 = new TestSystem(); - - const scene = builder - .withSystems(system1, system2) - .build(); - - expect(scene.systems.length).toBe(1); - }); - - test('流式调用应该工作正常', () => { - const entity = new Entity('TestEntity', 1); - const system = new TestSystem(); - - const scene = builder - .named('ComplexScene') - .withEntity(entity) - .withSystem(system) - .withEntityBuilder((builder) => { - return builder.named('BuilderEntity'); - }) - .build(); - - expect(scene.name).toBe('ComplexScene'); - expect(scene.entities.count).toBe(2); - expect(scene.systems.length).toBe(1); - }); - }); - - describe('ComponentBuilder - 组件构建器', () => { - test('应该能够创建组件构建器', () => { - const builder = new ComponentBuilder(TestComponent, 100); - expect(builder).toBeInstanceOf(ComponentBuilder); - }); - - test('应该能够设置组件属性', () => { - const component = new ComponentBuilder(TestComponent, 100) - .set('value', 200) - .build(); - - expect(component.value).toBe(200); - }); - - test('应该能够使用配置函数', () => { - const component = new ComponentBuilder(PositionComponent, 10, 20) - .configure((comp) => { - comp.x = 30; - comp.y = 40; - }) - .build(); - - expect(component.x).toBe(30); - expect(component.y).toBe(40); - }); - - test('应该能够条件性设置属性', () => { - const component = new ComponentBuilder(TestComponent, 100) - .setIf(true, 'value', 200) - .setIf(false, 'value', 300) - .build(); - - expect(component.value).toBe(200); - }); - - test('流式调用应该工作正常', () => { - const component = new ComponentBuilder(PositionComponent, 0, 0) - .set('x', 10) - .set('y', 20) - .setIf(true, 'x', 30) - .configure((comp) => { - comp.y = 40; - }) - .build(); - - expect(component.x).toBe(30); - expect(component.y).toBe(40); - }); - }); - - describe('ECSFluentAPI - 主API', () => { - let api: ECSFluentAPI; - - beforeEach(() => { - api = new ECSFluentAPI(scene, querySystem, eventSystem); - }); - - test('应该能够创建ECS API', () => { - expect(api).toBeInstanceOf(ECSFluentAPI); - }); - - test('应该能够创建实体构建器', () => { - const builder = api.createEntity(); - expect(builder).toBeInstanceOf(EntityBuilder); - }); - - test('应该能够创建场景构建器', () => { - const builder = api.createScene(); - expect(builder).toBeInstanceOf(SceneBuilder); - }); - - test('应该能够创建组件构建器', () => { - const builder = api.createComponent(TestComponent, 100); - expect(builder).toBeInstanceOf(ComponentBuilder); - }); - - test('应该能够创建查询构建器', () => { - const builder = api.query(); - expect(builder).toBeDefined(); - }); - - test('应该能够查找实体', () => { - const entity = scene.createEntity('TestEntity'); - entity.addComponent(new TestComponent(100)); - querySystem.setEntities([entity]); - - const results = api.find(TestComponent); - expect(results.length).toBe(1); - expect(results[0]).toBe(entity); - }); - - test('应该能够查找第一个匹配的实体', () => { - const entity1 = scene.createEntity('Entity1'); - const entity2 = scene.createEntity('Entity2'); - entity1.addComponent(new TestComponent(100)); - entity2.addComponent(new TestComponent(200)); - querySystem.setEntities([entity1, entity2]); - - const result = api.findFirst(TestComponent); - expect(result).not.toBeNull(); - expect([entity1, entity2]).toContain(result); - }); - - test('查找不存在的实体应该返回null', () => { - const result = api.findFirst(TestComponent); - expect(result).toBeNull(); - }); - - test('应该能够按名称查找实体', () => { - const entity = scene.createEntity('TestEntity'); - - const result = api.findByName('TestEntity'); - expect(result).toBe(entity); - }); - - test('应该能够按标签查找实体', () => { - const entity1 = scene.createEntity('Entity1'); - const entity2 = scene.createEntity('Entity2'); - entity1.tag = 42; - entity2.tag = 42; - - const results = api.findByTag(42); - expect(results.length).toBe(2); - expect(results).toContain(entity1); - expect(results).toContain(entity2); - }); - - test('应该能够触发同步事件', () => { - let eventReceived = false; - let eventData: any = null; - - api.on('test:event', (data) => { - eventReceived = true; - eventData = data; - }); - - api.emit('test:event', { message: 'hello' }); - - expect(eventReceived).toBe(true); - expect(eventData.message).toBe('hello'); - }); - - test('应该能够触发异步事件', async () => { - let eventReceived = false; - let eventData: any = null; - - api.on('test:event', (data) => { - eventReceived = true; - eventData = data; - }); - - await api.emitAsync('test:event', { message: 'hello' }); - - expect(eventReceived).toBe(true); - expect(eventData.message).toBe('hello'); - }); - - test('应该能够一次性监听事件', () => { - let callCount = 0; - - api.once('test:event', () => { - callCount++; - }); - - api.emit('test:event', {}); - api.emit('test:event', {}); - - expect(callCount).toBe(1); - }); - - test('应该能够移除事件监听器', () => { - let callCount = 0; - - const listenerId = api.on('test:event', () => { - callCount++; - }); - - api.emit('test:event', {}); - api.off('test:event', listenerId); - api.emit('test:event', {}); - - expect(callCount).toBe(1); - }); - - test('应该能够创建批量操作器', () => { - const entity1 = new Entity('Entity1', 1); - const entity2 = new Entity('Entity2', 2); - - const batch = api.batch([entity1, entity2]); - expect(batch).toBeInstanceOf(EntityBatchOperator); - }); - - test('应该能够获取统计信息', () => { - const stats = api.getStats(); - - expect(stats).toBeDefined(); - expect(stats.entityCount).toBeDefined(); - expect(stats.systemCount).toBeDefined(); - expect(stats.componentStats).toBeDefined(); - expect(stats.queryStats).toBeDefined(); - expect(stats.eventStats).toBeDefined(); - }); - }); - - describe('EntityBatchOperator - 批量操作器', () => { - let entity1: Entity; - let entity2: Entity; - let entity3: Entity; - let batchOp: EntityBatchOperator; - - beforeEach(() => { - entity1 = scene.createEntity('Entity1'); - entity2 = scene.createEntity('Entity2'); - entity3 = scene.createEntity('Entity3'); - batchOp = new EntityBatchOperator([entity1, entity2, entity3]); - }); - - test('应该能够创建批量操作器', () => { - expect(batchOp).toBeInstanceOf(EntityBatchOperator); - }); - - test('应该能够批量添加组件', () => { - const component = new TestComponent(100); - batchOp.addComponent(component); - - expect(entity1.hasComponent(TestComponent)).toBe(true); - expect(entity2.hasComponent(TestComponent)).toBe(true); - expect(entity3.hasComponent(TestComponent)).toBe(true); - }); - - test('应该能够批量移除组件', () => { - entity1.addComponent(new TestComponent(100)); - entity2.addComponent(new TestComponent(200)); - entity3.addComponent(new TestComponent(300)); - - batchOp.removeComponent(TestComponent); - - expect(entity1.hasComponent(TestComponent)).toBe(false); - expect(entity2.hasComponent(TestComponent)).toBe(false); - expect(entity3.hasComponent(TestComponent)).toBe(false); - }); - - test('应该能够批量设置活跃状态', () => { - batchOp.setActive(false); - - expect(entity1.active).toBe(false); - expect(entity2.active).toBe(false); - expect(entity3.active).toBe(false); - }); - - test('应该能够批量设置标签', () => { - batchOp.setTag(42); - - expect(entity1.tag).toBe(42); - expect(entity2.tag).toBe(42); - expect(entity3.tag).toBe(42); - }); - - test('应该能够批量执行操作', () => { - const names: string[] = []; - const indices: number[] = []; - - batchOp.forEach((entity, index) => { - names.push(entity.name); - indices.push(index); - }); - - expect(names).toEqual(['Entity1', 'Entity2', 'Entity3']); - expect(indices).toEqual([0, 1, 2]); - }); - - test('应该能够过滤实体', () => { - entity1.tag = 1; - entity2.tag = 2; - entity3.tag = 1; - - const filtered = batchOp.filter(entity => entity.tag === 1); - - expect(filtered.count()).toBe(2); - expect(filtered.toArray()).toContain(entity1); - expect(filtered.toArray()).toContain(entity3); - }); - - test('应该能够获取实体数组', () => { - const entities = batchOp.toArray(); - - expect(entities.length).toBe(3); - expect(entities).toContain(entity1); - expect(entities).toContain(entity2); - expect(entities).toContain(entity3); - }); - - test('应该能够获取实体数量', () => { - expect(batchOp.count()).toBe(3); - }); - - test('流式调用应该工作正常', () => { - const result = batchOp - .addComponent(new TestComponent(100)) - .setActive(false) - .setTag(42) - .forEach((entity) => { - entity.name = entity.name + '_Modified'; - }); - - expect(result).toBe(batchOp); - expect(entity1.hasComponent(TestComponent)).toBe(true); - expect(entity1.active).toBe(false); - expect(entity1.tag).toBe(42); - expect(entity1.name).toBe('Entity1_Modified'); - }); - }); - - describe('工厂函数和全局API', () => { - test('createECSAPI应该创建API实例', () => { - const api = createECSAPI(scene, querySystem, eventSystem); - expect(api).toBeInstanceOf(ECSFluentAPI); - }); - - test('initializeECS应该初始化全局ECS', () => { - initializeECS(scene, querySystem, eventSystem); - expect(ECS).toBeInstanceOf(ECSFluentAPI); - }); - - test('全局ECS应该可用', () => { - initializeECS(scene, querySystem, eventSystem); - - const builder = ECS.createEntity(); - expect(builder).toBeInstanceOf(EntityBuilder); - }); - }); -}); \ No newline at end of file diff --git a/packages/core/tests/ECS/Core/FluentAPI/EntityBuilder.test.ts b/packages/core/tests/ECS/Core/FluentAPI/EntityBuilder.test.ts new file mode 100644 index 00000000..4a3b9ba7 --- /dev/null +++ b/packages/core/tests/ECS/Core/FluentAPI/EntityBuilder.test.ts @@ -0,0 +1,321 @@ +import { EntityBuilder } from '../../../../src/ECS/Core/FluentAPI/EntityBuilder'; +import { Scene } from '../../../../src/ECS/Scene'; +import { Component } from '../../../../src/ECS/Component'; +import { HierarchySystem } from '../../../../src/ECS/Systems/HierarchySystem'; +import { ECSComponent } from '../../../../src/ECS/Decorators'; + +@ECSComponent('BuilderTestPosition') +class PositionComponent extends Component { + public x: number = 0; + public y: number = 0; + + constructor(x: number = 0, y: number = 0) { + super(); + this.x = x; + this.y = y; + } +} + +@ECSComponent('BuilderTestVelocity') +class VelocityComponent extends Component { + public vx: number = 0; + public vy: number = 0; +} + +@ECSComponent('BuilderTestHealth') +class HealthComponent extends Component { + public current: number = 100; + public max: number = 100; +} + +// Helper function to create EntityBuilder +function createBuilder(scene: Scene): EntityBuilder { + return new EntityBuilder(scene, scene.componentStorageManager); +} + +describe('EntityBuilder', () => { + let scene: Scene; + let hierarchySystem: HierarchySystem; + + beforeEach(() => { + scene = new Scene({ name: 'BuilderTestScene' }); + hierarchySystem = new HierarchySystem(); + scene.addSystem(hierarchySystem); + }); + + afterEach(() => { + scene.end(); + }); + + describe('basic building', () => { + test('should create entity with name', () => { + const builder = createBuilder(scene); + const entity = builder.named('TestEntity').build(); + + expect(entity.name).toBe('TestEntity'); + }); + + test('should create entity with tag', () => { + const builder = createBuilder(scene); + const entity = builder.tagged(0x100).build(); + + expect(entity.tag).toBe(0x100); + }); + + test('should support chaining name and tag', () => { + const entity = createBuilder(scene) + .named('ChainedEntity') + .tagged(0x200) + .build(); + + expect(entity.name).toBe('ChainedEntity'); + expect(entity.tag).toBe(0x200); + }); + }); + + describe('component management', () => { + test('should add single component with .with()', () => { + const entity = createBuilder(scene) + .with(new PositionComponent(10, 20)) + .build(); + + const pos = entity.getComponent(PositionComponent); + expect(pos).not.toBeNull(); + expect(pos!.x).toBe(10); + expect(pos!.y).toBe(20); + }); + + test('should add multiple components with .withComponents()', () => { + const entity = createBuilder(scene) + .withComponents( + new PositionComponent(5, 10), + new VelocityComponent(), + new HealthComponent() + ) + .build(); + + expect(entity.hasComponent(PositionComponent)).toBe(true); + expect(entity.hasComponent(VelocityComponent)).toBe(true); + expect(entity.hasComponent(HealthComponent)).toBe(true); + }); + + test('should conditionally add component with .withIf()', () => { + const shouldAdd = true; + const shouldNotAdd = false; + + const entity = createBuilder(scene) + .withIf(shouldAdd, new PositionComponent()) + .withIf(shouldNotAdd, new VelocityComponent()) + .build(); + + expect(entity.hasComponent(PositionComponent)).toBe(true); + expect(entity.hasComponent(VelocityComponent)).toBe(false); + }); + + test('should add component using factory with .withFactory()', () => { + const entity = createBuilder(scene) + .withFactory(() => new PositionComponent(100, 200)) + .build(); + + const pos = entity.getComponent(PositionComponent); + expect(pos).not.toBeNull(); + expect(pos!.x).toBe(100); + expect(pos!.y).toBe(200); + }); + + test('should configure existing component with .configure()', () => { + const entity = createBuilder(scene) + .with(new PositionComponent(0, 0)) + .configure(PositionComponent, (pos: PositionComponent) => { + pos.x = 999; + pos.y = 888; + }) + .build(); + + const pos = entity.getComponent(PositionComponent); + expect(pos!.x).toBe(999); + expect(pos!.y).toBe(888); + }); + + test('.configure() should do nothing if component does not exist', () => { + const entity = createBuilder(scene) + .configure(PositionComponent, (pos: PositionComponent) => { + pos.x = 100; + }) + .build(); + + expect(entity.hasComponent(PositionComponent)).toBe(false); + }); + }); + + describe('entity state', () => { + test('should set enabled state', () => { + const disabledEntity = createBuilder(scene) + .enabled(false) + .build(); + + const enabledEntity = createBuilder(scene) + .enabled(true) + .build(); + + expect(disabledEntity.enabled).toBe(false); + expect(enabledEntity.enabled).toBe(true); + }); + + test('should set active state', () => { + const inactiveEntity = createBuilder(scene) + .active(false) + .build(); + + const activeEntity = createBuilder(scene) + .active(true) + .build(); + + expect(inactiveEntity.active).toBe(false); + expect(activeEntity.active).toBe(true); + }); + }); + + describe('hierarchy building', () => { + test('should call withChild method', () => { + const childBuilder = createBuilder(scene).named('Child'); + const builder = createBuilder(scene) + .named('Parent') + .withChild(childBuilder); + + // withChild returns the builder for chaining + expect(builder).toBeInstanceOf(EntityBuilder); + }); + + test('should call withChildren method', () => { + const child1Builder = createBuilder(scene).named('Child1'); + const child2Builder = createBuilder(scene).named('Child2'); + + const builder = createBuilder(scene) + .named('Parent') + .withChildren(child1Builder, child2Builder); + + // withChildren returns the builder for chaining + expect(builder).toBeInstanceOf(EntityBuilder); + }); + + test('should call withChildFactory method', () => { + const builder = createBuilder(scene) + .named('Parent') + .with(new PositionComponent(100, 100)) + .withChildFactory((parentEntity) => { + return createBuilder(scene) + .named('ChildFromFactory') + .with(new PositionComponent(10, 20)); + }); + + // withChildFactory returns the builder for chaining + expect(builder).toBeInstanceOf(EntityBuilder); + }); + + test('should call withChildIf method', () => { + const shouldAdd = true; + const shouldNotAdd = false; + + const child1Builder = createBuilder(scene).named('Child1'); + const builder1 = createBuilder(scene) + .named('Parent') + .withChildIf(shouldAdd, child1Builder); + + expect(builder1).toBeInstanceOf(EntityBuilder); + + const child2Builder = createBuilder(scene).named('Child2'); + const builder2 = createBuilder(scene) + .named('Parent2') + .withChildIf(shouldNotAdd, child2Builder); + + expect(builder2).toBeInstanceOf(EntityBuilder); + }); + }); + + describe('spawning and cloning', () => { + test('should spawn entity to scene with .spawn()', () => { + const initialCount = scene.entities.count; + + const entity = createBuilder(scene) + .named('SpawnedEntity') + .with(new PositionComponent()) + .spawn(); + + expect(scene.entities.count).toBe(initialCount + 1); + expect(scene.findEntityById(entity.id)).toBe(entity); + }); + + test('.build() should not add to scene automatically', () => { + const initialCount = scene.entities.count; + + createBuilder(scene) + .named('BuiltEntity') + .build(); + + expect(scene.entities.count).toBe(initialCount); + }); + + test('should clone builder', () => { + const builder = createBuilder(scene) + .named('OriginalEntity') + .tagged(0x50); + + const clonedBuilder = builder.clone(); + + expect(clonedBuilder).not.toBe(builder); + expect(clonedBuilder).toBeInstanceOf(EntityBuilder); + }); + }); + + describe('complex building scenarios', () => { + test('should build complete entity with all options', () => { + const entity = createBuilder(scene) + .named('CompleteEntity') + .tagged(0x100) + .with(new PositionComponent(50, 75)) + .with(new VelocityComponent()) + .withFactory(() => new HealthComponent()) + .configure(HealthComponent, (h: HealthComponent) => { + h.current = 80; + h.max = 100; + }) + .enabled(true) + .active(true) + .build(); + + expect(entity.name).toBe('CompleteEntity'); + expect(entity.tag).toBe(0x100); + expect(entity.enabled).toBe(true); + expect(entity.active).toBe(true); + + expect(entity.hasComponent(PositionComponent)).toBe(true); + expect(entity.hasComponent(VelocityComponent)).toBe(true); + expect(entity.hasComponent(HealthComponent)).toBe(true); + + const health = entity.getComponent(HealthComponent); + expect(health!.current).toBe(80); + expect(health!.max).toBe(100); + }); + + test('should support complex chaining', () => { + const builder = createBuilder(scene) + .named('Root') + .with(new PositionComponent(1, 1)); + + // Add child builder chain + const childBuilder = createBuilder(scene) + .named('Child') + .with(new PositionComponent(2, 2)); + + // Chain withChild + builder.withChild(childBuilder); + + // Build and spawn + const root = builder.spawn(); + + expect(root.name).toBe('Root'); + expect(root.hasComponent(PositionComponent)).toBe(true); + }); + }); +}); diff --git a/packages/core/tests/ECS/EntityRefIntegration.test.ts b/packages/core/tests/ECS/EntityRefIntegration.test.ts deleted file mode 100644 index 3d14eac1..00000000 --- a/packages/core/tests/ECS/EntityRefIntegration.test.ts +++ /dev/null @@ -1,274 +0,0 @@ -import { describe, test, expect, beforeEach } from '@jest/globals'; -import { Scene } from '../../src/ECS/Scene'; -import { Component } from '../../src/ECS/Component'; -import { Entity } from '../../src/ECS/Entity'; -import { EntityRef, ECSComponent } from '../../src/ECS/Decorators'; - -@ECSComponent('ParentRef') -class ParentComponent extends Component { - @EntityRef() parent: Entity | null = null; -} - -@ECSComponent('TargetRef') -class TargetComponent extends Component { - @EntityRef() target: Entity | null = null; - @EntityRef() ally: Entity | null = null; -} - -describe('EntityRef Integration Tests', () => { - let scene: Scene; - - beforeEach(() => { - scene = new Scene({ name: 'TestScene' }); - }); - - describe('基础功能', () => { - test('应该支持EntityRef装饰器', () => { - const entity1 = scene.createEntity('Entity1'); - const entity2 = scene.createEntity('Entity2'); - const comp = entity1.addComponent(new ParentComponent()); - - comp.parent = entity2; - - expect(comp.parent).toBe(entity2); - }); - - test('Entity销毁时应该自动清理所有引用', () => { - const parent = scene.createEntity('Parent'); - const child1 = scene.createEntity('Child1'); - const child2 = scene.createEntity('Child2'); - - const comp1 = child1.addComponent(new ParentComponent()); - const comp2 = child2.addComponent(new ParentComponent()); - - comp1.parent = parent; - comp2.parent = parent; - - expect(comp1.parent).toBe(parent); - expect(comp2.parent).toBe(parent); - - parent.destroy(); - - expect(comp1.parent).toBeNull(); - expect(comp2.parent).toBeNull(); - }); - - test('修改引用应该更新ReferenceTracker', () => { - const entity1 = scene.createEntity('Entity1'); - const entity2 = scene.createEntity('Entity2'); - const entity3 = scene.createEntity('Entity3'); - const comp = entity1.addComponent(new ParentComponent()); - - comp.parent = entity2; - expect(scene.referenceTracker.getReferencesTo(entity2.id)).toHaveLength(1); - - comp.parent = entity3; - expect(scene.referenceTracker.getReferencesTo(entity2.id)).toHaveLength(0); - expect(scene.referenceTracker.getReferencesTo(entity3.id)).toHaveLength(1); - }); - - test('设置为null应该注销引用', () => { - const entity1 = scene.createEntity('Entity1'); - const entity2 = scene.createEntity('Entity2'); - const comp = entity1.addComponent(new ParentComponent()); - - comp.parent = entity2; - expect(scene.referenceTracker.getReferencesTo(entity2.id)).toHaveLength(1); - - comp.parent = null; - expect(scene.referenceTracker.getReferencesTo(entity2.id)).toHaveLength(0); - }); - }); - - describe('Component生命周期', () => { - test('移除Component应该清理其所有引用注册', () => { - const entity1 = scene.createEntity('Entity1'); - const entity2 = scene.createEntity('Entity2'); - const comp = entity1.addComponent(new ParentComponent()); - - comp.parent = entity2; - expect(scene.referenceTracker.getReferencesTo(entity2.id)).toHaveLength(1); - - entity1.removeComponent(comp); - expect(scene.referenceTracker.getReferencesTo(entity2.id)).toHaveLength(0); - }); - - test('移除Component应该清除entityId引用', () => { - const entity1 = scene.createEntity('Entity1'); - const comp = entity1.addComponent(new ParentComponent()); - - expect(comp.entityId).toBe(entity1.id); - - entity1.removeComponent(comp); - expect(comp.entityId).toBeNull(); - }); - }); - - describe('多属性引用', () => { - test('应该支持同一Component的多个EntityRef属性', () => { - const entity1 = scene.createEntity('Entity1'); - const entity2 = scene.createEntity('Entity2'); - const entity3 = scene.createEntity('Entity3'); - const comp = entity1.addComponent(new TargetComponent()); - - comp.target = entity2; - comp.ally = entity3; - - expect(comp.target).toBe(entity2); - expect(comp.ally).toBe(entity3); - - entity2.destroy(); - - expect(comp.target).toBeNull(); - expect(comp.ally).toBe(entity3); - - entity3.destroy(); - - expect(comp.ally).toBeNull(); - }); - }); - - describe('边界情况', () => { - test('跨Scene引用应该失败', () => { - const scene2 = new Scene({ name: 'TestScene2' }); - const entity1 = scene.createEntity('Entity1'); - const entity2 = scene2.createEntity('Entity2'); - const comp = entity1.addComponent(new ParentComponent()); - - comp.parent = entity2; - - expect(comp.parent).toBeNull(); - }); - - test('引用已销毁的Entity应该失败', () => { - const entity1 = scene.createEntity('Entity1'); - const entity2 = scene.createEntity('Entity2'); - const comp = entity1.addComponent(new ParentComponent()); - - entity2.destroy(); - comp.parent = entity2; - - expect(comp.parent).toBeNull(); - }); - - test('重复设置相同值不应重复注册', () => { - const entity1 = scene.createEntity('Entity1'); - const entity2 = scene.createEntity('Entity2'); - const comp = entity1.addComponent(new ParentComponent()); - - comp.parent = entity2; - comp.parent = entity2; - comp.parent = entity2; - - expect(scene.referenceTracker.getReferencesTo(entity2.id)).toHaveLength(1); - }); - - test('循环引用应该正常工作', () => { - const entity1 = scene.createEntity('Entity1'); - const entity2 = scene.createEntity('Entity2'); - const comp1 = entity1.addComponent(new ParentComponent()); - const comp2 = entity2.addComponent(new ParentComponent()); - - comp1.parent = entity2; - comp2.parent = entity1; - - expect(comp1.parent).toBe(entity2); - expect(comp2.parent).toBe(entity1); - - entity1.destroy(); - - expect(comp2.parent).toBeNull(); - expect(entity2.isDestroyed).toBe(false); - }); - }); - - describe('复杂场景', () => { - test('父子实体销毁应该正确清理引用', () => { - const parent = scene.createEntity('Parent'); - const child1 = scene.createEntity('Child1'); - const child2 = scene.createEntity('Child2'); - const observer = scene.createEntity('Observer'); - - parent.addChild(child1); - parent.addChild(child2); - - const observerComp = observer.addComponent(new TargetComponent()); - observerComp.target = parent; - observerComp.ally = child1; - - expect(observerComp.target).toBe(parent); - expect(observerComp.ally).toBe(child1); - - parent.destroy(); - - expect(observerComp.target).toBeNull(); - expect(observerComp.ally).toBeNull(); - expect(child1.isDestroyed).toBe(true); - expect(child2.isDestroyed).toBe(true); - }); - - test('大量引用场景', () => { - const target = scene.createEntity('Target'); - const entities: Entity[] = []; - const components: ParentComponent[] = []; - - for (let i = 0; i < 100; i++) { - const entity = scene.createEntity(`Entity${i}`); - const comp = entity.addComponent(new ParentComponent()); - comp.parent = target; - entities.push(entity); - components.push(comp); - } - - expect(scene.referenceTracker.getReferencesTo(target.id)).toHaveLength(100); - - target.destroy(); - - for (const comp of components) { - expect(comp.parent).toBeNull(); - } - - expect(scene.referenceTracker.getReferencesTo(target.id)).toHaveLength(0); - }); - - test('批量销毁后引用应全部清理', () => { - const entities: Entity[] = []; - const components: TargetComponent[] = []; - - for (let i = 0; i < 50; i++) { - entities.push(scene.createEntity(`Entity${i}`)); - } - - for (let i = 0; i < 50; i++) { - const comp = entities[i].addComponent(new TargetComponent()); - if (i > 0) { - comp.target = entities[i - 1]; - } - if (i < 49) { - comp.ally = entities[i + 1]; - } - components.push(comp); - } - - scene.destroyAllEntities(); - - for (const comp of components) { - expect(comp.target).toBeNull(); - expect(comp.ally).toBeNull(); - } - }); - }); - - describe('调试功能', () => { - test('getDebugInfo应该返回引用信息', () => { - const entity1 = scene.createEntity('Entity1'); - const entity2 = scene.createEntity('Entity2'); - const comp = entity1.addComponent(new ParentComponent()); - - comp.parent = entity2; - - const debugInfo = scene.referenceTracker.getDebugInfo(); - expect(debugInfo).toHaveProperty(`entity_${entity2.id}`); - }); - }); -}); diff --git a/packages/core/tests/ECS/EntityTags.test.ts b/packages/core/tests/ECS/EntityTags.test.ts new file mode 100644 index 00000000..7d34deb3 --- /dev/null +++ b/packages/core/tests/ECS/EntityTags.test.ts @@ -0,0 +1,237 @@ +import { + EntityTags, + hasEntityTag, + addEntityTag, + removeEntityTag, + isFolder, + isHidden, + isLocked +} from '../../src/ECS/EntityTags'; + +describe('EntityTags', () => { + describe('tag constants', () => { + test('should have correct NONE value', () => { + expect(EntityTags.NONE).toBe(0x0000); + }); + + test('should have correct FOLDER value', () => { + expect(EntityTags.FOLDER).toBe(0x1000); + }); + + test('should have correct HIDDEN value', () => { + expect(EntityTags.HIDDEN).toBe(0x2000); + }); + + test('should have correct LOCKED value', () => { + expect(EntityTags.LOCKED).toBe(0x4000); + }); + + test('should have correct EDITOR_ONLY value', () => { + expect(EntityTags.EDITOR_ONLY).toBe(0x8000); + }); + + test('should have correct PREFAB_INSTANCE value', () => { + expect(EntityTags.PREFAB_INSTANCE).toBe(0x0100); + }); + + test('should have correct PREFAB_ROOT value', () => { + expect(EntityTags.PREFAB_ROOT).toBe(0x0200); + }); + + test('all tags should have unique values', () => { + const values = Object.values(EntityTags).filter((v) => typeof v === 'number'); + const uniqueValues = new Set(values); + expect(uniqueValues.size).toBe(values.length); + }); + }); + + describe('hasEntityTag', () => { + test('should return true when tag is present', () => { + const tag = EntityTags.FOLDER; + expect(hasEntityTag(tag, EntityTags.FOLDER)).toBe(true); + }); + + test('should return false when tag is not present', () => { + const tag = EntityTags.FOLDER; + expect(hasEntityTag(tag, EntityTags.HIDDEN)).toBe(false); + }); + + test('should work with combined tags', () => { + const combined = EntityTags.FOLDER | EntityTags.HIDDEN | EntityTags.LOCKED; + + expect(hasEntityTag(combined, EntityTags.FOLDER)).toBe(true); + expect(hasEntityTag(combined, EntityTags.HIDDEN)).toBe(true); + expect(hasEntityTag(combined, EntityTags.LOCKED)).toBe(true); + expect(hasEntityTag(combined, EntityTags.EDITOR_ONLY)).toBe(false); + }); + + test('should return false for NONE tag', () => { + const tag = EntityTags.NONE; + expect(hasEntityTag(tag, EntityTags.FOLDER)).toBe(false); + }); + }); + + describe('addEntityTag', () => { + test('should add tag to empty tags', () => { + const result = addEntityTag(EntityTags.NONE, EntityTags.FOLDER); + expect(result).toBe(EntityTags.FOLDER); + }); + + test('should add tag to existing tags', () => { + const existing = EntityTags.FOLDER as number; + const result = addEntityTag(existing, EntityTags.HIDDEN); + + expect(hasEntityTag(result, EntityTags.FOLDER)).toBe(true); + expect(hasEntityTag(result, EntityTags.HIDDEN)).toBe(true); + }); + + test('should not change value when adding same tag', () => { + const existing = EntityTags.FOLDER as number; + const result = addEntityTag(existing, EntityTags.FOLDER); + + expect(result).toBe(EntityTags.FOLDER); + }); + + test('should handle multiple tag additions', () => { + let tag: number = EntityTags.NONE; + tag = addEntityTag(tag, EntityTags.FOLDER); + tag = addEntityTag(tag, EntityTags.HIDDEN); + tag = addEntityTag(tag, EntityTags.LOCKED); + + expect(hasEntityTag(tag, EntityTags.FOLDER)).toBe(true); + expect(hasEntityTag(tag, EntityTags.HIDDEN)).toBe(true); + expect(hasEntityTag(tag, EntityTags.LOCKED)).toBe(true); + }); + }); + + describe('removeEntityTag', () => { + test('should remove tag from combined tags', () => { + const combined = (EntityTags.FOLDER | EntityTags.HIDDEN) as number; + const result = removeEntityTag(combined, EntityTags.HIDDEN); + + expect(hasEntityTag(result, EntityTags.FOLDER)).toBe(true); + expect(hasEntityTag(result, EntityTags.HIDDEN)).toBe(false); + }); + + test('should return same value when removing non-existent tag', () => { + const existing = EntityTags.FOLDER as number; + const result = removeEntityTag(existing, EntityTags.HIDDEN); + + expect(result).toBe(EntityTags.FOLDER); + }); + + test('should return NONE when removing last tag', () => { + const result = removeEntityTag(EntityTags.FOLDER, EntityTags.FOLDER); + expect(result).toBe(EntityTags.NONE); + }); + + test('should handle multiple tag removals', () => { + let tag: number = EntityTags.FOLDER | EntityTags.HIDDEN | EntityTags.LOCKED; + tag = removeEntityTag(tag, EntityTags.FOLDER); + tag = removeEntityTag(tag, EntityTags.LOCKED); + + expect(hasEntityTag(tag, EntityTags.FOLDER)).toBe(false); + expect(hasEntityTag(tag, EntityTags.HIDDEN)).toBe(true); + expect(hasEntityTag(tag, EntityTags.LOCKED)).toBe(false); + }); + }); + + describe('isFolder', () => { + test('should return true for folder tag', () => { + expect(isFolder(EntityTags.FOLDER)).toBe(true); + }); + + test('should return true for combined tags including folder', () => { + const combined = EntityTags.FOLDER | EntityTags.HIDDEN; + expect(isFolder(combined)).toBe(true); + }); + + test('should return false for non-folder tag', () => { + expect(isFolder(EntityTags.HIDDEN)).toBe(false); + expect(isFolder(EntityTags.NONE)).toBe(false); + }); + }); + + describe('isHidden', () => { + test('should return true for hidden tag', () => { + expect(isHidden(EntityTags.HIDDEN)).toBe(true); + }); + + test('should return true for combined tags including hidden', () => { + const combined = EntityTags.FOLDER | EntityTags.HIDDEN; + expect(isHidden(combined)).toBe(true); + }); + + test('should return false for non-hidden tag', () => { + expect(isHidden(EntityTags.FOLDER)).toBe(false); + expect(isHidden(EntityTags.NONE)).toBe(false); + }); + }); + + describe('isLocked', () => { + test('should return true for locked tag', () => { + expect(isLocked(EntityTags.LOCKED)).toBe(true); + }); + + test('should return true for combined tags including locked', () => { + const combined = EntityTags.FOLDER | EntityTags.LOCKED; + expect(isLocked(combined)).toBe(true); + }); + + test('should return false for non-locked tag', () => { + expect(isLocked(EntityTags.FOLDER)).toBe(false); + expect(isLocked(EntityTags.NONE)).toBe(false); + }); + }); + + describe('tag combinations', () => { + test('should correctly combine and identify multiple tags', () => { + const tag = (EntityTags.FOLDER | EntityTags.HIDDEN | EntityTags.PREFAB_ROOT) as number; + + expect(isFolder(tag)).toBe(true); + expect(isHidden(tag)).toBe(true); + expect(hasEntityTag(tag, EntityTags.PREFAB_ROOT)).toBe(true); + expect(isLocked(tag)).toBe(false); + }); + + test('should support complex add/remove operations', () => { + let tag: number = EntityTags.NONE; + + // Add tags + tag = addEntityTag(tag, EntityTags.FOLDER); + tag = addEntityTag(tag, EntityTags.HIDDEN); + tag = addEntityTag(tag, EntityTags.LOCKED); + tag = addEntityTag(tag, EntityTags.EDITOR_ONLY); + + expect(isFolder(tag)).toBe(true); + expect(isHidden(tag)).toBe(true); + expect(isLocked(tag)).toBe(true); + expect(hasEntityTag(tag, EntityTags.EDITOR_ONLY)).toBe(true); + + // Remove some tags + tag = removeEntityTag(tag, EntityTags.HIDDEN); + tag = removeEntityTag(tag, EntityTags.LOCKED); + + expect(isFolder(tag)).toBe(true); + expect(isHidden(tag)).toBe(false); + expect(isLocked(tag)).toBe(false); + expect(hasEntityTag(tag, EntityTags.EDITOR_ONLY)).toBe(true); + }); + + test('should work correctly with prefab tags', () => { + const prefabInstanceTag = EntityTags.PREFAB_INSTANCE as number; + const prefabRootTag = EntityTags.PREFAB_ROOT as number; + + expect(hasEntityTag(prefabInstanceTag, EntityTags.PREFAB_INSTANCE)).toBe(true); + expect(hasEntityTag(prefabInstanceTag, EntityTags.PREFAB_ROOT)).toBe(false); + + expect(hasEntityTag(prefabRootTag, EntityTags.PREFAB_ROOT)).toBe(true); + expect(hasEntityTag(prefabRootTag, EntityTags.PREFAB_INSTANCE)).toBe(false); + + // Combine both + const combined = (prefabInstanceTag | prefabRootTag) as number; + expect(hasEntityTag(combined, EntityTags.PREFAB_INSTANCE)).toBe(true); + expect(hasEntityTag(combined, EntityTags.PREFAB_ROOT)).toBe(true); + }); + }); +}); diff --git a/packages/core/tests/ECS/Hierarchy.test.ts b/packages/core/tests/ECS/Hierarchy.test.ts new file mode 100644 index 00000000..39206875 --- /dev/null +++ b/packages/core/tests/ECS/Hierarchy.test.ts @@ -0,0 +1,648 @@ +import { Scene, Entity, HierarchyComponent, HierarchySystem } from '../../src'; + +describe('HierarchySystem', () => { + let scene: Scene; + let hierarchySystem: HierarchySystem; + + beforeEach(() => { + scene = new Scene(); + scene.initialize(); + hierarchySystem = new HierarchySystem(); + scene.addSystem(hierarchySystem); + }); + + afterEach(() => { + scene.end(); + }); + + describe('setParent', () => { + it('should set parent-child relationship', () => { + const parent = scene.createEntity('Parent'); + const child = scene.createEntity('Child'); + + hierarchySystem.setParent(child, parent); + + expect(hierarchySystem.getParent(child)).toBe(parent); + expect(hierarchySystem.getChildren(parent)).toContain(child); + expect(hierarchySystem.getChildCount(parent)).toBe(1); + }); + + it('should auto-add HierarchyComponent if not present', () => { + const parent = scene.createEntity('Parent'); + const child = scene.createEntity('Child'); + + expect(parent.getComponent(HierarchyComponent)).toBeNull(); + expect(child.getComponent(HierarchyComponent)).toBeNull(); + + hierarchySystem.setParent(child, parent); + + expect(parent.getComponent(HierarchyComponent)).not.toBeNull(); + expect(child.getComponent(HierarchyComponent)).not.toBeNull(); + }); + + it('should move child to root when parent is null', () => { + const parent = scene.createEntity('Parent'); + const child = scene.createEntity('Child'); + + hierarchySystem.setParent(child, parent); + expect(hierarchySystem.getParent(child)).toBe(parent); + + hierarchySystem.setParent(child, null); + expect(hierarchySystem.getParent(child)).toBeNull(); + expect(hierarchySystem.getChildren(parent)).not.toContain(child); + }); + + it('should transfer child to new parent', () => { + const parent1 = scene.createEntity('Parent1'); + const parent2 = scene.createEntity('Parent2'); + const child = scene.createEntity('Child'); + + hierarchySystem.setParent(child, parent1); + expect(hierarchySystem.getParent(child)).toBe(parent1); + expect(hierarchySystem.getChildCount(parent1)).toBe(1); + + hierarchySystem.setParent(child, parent2); + expect(hierarchySystem.getParent(child)).toBe(parent2); + expect(hierarchySystem.getChildCount(parent1)).toBe(0); + expect(hierarchySystem.getChildCount(parent2)).toBe(1); + }); + + it('should throw error on circular reference', () => { + const parent = scene.createEntity('Parent'); + const child = scene.createEntity('Child'); + const grandchild = scene.createEntity('Grandchild'); + + hierarchySystem.setParent(child, parent); + hierarchySystem.setParent(grandchild, child); + + expect(() => { + hierarchySystem.setParent(parent, grandchild); + }).toThrow('Cannot set parent: would create circular reference'); + }); + + it('should not change if setting same parent', () => { + const parent = scene.createEntity('Parent'); + const child = scene.createEntity('Child'); + + hierarchySystem.setParent(child, parent); + const hierarchy = child.getComponent(HierarchyComponent)!; + hierarchy.bCacheDirty = false; + + hierarchySystem.setParent(child, parent); + // Should not mark dirty since parent didn't change + expect(hierarchy.bCacheDirty).toBe(false); + }); + }); + + describe('insertChildAt', () => { + it('should insert child at specific position', () => { + const parent = scene.createEntity('Parent'); + const child1 = scene.createEntity('Child1'); + const child2 = scene.createEntity('Child2'); + const child3 = scene.createEntity('Child3'); + + hierarchySystem.setParent(child1, parent); + hierarchySystem.setParent(child3, parent); + hierarchySystem.insertChildAt(parent, child2, 1); + + const children = hierarchySystem.getChildren(parent); + expect(children[0]).toBe(child1); + expect(children[1]).toBe(child2); + expect(children[2]).toBe(child3); + }); + + it('should append child when index is -1', () => { + const parent = scene.createEntity('Parent'); + const child1 = scene.createEntity('Child1'); + const child2 = scene.createEntity('Child2'); + + hierarchySystem.setParent(child1, parent); + hierarchySystem.insertChildAt(parent, child2, -1); + + const children = hierarchySystem.getChildren(parent); + expect(children[children.length - 1]).toBe(child2); + }); + }); + + describe('removeChild', () => { + it('should remove child from parent', () => { + const parent = scene.createEntity('Parent'); + const child = scene.createEntity('Child'); + + hierarchySystem.setParent(child, parent); + expect(hierarchySystem.getChildCount(parent)).toBe(1); + + const result = hierarchySystem.removeChild(parent, child); + expect(result).toBe(true); + expect(hierarchySystem.getChildCount(parent)).toBe(0); + expect(hierarchySystem.getParent(child)).toBeNull(); + }); + + it('should return false if child is not a child of parent', () => { + const parent1 = scene.createEntity('Parent1'); + const parent2 = scene.createEntity('Parent2'); + const child = scene.createEntity('Child'); + + hierarchySystem.setParent(child, parent1); + + const result = hierarchySystem.removeChild(parent2, child); + expect(result).toBe(false); + }); + }); + + describe('removeAllChildren', () => { + it('should remove all children from parent', () => { + const parent = scene.createEntity('Parent'); + const child1 = scene.createEntity('Child1'); + const child2 = scene.createEntity('Child2'); + const child3 = scene.createEntity('Child3'); + + hierarchySystem.setParent(child1, parent); + hierarchySystem.setParent(child2, parent); + hierarchySystem.setParent(child3, parent); + expect(hierarchySystem.getChildCount(parent)).toBe(3); + + hierarchySystem.removeAllChildren(parent); + expect(hierarchySystem.getChildCount(parent)).toBe(0); + expect(hierarchySystem.getParent(child1)).toBeNull(); + expect(hierarchySystem.getParent(child2)).toBeNull(); + expect(hierarchySystem.getParent(child3)).toBeNull(); + }); + }); + + describe('hierarchy queries', () => { + it('should check if entity has children', () => { + const parent = scene.createEntity('Parent'); + const child = scene.createEntity('Child'); + + expect(hierarchySystem.hasChildren(parent)).toBe(false); + + hierarchySystem.setParent(child, parent); + expect(hierarchySystem.hasChildren(parent)).toBe(true); + }); + + it('should check isAncestorOf', () => { + const grandparent = scene.createEntity('Grandparent'); + const parent = scene.createEntity('Parent'); + const child = scene.createEntity('Child'); + + hierarchySystem.setParent(parent, grandparent); + hierarchySystem.setParent(child, parent); + + expect(hierarchySystem.isAncestorOf(grandparent, child)).toBe(true); + expect(hierarchySystem.isAncestorOf(parent, child)).toBe(true); + expect(hierarchySystem.isAncestorOf(child, grandparent)).toBe(false); + }); + + it('should check isDescendantOf', () => { + const grandparent = scene.createEntity('Grandparent'); + const parent = scene.createEntity('Parent'); + const child = scene.createEntity('Child'); + + hierarchySystem.setParent(parent, grandparent); + hierarchySystem.setParent(child, parent); + + expect(hierarchySystem.isDescendantOf(child, grandparent)).toBe(true); + expect(hierarchySystem.isDescendantOf(child, parent)).toBe(true); + expect(hierarchySystem.isDescendantOf(grandparent, child)).toBe(false); + }); + + it('should get root entity', () => { + const root = scene.createEntity('Root'); + const child = scene.createEntity('Child'); + const grandchild = scene.createEntity('Grandchild'); + + hierarchySystem.setParent(child, root); + hierarchySystem.setParent(grandchild, child); + + expect(hierarchySystem.getRoot(grandchild)).toBe(root); + expect(hierarchySystem.getRoot(child)).toBe(root); + expect(hierarchySystem.getRoot(root)).toBe(root); + }); + + it('should get depth correctly', () => { + const root = scene.createEntity('Root'); + const child = scene.createEntity('Child'); + const grandchild = scene.createEntity('Grandchild'); + + root.addComponent(new HierarchyComponent()); + hierarchySystem.setParent(child, root); + hierarchySystem.setParent(grandchild, child); + + expect(hierarchySystem.getDepth(root)).toBe(0); + expect(hierarchySystem.getDepth(child)).toBe(1); + expect(hierarchySystem.getDepth(grandchild)).toBe(2); + }); + }); + + describe('findChild', () => { + it('should find child by name', () => { + const parent = scene.createEntity('Parent'); + const child1 = scene.createEntity('Child1'); + const child2 = scene.createEntity('Target'); + + hierarchySystem.setParent(child1, parent); + hierarchySystem.setParent(child2, parent); + + const found = hierarchySystem.findChild(parent, 'Target'); + expect(found).toBe(child2); + }); + + it('should find child recursively', () => { + const root = scene.createEntity('Root'); + const child = scene.createEntity('Child'); + const grandchild = scene.createEntity('Target'); + + hierarchySystem.setParent(child, root); + hierarchySystem.setParent(grandchild, child); + + const found = hierarchySystem.findChild(root, 'Target', true); + expect(found).toBe(grandchild); + + const notFound = hierarchySystem.findChild(root, 'Target', false); + expect(notFound).toBeNull(); + }); + }); + + describe('forEachChild', () => { + it('should iterate over children', () => { + const parent = scene.createEntity('Parent'); + const child1 = scene.createEntity('Child1'); + const child2 = scene.createEntity('Child2'); + + hierarchySystem.setParent(child1, parent); + hierarchySystem.setParent(child2, parent); + + const visited: Entity[] = []; + hierarchySystem.forEachChild(parent, (child) => { + visited.push(child); + }); + + expect(visited).toContain(child1); + expect(visited).toContain(child2); + expect(visited.length).toBe(2); + }); + + it('should iterate recursively', () => { + const root = scene.createEntity('Root'); + const child = scene.createEntity('Child'); + const grandchild = scene.createEntity('Grandchild'); + + hierarchySystem.setParent(child, root); + hierarchySystem.setParent(grandchild, child); + + const visited: Entity[] = []; + hierarchySystem.forEachChild(root, (entity) => { + visited.push(entity); + }, true); + + expect(visited).toContain(child); + expect(visited).toContain(grandchild); + expect(visited.length).toBe(2); + }); + }); + + describe('getRootEntities', () => { + it('should return all root entities', () => { + const root1 = scene.createEntity('Root1'); + const root2 = scene.createEntity('Root2'); + const child = scene.createEntity('Child'); + + root1.addComponent(new HierarchyComponent()); + root2.addComponent(new HierarchyComponent()); + hierarchySystem.setParent(child, root1); + + const roots = hierarchySystem.getRootEntities(); + expect(roots).toContain(root1); + expect(roots).toContain(root2); + expect(roots).not.toContain(child); + }); + }); + + describe('activeInHierarchy', () => { + it('should be inactive if parent is inactive', () => { + const parent = scene.createEntity('Parent'); + const child = scene.createEntity('Child'); + + hierarchySystem.setParent(child, parent); + + expect(hierarchySystem.isActiveInHierarchy(child)).toBe(true); + + parent.active = false; + // Mark cache dirty to recalculate + const childHierarchy = child.getComponent(HierarchyComponent)!; + childHierarchy.bCacheDirty = true; + + expect(hierarchySystem.isActiveInHierarchy(child)).toBe(false); + }); + + it('should be inactive if self is inactive', () => { + const parent = scene.createEntity('Parent'); + const child = scene.createEntity('Child'); + + hierarchySystem.setParent(child, parent); + child.active = false; + + expect(hierarchySystem.isActiveInHierarchy(child)).toBe(false); + }); + }); +}); + +describe('HierarchyComponent', () => { + it('should have correct default values', () => { + const component = new HierarchyComponent(); + + expect(component.parentId).toBeNull(); + expect(component.childIds).toEqual([]); + expect(component.depth).toBe(0); + expect(component.bActiveInHierarchy).toBe(true); + expect(component.bCacheDirty).toBe(true); + }); +}); + +describe('HierarchySystem - Extended Tests', () => { + let scene: Scene; + let hierarchySystem: HierarchySystem; + + beforeEach(() => { + scene = new Scene(); + scene.initialize(); + hierarchySystem = new HierarchySystem(); + scene.addSystem(hierarchySystem); + }); + + afterEach(() => { + scene.end(); + }); + + describe('findChildrenByTag', () => { + it('should find children by tag', () => { + const parent = scene.createEntity('Parent'); + const child1 = scene.createEntity('Child1'); + const child2 = scene.createEntity('Child2'); + const child3 = scene.createEntity('Child3'); + + child1.tag = 0x01; + child2.tag = 0x02; + child3.tag = 0x01; + + hierarchySystem.setParent(child1, parent); + hierarchySystem.setParent(child2, parent); + hierarchySystem.setParent(child3, parent); + + const found = hierarchySystem.findChildrenByTag(parent, 0x01); + expect(found.length).toBe(2); + expect(found).toContain(child1); + expect(found).toContain(child3); + }); + + it('should find children by tag recursively', () => { + const root = scene.createEntity('Root'); + const child = scene.createEntity('Child'); + const grandchild = scene.createEntity('Grandchild'); + + child.tag = 0x01; + grandchild.tag = 0x01; + + hierarchySystem.setParent(child, root); + hierarchySystem.setParent(grandchild, child); + + const foundNonRecursive = hierarchySystem.findChildrenByTag(root, 0x01, false); + expect(foundNonRecursive.length).toBe(1); + expect(foundNonRecursive[0]).toBe(child); + + const foundRecursive = hierarchySystem.findChildrenByTag(root, 0x01, true); + expect(foundRecursive.length).toBe(2); + expect(foundRecursive).toContain(child); + expect(foundRecursive).toContain(grandchild); + }); + + it('should return empty array when no children match tag', () => { + const parent = scene.createEntity('Parent'); + const child = scene.createEntity('Child'); + child.tag = 0x01; + + hierarchySystem.setParent(child, parent); + + const found = hierarchySystem.findChildrenByTag(parent, 0x02); + expect(found).toEqual([]); + }); + }); + + describe('flattenHierarchy', () => { + it('should flatten hierarchy with expanded nodes', () => { + const root = scene.createEntity('Root'); + const child1 = scene.createEntity('Child1'); + const child2 = scene.createEntity('Child2'); + const grandchild = scene.createEntity('Grandchild'); + + root.addComponent(new HierarchyComponent()); + hierarchySystem.setParent(child1, root); + hierarchySystem.setParent(child2, root); + hierarchySystem.setParent(grandchild, child1); + + const expandedIds = new Set([root.id, child1.id]); + const flattened = hierarchySystem.flattenHierarchy(expandedIds); + + expect(flattened.length).toBe(4); + expect(flattened[0].entity).toBe(root); + expect(flattened[0].depth).toBe(0); + expect(flattened[0].bHasChildren).toBe(true); + expect(flattened[0].bIsExpanded).toBe(true); + }); + + it('should not include children of collapsed nodes', () => { + const root = scene.createEntity('Root'); + const child = scene.createEntity('Child'); + const grandchild = scene.createEntity('Grandchild'); + + root.addComponent(new HierarchyComponent()); + hierarchySystem.setParent(child, root); + hierarchySystem.setParent(grandchild, child); + + // Root is expanded, but child is collapsed + const expandedIds = new Set([root.id]); + const flattened = hierarchySystem.flattenHierarchy(expandedIds); + + expect(flattened.length).toBe(2); + expect(flattened[0].entity).toBe(root); + expect(flattened[1].entity).toBe(child); + expect(flattened[1].bHasChildren).toBe(true); + expect(flattened[1].bIsExpanded).toBe(false); + }); + + it('should return empty array when no root entities', () => { + const flattened = hierarchySystem.flattenHierarchy(new Set()); + expect(flattened).toEqual([]); + }); + }); + + describe('updateOrder', () => { + it('should have negative update order for early processing', () => { + expect(hierarchySystem.updateOrder).toBe(-1000); + }); + }); + + describe('process - cache update', () => { + it('should update dirty caches during process', () => { + const parent = scene.createEntity('Parent'); + const child = scene.createEntity('Child'); + + hierarchySystem.setParent(child, parent); + + // Cache should be dirty after setParent + const childHierarchy = child.getComponent(HierarchyComponent)!; + expect(childHierarchy.bCacheDirty).toBe(true); + + // Update scene to process + scene.update(); + + // Cache should be clean after process + expect(childHierarchy.bCacheDirty).toBe(false); + }); + }); + + describe('insertChildAt edge cases', () => { + it('should handle circular reference prevention', () => { + const parent = scene.createEntity('Parent'); + const child = scene.createEntity('Child'); + const grandchild = scene.createEntity('Grandchild'); + + hierarchySystem.setParent(child, parent); + hierarchySystem.setParent(grandchild, child); + + expect(() => { + hierarchySystem.insertChildAt(grandchild, parent, 0); + }).toThrow('Cannot set parent: would create circular reference'); + }); + + it('should move child within same parent to different position', () => { + const parent = scene.createEntity('Parent'); + const child1 = scene.createEntity('Child1'); + const child2 = scene.createEntity('Child2'); + const child3 = scene.createEntity('Child3'); + + hierarchySystem.setParent(child1, parent); + hierarchySystem.setParent(child2, parent); + hierarchySystem.setParent(child3, parent); + + // Move child3 to position 0 + hierarchySystem.insertChildAt(parent, child3, 0); + + const children = hierarchySystem.getChildren(parent); + expect(children[0]).toBe(child3); + expect(children[1]).toBe(child1); + expect(children[2]).toBe(child2); + }); + }); + + describe('removeChild edge cases', () => { + it('should return false when parent has no HierarchyComponent', () => { + const parent = scene.createEntity('Parent'); + const child = scene.createEntity('Child'); + + const result = hierarchySystem.removeChild(parent, child); + expect(result).toBe(false); + }); + + it('should return false when child has no HierarchyComponent', () => { + const parent = scene.createEntity('Parent'); + const child = scene.createEntity('Child'); + + parent.addComponent(new HierarchyComponent()); + + const result = hierarchySystem.removeChild(parent, child); + expect(result).toBe(false); + }); + }); + + describe('removeAllChildren edge cases', () => { + it('should handle entity with no HierarchyComponent', () => { + const parent = scene.createEntity('Parent'); + + expect(() => { + hierarchySystem.removeAllChildren(parent); + }).not.toThrow(); + }); + }); + + describe('getChildren edge cases', () => { + it('should return empty array when entity has no HierarchyComponent', () => { + const entity = scene.createEntity('Entity'); + const children = hierarchySystem.getChildren(entity); + expect(children).toEqual([]); + }); + }); + + describe('getChildCount edge cases', () => { + it('should return 0 when entity has no HierarchyComponent', () => { + const entity = scene.createEntity('Entity'); + expect(hierarchySystem.getChildCount(entity)).toBe(0); + }); + }); + + describe('getDepth edge cases', () => { + it('should return 0 when entity has no HierarchyComponent', () => { + const entity = scene.createEntity('Entity'); + expect(hierarchySystem.getDepth(entity)).toBe(0); + }); + + it('should use cached depth when cache is valid', () => { + const parent = scene.createEntity('Parent'); + const child = scene.createEntity('Child'); + + parent.addComponent(new HierarchyComponent()); + hierarchySystem.setParent(child, parent); + + // First call computes depth + const depth1 = hierarchySystem.getDepth(child); + expect(depth1).toBe(1); + + // Mark cache as valid + const childHierarchy = child.getComponent(HierarchyComponent)!; + childHierarchy.bCacheDirty = false; + + // Second call should use cache + const depth2 = hierarchySystem.getDepth(child); + expect(depth2).toBe(1); + }); + }); + + describe('isActiveInHierarchy edge cases', () => { + it('should return entity.active when entity has no HierarchyComponent', () => { + const entity = scene.createEntity('Entity'); + entity.active = true; + expect(hierarchySystem.isActiveInHierarchy(entity)).toBe(true); + + entity.active = false; + expect(hierarchySystem.isActiveInHierarchy(entity)).toBe(false); + }); + + it('should use cached value when cache is valid', () => { + const parent = scene.createEntity('Parent'); + const child = scene.createEntity('Child'); + + hierarchySystem.setParent(child, parent); + + // First call computes activeInHierarchy + const active1 = hierarchySystem.isActiveInHierarchy(child); + expect(active1).toBe(true); + + // Mark cache as valid + const childHierarchy = child.getComponent(HierarchyComponent)!; + childHierarchy.bCacheDirty = false; + + // Second call should use cache + const active2 = hierarchySystem.isActiveInHierarchy(child); + expect(active2).toBe(true); + }); + }); + + describe('dispose', () => { + it('should not throw when disposing', () => { + expect(() => { + hierarchySystem.dispose(); + }).not.toThrow(); + }); + }); +}); diff --git a/packages/core/tests/ECS/Scene.test.ts b/packages/core/tests/ECS/Scene.test.ts index 9a515052..1e82d94d 100644 --- a/packages/core/tests/ECS/Scene.test.ts +++ b/packages/core/tests/ECS/Scene.test.ts @@ -629,4 +629,201 @@ describe('Scene - 场景管理系统测试', () => { scene2.end(); }); }); + + describe('扩展测试 - 补齐覆盖率', () => { + describe('实体标签查找', () => { + test('findEntitiesByTag 应该返回具有指定标签的实体', () => { + const entity1 = scene.createEntity('Entity1'); + entity1.tag = 0x01; + + const entity2 = scene.createEntity('Entity2'); + entity2.tag = 0x02; + + const entity3 = scene.createEntity('Entity3'); + entity3.tag = 0x01; + + const found = scene.findEntitiesByTag(0x01); + + expect(found.length).toBe(2); + expect(found).toContain(entity1); + expect(found).toContain(entity3); + }); + + test('findEntitiesByTag 应该在没有匹配时返回空数组', () => { + scene.createEntity('Entity1'); + + const found = scene.findEntitiesByTag(0xFF); + + expect(found).toEqual([]); + }); + }); + + describe('批量实体操作', () => { + test('destroyEntities 应该批量销毁实体', () => { + const entities = scene.createEntities(5, 'Entity'); + expect(scene.entities.count).toBe(5); + + const toDestroy = entities.slice(0, 3); + scene.destroyEntities(toDestroy); + + expect(scene.entities.count).toBe(2); + }); + + test('destroyEntities 应该处理空数组', () => { + scene.createEntities(3, 'Entity'); + + expect(() => { + scene.destroyEntities([]); + }).not.toThrow(); + + expect(scene.entities.count).toBe(3); + }); + }); + + describe('查询方法', () => { + test('queryAny 应该返回具有任意一个组件的实体', () => { + const entity1 = scene.createEntity('Entity1'); + entity1.addComponent(new PositionComponent()); + + const entity2 = scene.createEntity('Entity2'); + entity2.addComponent(new VelocityComponent()); + + const entity3 = scene.createEntity('Entity3'); + entity3.addComponent(new HealthComponent()); + + const result = scene.queryAny(PositionComponent, VelocityComponent); + + expect(result.entities.length).toBe(2); + }); + + test('queryNone 应该返回不包含指定组件的实体', () => { + const entity1 = scene.createEntity('Entity1'); + entity1.addComponent(new PositionComponent()); + + const entity2 = scene.createEntity('Entity2'); + entity2.addComponent(new VelocityComponent()); + + const entity3 = scene.createEntity('Entity3'); + + const result = scene.queryNone(PositionComponent); + + expect(result.entities.length).toBe(2); + expect(result.entities).toContain(entity2); + expect(result.entities).toContain(entity3); + }); + + test('query 应该创建类型安全的查询构建器', () => { + const builder = scene.query(); + expect(builder).toBeDefined(); + + const matcher = builder.withAll(PositionComponent).buildMatcher(); + expect(matcher).toBeDefined(); + }); + }); + + describe('服务容器', () => { + test('scene.services 应该返回服务容器', () => { + expect(scene.services).toBeDefined(); + }); + }); + + describe('系统错误处理', () => { + test('频繁出错的系统应该被自动禁用', () => { + const consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + + class ErrorProneSystem extends EntitySystem { + constructor() { + super(Matcher.empty().all(PositionComponent)); + } + + protected override process(): void { + throw new Error('Intentional error'); + } + } + + const system = new ErrorProneSystem(); + scene.addEntityProcessor(system); + + const entity = scene.createEntity('TestEntity'); + entity.addComponent(new PositionComponent(0, 0)); + + // 多次更新以触发错误阈值 + for (let i = 0; i < 15; i++) { + scene.update(); + } + + // 系统应该被禁用 + expect(system.enabled).toBe(false); + + consoleSpy.mockRestore(); + }); + }); + + describe('已废弃方法', () => { + test('getEntityByName 应该作为 findEntity 的别名工作', () => { + const entity = scene.createEntity('TestEntity'); + + const found = scene.getEntityByName('TestEntity'); + + expect(found).toBe(entity); + }); + + test('getEntitiesByTag 应该作为 findEntitiesByTag 的别名工作', () => { + const entity = scene.createEntity('Entity'); + entity.tag = 0x10; + + const found = scene.getEntitiesByTag(0x10); + + expect(found.length).toBe(1); + expect(found[0]).toBe(entity); + }); + }); + + describe('系统管理扩展', () => { + test('getSystem 应该返回指定类型的系统', () => { + const movementSystem = new MovementSystem(); + scene.addEntityProcessor(movementSystem); + + const found = scene.getSystem(MovementSystem); + + expect(found).toBe(movementSystem); + }); + + test('getSystem 应该在系统不存在时返回 null', () => { + const found = scene.getSystem(MovementSystem); + + expect(found).toBeNull(); + }); + + test('markSystemsOrderDirty 应该标记系统顺序为脏', () => { + const system1 = new MovementSystem(); + const system2 = new RenderSystem(); + + scene.addEntityProcessor(system1); + scene.addEntityProcessor(system2); + + // 访问 systems 以清除脏标记 + const _ = scene.systems; + + // 标记为脏 + scene.markSystemsOrderDirty(); + + // 再次访问应该重新构建缓存 + const systems = scene.systems; + expect(systems).toBeDefined(); + }); + }); + + describe('延迟缓存清理', () => { + test('addEntity 应该支持延迟缓存清理', () => { + scene.createEntity('Entity1'); + const entity2 = new Entity('Entity2', scene.identifierPool.checkOut()); + + // 延迟缓存清理 + scene.addEntity(entity2, true); + + expect(scene.entities.count).toBe(2); + }); + }); + }); }); \ No newline at end of file diff --git a/packages/core/tests/ECS/Serialization/EntitySerializer.test.ts b/packages/core/tests/ECS/Serialization/EntitySerializer.test.ts new file mode 100644 index 00000000..36809ab7 --- /dev/null +++ b/packages/core/tests/ECS/Serialization/EntitySerializer.test.ts @@ -0,0 +1,495 @@ +import { EntitySerializer, SerializedEntity } from '../../../src/ECS/Serialization/EntitySerializer'; +import { Scene } from '../../../src/ECS/Scene'; +import { Entity } from '../../../src/ECS/Entity'; +import { Component } from '../../../src/ECS/Component'; +import { HierarchySystem } from '../../../src/ECS/Systems/HierarchySystem'; +import { HierarchyComponent } from '../../../src/ECS/Components/HierarchyComponent'; +import { ECSComponent } from '../../../src/ECS/Decorators'; +import { ComponentRegistry, ComponentType } from '../../../src/ECS/Core/ComponentStorage'; +import { Serializable, Serialize } from '../../../src/ECS/Serialization'; + +@ECSComponent('EntitySerTest_Position') +@Serializable({ version: 1 }) +class PositionComponent extends Component { + @Serialize() + public x: number = 0; + + @Serialize() + public y: number = 0; + + constructor(x: number = 0, y: number = 0) { + super(); + this.x = x; + this.y = y; + } +} + +@ECSComponent('EntitySerTest_Velocity') +@Serializable({ version: 1 }) +class VelocityComponent extends Component { + @Serialize() + public vx: number = 0; + + @Serialize() + public vy: number = 0; +} + +describe('EntitySerializer', () => { + let scene: Scene; + let hierarchySystem: HierarchySystem; + let componentRegistry: Map; + + beforeEach(() => { + ComponentRegistry.reset(); + ComponentRegistry.register(PositionComponent); + ComponentRegistry.register(VelocityComponent); + ComponentRegistry.register(HierarchyComponent); + + scene = new Scene({ name: 'EntitySerializerTestScene' }); + hierarchySystem = new HierarchySystem(); + scene.addSystem(hierarchySystem); + + componentRegistry = ComponentRegistry.getAllComponentNames() as Map; + }); + + afterEach(() => { + scene.end(); + }); + + describe('serialize', () => { + test('should serialize basic entity properties', () => { + const entity = scene.createEntity('TestEntity'); + entity.tag = 42; + entity.active = false; + entity.enabled = false; + entity.updateOrder = 10; + + const serialized = EntitySerializer.serialize(entity, false); + + expect(serialized.id).toBe(entity.id); + expect(serialized.name).toBe('TestEntity'); + expect(serialized.tag).toBe(42); + expect(serialized.active).toBe(false); + expect(serialized.enabled).toBe(false); + expect(serialized.updateOrder).toBe(10); + }); + + test('should serialize entity with components', () => { + const entity = scene.createEntity('Entity'); + entity.addComponent(new PositionComponent(100, 200)); + entity.addComponent(new VelocityComponent()); + + const serialized = EntitySerializer.serialize(entity, false); + + expect(serialized.components.length).toBe(2); + }); + + test('should serialize entity without children when includeChildren is false', () => { + const parent = scene.createEntity('Parent'); + const child = scene.createEntity('Child'); + hierarchySystem.setParent(child, parent); + + const serialized = EntitySerializer.serialize(parent, false, hierarchySystem); + + expect(serialized.children).toEqual([]); + }); + + test('should serialize entity with children when includeChildren is true', () => { + const parent = scene.createEntity('Parent'); + const child1 = scene.createEntity('Child1'); + const child2 = scene.createEntity('Child2'); + + hierarchySystem.setParent(child1, parent); + hierarchySystem.setParent(child2, parent); + + const serialized = EntitySerializer.serialize(parent, true, hierarchySystem); + + expect(serialized.children.length).toBe(2); + expect(serialized.children.some(c => c.name === 'Child1')).toBe(true); + expect(serialized.children.some(c => c.name === 'Child2')).toBe(true); + }); + + test('should serialize nested hierarchy', () => { + const root = scene.createEntity('Root'); + const child = scene.createEntity('Child'); + const grandchild = scene.createEntity('Grandchild'); + + hierarchySystem.setParent(child, root); + hierarchySystem.setParent(grandchild, child); + + const serialized = EntitySerializer.serialize(root, true, hierarchySystem); + + expect(serialized.children.length).toBe(1); + expect(serialized.children[0].name).toBe('Child'); + expect(serialized.children[0].children.length).toBe(1); + expect(serialized.children[0].children[0].name).toBe('Grandchild'); + }); + + test('should include parentId in serialized data', () => { + const parent = scene.createEntity('Parent'); + const child = scene.createEntity('Child'); + hierarchySystem.setParent(child, parent); + + const serializedChild = EntitySerializer.serialize(child, false, hierarchySystem); + + expect(serializedChild.parentId).toBe(parent.id); + }); + }); + + describe('deserialize', () => { + test('should deserialize basic entity properties', () => { + const serialized: SerializedEntity = { + id: 999, + name: 'DeserializedEntity', + tag: 77, + active: false, + enabled: false, + updateOrder: 5, + components: [], + children: [] + }; + + let nextId = 1; + const entity = EntitySerializer.deserialize( + serialized, + componentRegistry, + () => nextId++, + false + ); + + expect(entity.name).toBe('DeserializedEntity'); + expect(entity.tag).toBe(77); + expect(entity.active).toBe(false); + expect(entity.enabled).toBe(false); + expect(entity.updateOrder).toBe(5); + }); + + test('should preserve IDs when preserveIds is true', () => { + const serialized: SerializedEntity = { + id: 999, + name: 'Entity', + tag: 0, + active: true, + enabled: true, + updateOrder: 0, + components: [], + children: [] + }; + + const entity = EntitySerializer.deserialize( + serialized, + componentRegistry, + () => 1, + true + ); + + expect(entity.id).toBe(999); + }); + + test('should generate new IDs when preserveIds is false', () => { + const serialized: SerializedEntity = { + id: 999, + name: 'Entity', + tag: 0, + active: true, + enabled: true, + updateOrder: 0, + components: [], + children: [] + }; + + let nextId = 100; + const entity = EntitySerializer.deserialize( + serialized, + componentRegistry, + () => nextId++, + false + ); + + expect(entity.id).toBe(100); + }); + + test('should deserialize components', () => { + const serialized: SerializedEntity = { + id: 1, + name: 'Entity', + tag: 0, + active: true, + enabled: true, + updateOrder: 0, + components: [ + { type: 'EntitySerTest_Position', version: 1, data: { x: 100, y: 200 } } + ], + children: [] + }; + + const entity = EntitySerializer.deserialize( + serialized, + componentRegistry, + () => 1, + true, + scene + ); + + expect(entity.hasComponent(PositionComponent)).toBe(true); + const pos = entity.getComponent(PositionComponent)!; + expect(pos.x).toBe(100); + expect(pos.y).toBe(200); + }); + + test('should deserialize children with hierarchy relationships', () => { + const serialized: SerializedEntity = { + id: 1, + name: 'Parent', + tag: 0, + active: true, + enabled: true, + updateOrder: 0, + components: [], + children: [ + { + id: 2, + name: 'Child', + tag: 0, + active: true, + enabled: true, + updateOrder: 0, + components: [], + children: [] + } + ] + }; + + let nextId = 10; + const allEntities = new Map(); + const entity = EntitySerializer.deserialize( + serialized, + componentRegistry, + () => nextId++, + false, + scene, + hierarchySystem, + allEntities + ); + + expect(allEntities.size).toBe(2); + + // Add deserialized entities to scene so hierarchySystem can find them + for (const [, e] of allEntities) { + scene.addEntity(e); + } + + const children = hierarchySystem.getChildren(entity); + expect(children.length).toBe(1); + expect(children[0].name).toBe('Child'); + }); + }); + + describe('serializeEntities', () => { + test('should serialize multiple entities', () => { + const entity1 = scene.createEntity('Entity1'); + const entity2 = scene.createEntity('Entity2'); + + const serialized = EntitySerializer.serializeEntities([entity1, entity2], false); + + expect(serialized.length).toBe(2); + }); + + test('should only serialize root entities when includeChildren is true', () => { + const root = scene.createEntity('Root'); + const child = scene.createEntity('Child'); + hierarchySystem.setParent(child, root); + + const serialized = EntitySerializer.serializeEntities( + [root, child], + true, + hierarchySystem + ); + + // Should only have root (child is serialized inside root) + expect(serialized.length).toBe(1); + expect(serialized[0].name).toBe('Root'); + expect(serialized[0].children.length).toBe(1); + }); + }); + + describe('deserializeEntities', () => { + test('should deserialize multiple entities', () => { + const serializedEntities: SerializedEntity[] = [ + { + id: 1, + name: 'Entity1', + tag: 0, + active: true, + enabled: true, + updateOrder: 0, + components: [], + children: [] + }, + { + id: 2, + name: 'Entity2', + tag: 0, + active: true, + enabled: true, + updateOrder: 0, + components: [], + children: [] + } + ]; + + let nextId = 100; + const { rootEntities, allEntities } = EntitySerializer.deserializeEntities( + serializedEntities, + componentRegistry, + () => nextId++, + false, + scene + ); + + expect(rootEntities.length).toBe(2); + expect(allEntities.size).toBe(2); + }); + + test('should deserialize entities with nested hierarchy', () => { + const serializedEntities: SerializedEntity[] = [ + { + id: 1, + name: 'Root', + tag: 0, + active: true, + enabled: true, + updateOrder: 0, + components: [], + children: [ + { + id: 2, + name: 'Child', + tag: 0, + active: true, + enabled: true, + updateOrder: 0, + components: [], + children: [ + { + id: 3, + name: 'Grandchild', + tag: 0, + active: true, + enabled: true, + updateOrder: 0, + components: [], + children: [] + } + ] + } + ] + } + ]; + + let nextId = 10; + const { rootEntities, allEntities } = EntitySerializer.deserializeEntities( + serializedEntities, + componentRegistry, + () => nextId++, + false, + scene, + hierarchySystem + ); + + expect(rootEntities.length).toBe(1); + expect(allEntities.size).toBe(3); + }); + }); + + describe('clone', () => { + test('should clone entity with new ID', () => { + const original = scene.createEntity('Original'); + original.tag = 99; + original.addComponent(new PositionComponent(50, 100)); + + // Use serialize + deserialize with scene for proper cloning + const serialized = EntitySerializer.serialize(original, false); + let nextId = 1000; + const cloned = EntitySerializer.deserialize( + serialized, + componentRegistry, + () => nextId++, + false, + scene // Pass scene so components can be added + ); + + expect(cloned.id).not.toBe(original.id); + expect(cloned.name).toBe('Original'); + expect(cloned.tag).toBe(99); + expect(cloned.hasComponent(PositionComponent)).toBe(true); + + const clonedPos = cloned.getComponent(PositionComponent)!; + expect(clonedPos.x).toBe(50); + expect(clonedPos.y).toBe(100); + }); + + test('should clone entity basic properties without components', () => { + // Test the clone method directly with entity that has no components + const original = scene.createEntity('Original'); + original.tag = 99; + original.active = false; + original.enabled = false; + original.updateOrder = 5; + + let nextId = 1000; + const cloned = EntitySerializer.clone( + original, + componentRegistry, + () => nextId++ + ); + + expect(cloned.id).not.toBe(original.id); + expect(cloned.name).toBe('Original'); + expect(cloned.tag).toBe(99); + expect(cloned.active).toBe(false); + expect(cloned.enabled).toBe(false); + expect(cloned.updateOrder).toBe(5); + }); + + test('should clone entity with children hierarchy data', () => { + const parent = scene.createEntity('Parent'); + const child = scene.createEntity('Child'); + hierarchySystem.setParent(child, parent); + + // Serialize with children + const serialized = EntitySerializer.serialize(parent, true, hierarchySystem); + + // Verify the serialized data contains children + expect(serialized.children.length).toBe(1); + expect(serialized.children[0].name).toBe('Child'); + }); + }); + + describe('edge cases', () => { + test('should handle entity with no components', () => { + const entity = scene.createEntity('Empty'); + const serialized = EntitySerializer.serialize(entity, false); + + expect(serialized.components).toEqual([]); + }); + + test('should handle entity with no hierarchy component', () => { + const entity = new Entity('Standalone', 999); + const serialized = EntitySerializer.serialize(entity, true); + + expect(serialized.children).toEqual([]); + expect(serialized.parentId).toBeUndefined(); + }); + + test('should handle default values in serialization', () => { + const entity = scene.createEntity('Default'); + + const serialized = EntitySerializer.serialize(entity, false); + + expect(serialized.active).toBe(true); + expect(serialized.enabled).toBe(true); + expect(serialized.updateOrder).toBe(0); + }); + }); +}); diff --git a/packages/core/tests/ECS/Serialization/ParentChildSerialization.test.ts b/packages/core/tests/ECS/Serialization/ParentChildSerialization.test.ts deleted file mode 100644 index 69c9c809..00000000 --- a/packages/core/tests/ECS/Serialization/ParentChildSerialization.test.ts +++ /dev/null @@ -1,117 +0,0 @@ -/** - * 父子实体序列化和反序列化测试 - * - * 测试场景序列化和反序列化时父子实体关系的正确性 - */ - -import { Scene } from '../../../src'; - -describe('父子实体序列化测试', () => { - let scene: Scene; - - beforeEach(() => { - scene = new Scene({ name: 'TestScene' }); - }); - - afterEach(() => { - scene.end(); - }); - - test('应该正确反序列化父子实体层次结构', () => { - // 创建父实体 - const parent = scene.createEntity('parent'); - parent.tag = 100; - - // 创建2个子实体 - const child1 = scene.createEntity('child1'); - child1.tag = 200; - parent.addChild(child1); - - const child2 = scene.createEntity('child2'); - child2.tag = 200; - parent.addChild(child2); - - // 创建1个顶层实体(对照组) - const topLevel = scene.createEntity('topLevel'); - topLevel.tag = 200; - - // 验证序列化前的状态 - expect(scene.querySystem.queryAll().entities.length).toBe(4); - expect(scene.findEntitiesByTag(100).length).toBe(1); - expect(scene.findEntitiesByTag(200).length).toBe(3); - - // 序列化 - const serialized = scene.serialize({ format: 'json' }); - - // 创建新场景并反序列化 - const scene2 = new Scene({ name: 'LoadTestScene' }); - scene2.deserialize(serialized as string, { - strategy: 'replace', - preserveIds: true, - }); - - // 验证所有实体都被正确恢复 - const allEntities = scene2.querySystem.queryAll().entities; - expect(allEntities.length).toBe(4); - expect(scene2.findEntitiesByTag(100).length).toBe(1); - expect(scene2.findEntitiesByTag(200).length).toBe(3); - - // 验证父子关系正确恢复 - const restoredParent = scene2.findEntity('parent'); - expect(restoredParent).not.toBeNull(); - expect(restoredParent!.children.length).toBe(2); - - const restoredChild1 = scene2.findEntity('child1'); - const restoredChild2 = scene2.findEntity('child2'); - expect(restoredChild1).not.toBeNull(); - expect(restoredChild2).not.toBeNull(); - expect(restoredChild1!.parent).toBe(restoredParent); - expect(restoredChild2!.parent).toBe(restoredParent); - - scene2.end(); - }); - - test('应该正确反序列化多层级实体层次结构', () => { - // 创建多层级实体结构:grandparent -> parent -> child - const grandparent = scene.createEntity('grandparent'); - grandparent.tag = 1; - - const parent = scene.createEntity('parent'); - parent.tag = 2; - grandparent.addChild(parent); - - const child = scene.createEntity('child'); - child.tag = 3; - parent.addChild(child); - - expect(scene.querySystem.queryAll().entities.length).toBe(3); - - // 序列化 - const serialized = scene.serialize({ format: 'json' }); - - // 创建新场景并反序列化 - const scene2 = new Scene({ name: 'LoadTestScene' }); - scene2.deserialize(serialized as string, { - strategy: 'replace', - preserveIds: true, - }); - - // 验证多层级结构正确恢复 - expect(scene2.querySystem.queryAll().entities.length).toBe(3); - - const restoredGrandparent = scene2.findEntity('grandparent'); - const restoredParent = scene2.findEntity('parent'); - const restoredChild = scene2.findEntity('child'); - - expect(restoredGrandparent).not.toBeNull(); - expect(restoredParent).not.toBeNull(); - expect(restoredChild).not.toBeNull(); - - expect(restoredParent!.parent).toBe(restoredGrandparent); - expect(restoredChild!.parent).toBe(restoredParent); - expect(restoredGrandparent!.children.length).toBe(1); - expect(restoredParent!.children.length).toBe(1); - - scene2.end(); - }); -}); diff --git a/packages/core/tests/ECS/Serialization/SceneSerializer.test.ts b/packages/core/tests/ECS/Serialization/SceneSerializer.test.ts new file mode 100644 index 00000000..1ed03a46 --- /dev/null +++ b/packages/core/tests/ECS/Serialization/SceneSerializer.test.ts @@ -0,0 +1,439 @@ +import { SceneSerializer } from '../../../src/ECS/Serialization/SceneSerializer'; +import { Scene } from '../../../src/ECS/Scene'; +import { Component } from '../../../src/ECS/Component'; +import { HierarchySystem } from '../../../src/ECS/Systems/HierarchySystem'; +import { ECSComponent } from '../../../src/ECS/Decorators'; +import { ComponentRegistry, ComponentType } from '../../../src/ECS/Core/ComponentStorage'; +import { Serializable, Serialize } from '../../../src/ECS/Serialization'; + +@ECSComponent('SceneSerTest_Position') +@Serializable({ version: 1 }) +class PositionComponent extends Component { + @Serialize() + public x: number = 0; + + @Serialize() + public y: number = 0; + + constructor(x: number = 0, y: number = 0) { + super(); + this.x = x; + this.y = y; + } +} + +@ECSComponent('SceneSerTest_Velocity') +@Serializable({ version: 1 }) +class VelocityComponent extends Component { + @Serialize() + public vx: number = 0; + + @Serialize() + public vy: number = 0; +} + +describe('SceneSerializer', () => { + let scene: Scene; + let componentRegistry: Map; + + beforeEach(() => { + ComponentRegistry.reset(); + ComponentRegistry.register(PositionComponent); + ComponentRegistry.register(VelocityComponent); + + scene = new Scene({ name: 'SceneSerializerTestScene' }); + + componentRegistry = ComponentRegistry.getAllComponentNames() as Map; + }); + + afterEach(() => { + scene.end(); + }); + + describe('serialize', () => { + test('should serialize scene to JSON string', () => { + scene.createEntity('Entity1').addComponent(new PositionComponent(10, 20)); + scene.createEntity('Entity2').addComponent(new VelocityComponent()); + + const result = SceneSerializer.serialize(scene); + + expect(typeof result).toBe('string'); + const parsed = JSON.parse(result as string); + expect(parsed.name).toBe('SceneSerializerTestScene'); + expect(parsed.entities.length).toBe(2); + }); + + test('should serialize scene to binary format', () => { + scene.createEntity('Entity'); + + const result = SceneSerializer.serialize(scene, { format: 'binary' }); + + expect(result).toBeInstanceOf(Uint8Array); + }); + + test('should include metadata when requested', () => { + scene.createEntity('Entity'); + + const result = SceneSerializer.serialize(scene, { includeMetadata: true }); + const parsed = JSON.parse(result as string); + + expect(parsed.metadata).toBeDefined(); + expect(parsed.metadata.entityCount).toBe(1); + expect(parsed.timestamp).toBeDefined(); + }); + + test('should pretty print JSON when requested', () => { + scene.createEntity('Entity'); + + const result = SceneSerializer.serialize(scene, { pretty: true }); + + expect(typeof result).toBe('string'); + expect((result as string).includes('\n')).toBe(true); + expect((result as string).includes(' ')).toBe(true); + }); + + test('should serialize scene data', () => { + scene.sceneData.set('level', 5); + scene.sceneData.set('config', { difficulty: 'hard' }); + + const result = SceneSerializer.serialize(scene); + const parsed = JSON.parse(result as string); + + expect(parsed.sceneData).toBeDefined(); + expect(parsed.sceneData.level).toBe(5); + expect(parsed.sceneData.config.difficulty).toBe('hard'); + }); + + test('should serialize with component filter', () => { + scene.createEntity('Entity1').addComponent(new PositionComponent()); + scene.createEntity('Entity2').addComponent(new VelocityComponent()); + + const result = SceneSerializer.serialize(scene, { + components: [PositionComponent] + }); + const parsed = JSON.parse(result as string); + + // Only entities with PositionComponent should be included + expect(parsed.entities.length).toBe(1); + }); + }); + + describe('deserialize', () => { + test('should deserialize scene from JSON string', () => { + scene.createEntity('Entity1').addComponent(new PositionComponent(100, 200)); + + const serialized = SceneSerializer.serialize(scene); + + const newScene = new Scene({ name: 'NewScene' }); + SceneSerializer.deserialize(newScene, serialized, { componentRegistry }); + + expect(newScene.entities.count).toBe(1); + const entity = newScene.findEntity('Entity1'); + expect(entity).not.toBeNull(); + expect(entity!.hasComponent(PositionComponent)).toBe(true); + + const pos = entity!.getComponent(PositionComponent)!; + expect(pos.x).toBe(100); + expect(pos.y).toBe(200); + + newScene.end(); + }); + + test('should deserialize scene from binary format', () => { + scene.createEntity('BinaryEntity').addComponent(new PositionComponent(50, 75)); + + const serialized = SceneSerializer.serialize(scene, { format: 'binary' }); + + const newScene = new Scene({ name: 'NewScene' }); + SceneSerializer.deserialize(newScene, serialized, { componentRegistry }); + + expect(newScene.entities.count).toBe(1); + const entity = newScene.findEntity('BinaryEntity'); + expect(entity).not.toBeNull(); + + newScene.end(); + }); + + test('should replace existing entities with strategy replace', () => { + scene.createEntity('Original'); + const serialized = SceneSerializer.serialize(scene); + + const targetScene = new Scene({ name: 'Target' }); + targetScene.createEntity('Existing1'); + targetScene.createEntity('Existing2'); + expect(targetScene.entities.count).toBe(2); + + SceneSerializer.deserialize(targetScene, serialized, { + strategy: 'replace', + componentRegistry + }); + + expect(targetScene.entities.count).toBe(1); + expect(targetScene.findEntity('Original')).not.toBeNull(); + expect(targetScene.findEntity('Existing1')).toBeNull(); + + targetScene.end(); + }); + + test('should merge with existing entities with strategy merge', () => { + scene.createEntity('FromSave'); + const serialized = SceneSerializer.serialize(scene); + + const targetScene = new Scene({ name: 'Target' }); + targetScene.createEntity('Existing'); + expect(targetScene.entities.count).toBe(1); + + SceneSerializer.deserialize(targetScene, serialized, { + strategy: 'merge', + componentRegistry + }); + + expect(targetScene.entities.count).toBe(2); + expect(targetScene.findEntity('Existing')).not.toBeNull(); + expect(targetScene.findEntity('FromSave')).not.toBeNull(); + + targetScene.end(); + }); + + test('should restore scene data', () => { + scene.sceneData.set('weather', 'sunny'); + scene.sceneData.set('time', 12.5); + + const serialized = SceneSerializer.serialize(scene); + + const newScene = new Scene({ name: 'NewScene' }); + SceneSerializer.deserialize(newScene, serialized, { componentRegistry }); + + expect(newScene.sceneData.get('weather')).toBe('sunny'); + expect(newScene.sceneData.get('time')).toBe(12.5); + + newScene.end(); + }); + + test('should call migration function when versions differ', () => { + scene.createEntity('Entity'); + const serialized = SceneSerializer.serialize(scene); + + // Manually modify version + const parsed = JSON.parse(serialized as string); + parsed.version = 0; + const modifiedSerialized = JSON.stringify(parsed); + + const migrationFn = jest.fn((oldVersion, newVersion, data) => { + expect(oldVersion).toBe(0); + return data; + }); + + const newScene = new Scene({ name: 'NewScene' }); + SceneSerializer.deserialize(newScene, modifiedSerialized, { + componentRegistry, + migration: migrationFn + }); + + expect(migrationFn).toHaveBeenCalled(); + + newScene.end(); + }); + + test('should throw on invalid JSON', () => { + const newScene = new Scene({ name: 'NewScene' }); + + expect(() => { + SceneSerializer.deserialize(newScene, 'invalid json{{{', { componentRegistry }); + }).toThrow(); + + newScene.end(); + }); + }); + + describe('validate', () => { + test('should validate correct save data', () => { + scene.createEntity('Entity'); + const serialized = SceneSerializer.serialize(scene); + + const result = SceneSerializer.validate(serialized as string); + + expect(result.valid).toBe(true); + expect(result.version).toBe(1); + }); + + test('should return errors for missing version', () => { + const invalid = JSON.stringify({ entities: [], componentTypeRegistry: [] }); + + const result = SceneSerializer.validate(invalid); + + expect(result.valid).toBe(false); + expect(result.errors).toContain('Missing version field'); + }); + + test('should return errors for missing entities', () => { + const invalid = JSON.stringify({ version: 1, componentTypeRegistry: [] }); + + const result = SceneSerializer.validate(invalid); + + expect(result.valid).toBe(false); + expect(result.errors).toContain('Missing or invalid entities field'); + }); + + test('should return errors for missing componentTypeRegistry', () => { + const invalid = JSON.stringify({ version: 1, entities: [] }); + + const result = SceneSerializer.validate(invalid); + + expect(result.valid).toBe(false); + expect(result.errors).toContain('Missing or invalid componentTypeRegistry field'); + }); + + test('should handle JSON parse errors', () => { + const result = SceneSerializer.validate('not valid json'); + + expect(result.valid).toBe(false); + expect(result.errors).toBeDefined(); + expect(result.errors![0]).toContain('JSON parse error'); + }); + }); + + describe('getInfo', () => { + test('should return info from save data', () => { + scene.name = 'InfoTestScene'; + scene.createEntity('Entity1'); + scene.createEntity('Entity2'); + scene.createEntity('Entity3'); + + const serialized = SceneSerializer.serialize(scene); + const info = SceneSerializer.getInfo(serialized as string); + + expect(info).not.toBeNull(); + expect(info!.name).toBe('InfoTestScene'); + expect(info!.entityCount).toBe(3); + expect(info!.version).toBe(1); + }); + + test('should return null for invalid data', () => { + const info = SceneSerializer.getInfo('invalid'); + + expect(info).toBeNull(); + }); + + test('should include timestamp when present', () => { + scene.createEntity('Entity'); + const serialized = SceneSerializer.serialize(scene, { includeMetadata: true }); + const info = SceneSerializer.getInfo(serialized as string); + + expect(info!.timestamp).toBeDefined(); + }); + }); + + describe('scene data serialization', () => { + test('should serialize Date objects', () => { + const date = new Date('2024-01-01T00:00:00Z'); + scene.sceneData.set('createdAt', date); + + const serialized = SceneSerializer.serialize(scene); + const parsed = JSON.parse(serialized as string); + + expect(parsed.sceneData.createdAt.__type).toBe('Date'); + }); + + test('should deserialize Date objects', () => { + const date = new Date('2024-01-01T00:00:00Z'); + scene.sceneData.set('createdAt', date); + + const serialized = SceneSerializer.serialize(scene); + + const newScene = new Scene({ name: 'NewScene' }); + SceneSerializer.deserialize(newScene, serialized, { componentRegistry }); + + const restoredDate = newScene.sceneData.get('createdAt'); + expect(restoredDate).toBeInstanceOf(Date); + expect(restoredDate.getTime()).toBe(date.getTime()); + + newScene.end(); + }); + + test('should serialize Map objects', () => { + const map = new Map([['key1', 'value1'], ['key2', 'value2']]); + scene.sceneData.set('mapping', map); + + const serialized = SceneSerializer.serialize(scene); + const parsed = JSON.parse(serialized as string); + + expect(parsed.sceneData.mapping.__type).toBe('Map'); + }); + + test('should deserialize Map objects', () => { + const map = new Map([['key1', 'value1'], ['key2', 'value2']]); + scene.sceneData.set('mapping', map); + + const serialized = SceneSerializer.serialize(scene); + + const newScene = new Scene({ name: 'NewScene' }); + SceneSerializer.deserialize(newScene, serialized, { componentRegistry }); + + const restoredMap = newScene.sceneData.get('mapping'); + expect(restoredMap).toBeInstanceOf(Map); + expect(restoredMap.get('key1')).toBe('value1'); + expect(restoredMap.get('key2')).toBe('value2'); + + newScene.end(); + }); + + test('should serialize Set objects', () => { + const set = new Set([1, 2, 3]); + scene.sceneData.set('numbers', set); + + const serialized = SceneSerializer.serialize(scene); + const parsed = JSON.parse(serialized as string); + + expect(parsed.sceneData.numbers.__type).toBe('Set'); + }); + + test('should deserialize Set objects', () => { + const set = new Set([1, 2, 3]); + scene.sceneData.set('numbers', set); + + const serialized = SceneSerializer.serialize(scene); + + const newScene = new Scene({ name: 'NewScene' }); + SceneSerializer.deserialize(newScene, serialized, { componentRegistry }); + + const restoredSet = newScene.sceneData.get('numbers'); + expect(restoredSet).toBeInstanceOf(Set); + expect(restoredSet.has(1)).toBe(true); + expect(restoredSet.has(2)).toBe(true); + expect(restoredSet.has(3)).toBe(true); + + newScene.end(); + }); + }); + + describe('hierarchy serialization', () => { + test('should serialize and deserialize entity hierarchy', () => { + const hierarchySystem = new HierarchySystem(); + scene.addSystem(hierarchySystem); + + const root = scene.createEntity('Root'); + const child1 = scene.createEntity('Child1'); + const child2 = scene.createEntity('Child2'); + + hierarchySystem.setParent(child1, root); + hierarchySystem.setParent(child2, root); + + const serialized = SceneSerializer.serialize(scene); + + const newScene = new Scene({ name: 'NewScene' }); + const newHierarchySystem = new HierarchySystem(); + newScene.addSystem(newHierarchySystem); + + SceneSerializer.deserialize(newScene, serialized, { componentRegistry }); + + const newRoot = newScene.findEntity('Root'); + expect(newRoot).not.toBeNull(); + + const children = newHierarchySystem.getChildren(newRoot!); + expect(children.length).toBe(2); + + newScene.end(); + }); + }); +}); diff --git a/packages/core/tests/ECS/Serialization/Serialization.test.ts b/packages/core/tests/ECS/Serialization/Serialization.test.ts deleted file mode 100644 index acedd0ac..00000000 --- a/packages/core/tests/ECS/Serialization/Serialization.test.ts +++ /dev/null @@ -1,629 +0,0 @@ -/** - * 序列化系统测试 - */ - -import { Component } from '../../../src/ECS/Component'; -import { Scene } from '../../../src/ECS/Scene'; -import { Entity } from '../../../src/ECS/Entity'; -import { - Serializable, - Serialize, - SerializeAsMap, - SerializeAsSet, - IgnoreSerialization, - ComponentSerializer, - EntitySerializer, - SceneSerializer, - VersionMigrationManager, - MigrationBuilder -} from '../../../src/ECS/Serialization'; -import { ECSComponent } from '../../../src/ECS/Decorators'; -import { ComponentRegistry } from '../../../src/ECS/Core/ComponentStorage'; - -// 测试组件定义 -@ECSComponent('Position') -@Serializable({ version: 1 }) -class PositionComponent extends Component { - @Serialize() - public x: number = 0; - - @Serialize() - public y: number = 0; - - constructor(x: number = 0, y: number = 0) { - super(); - this.x = x; - this.y = y; - } -} - -@ECSComponent('Velocity') -@Serializable({ version: 1 }) -class VelocityComponent extends Component { - @Serialize() - public dx: number = 0; - - @Serialize() - public dy: number = 0; -} - -@ECSComponent('Player') -@Serializable({ version: 1 }) -class PlayerComponent extends Component { - @Serialize() - public name: string = ''; - - @Serialize() - public level: number = 1; - - @SerializeAsMap() - public inventory: Map = new Map(); - - @SerializeAsSet() - public tags: Set = new Set(); - - @IgnoreSerialization() - public tempCache: any = null; -} - -@ECSComponent('Health') -@Serializable({ version: 1 }) -class HealthComponent extends Component { - @Serialize() - public current: number = 100; - - @Serialize() - public max: number = 100; -} - -// 非可序列化组件 -class NonSerializableComponent extends Component { - public data: any = null; -} - -describe('ECS Serialization System', () => { - let scene: Scene; - - beforeEach(() => { - // 清空测试环境 - ComponentRegistry.reset(); - - // 重新注册测试组件(因为reset会清空所有注册) - ComponentRegistry.register(PositionComponent); - ComponentRegistry.register(VelocityComponent); - ComponentRegistry.register(PlayerComponent); - ComponentRegistry.register(HealthComponent); - - // 创建测试场景 - scene = new Scene(); - }); - - describe('Component Serialization', () => { - it('should serialize a simple component', () => { - const position = new PositionComponent(100, 200); - const serialized = ComponentSerializer.serialize(position); - - expect(serialized).not.toBeNull(); - expect(serialized!.type).toBe('Position'); - expect(serialized!.version).toBe(1); - expect(serialized!.data.x).toBe(100); - expect(serialized!.data.y).toBe(200); - }); - - it('should deserialize a simple component', () => { - const serializedData = { - type: 'Position', - version: 1, - data: { x: 150, y: 250 } - }; - - const registry = ComponentRegistry.getAllComponentNames() as Map; - const component = ComponentSerializer.deserialize(serializedData, registry); - - expect(component).not.toBeNull(); - expect(component).toBeInstanceOf(PositionComponent); - expect((component as PositionComponent).x).toBe(150); - expect((component as PositionComponent).y).toBe(250); - }); - - it('should serialize Map fields', () => { - const player = new PlayerComponent(); - player.name = 'Hero'; - player.level = 5; - player.inventory.set('sword', 1); - player.inventory.set('potion', 10); - - const serialized = ComponentSerializer.serialize(player); - - expect(serialized).not.toBeNull(); - expect(serialized!.data.inventory).toEqual([ - ['sword', 1], - ['potion', 10] - ]); - }); - - it('should deserialize Map fields', () => { - const serializedData = { - type: 'Player', - version: 1, - data: { - name: 'Hero', - level: 5, - inventory: [ - ['sword', 1], - ['potion', 10] - ], - tags: ['warrior', 'hero'] - } - }; - - const registry = ComponentRegistry.getAllComponentNames() as Map; - const component = ComponentSerializer.deserialize( - serializedData, - registry - ) as PlayerComponent; - - expect(component).not.toBeNull(); - expect(component.inventory.get('sword')).toBe(1); - expect(component.inventory.get('potion')).toBe(10); - expect(component.tags.has('warrior')).toBe(true); - expect(component.tags.has('hero')).toBe(true); - }); - - it('should ignore fields marked with @IgnoreSerialization', () => { - const player = new PlayerComponent(); - player.tempCache = { foo: 'bar' }; - - const serialized = ComponentSerializer.serialize(player); - - expect(serialized).not.toBeNull(); - expect(serialized!.data.tempCache).toBeUndefined(); - }); - - it('should return null for non-serializable components', () => { - const nonSerializable = new NonSerializableComponent(); - const serialized = ComponentSerializer.serialize(nonSerializable); - - expect(serialized).toBeNull(); - }); - }); - - describe('Entity Serialization', () => { - it('should serialize an entity with components', () => { - const entity = scene.createEntity('Player'); - entity.addComponent(new PositionComponent(50, 100)); - entity.addComponent(new VelocityComponent()); - entity.tag = 10; - - const serialized = EntitySerializer.serialize(entity); - - expect(serialized.id).toBe(entity.id); - expect(serialized.name).toBe('Player'); - expect(serialized.tag).toBe(10); - expect(serialized.components.length).toBe(2); - }); - - it('should serialize entity hierarchy', () => { - const parent = scene.createEntity('Parent'); - const child = scene.createEntity('Child'); - - parent.addComponent(new PositionComponent(0, 0)); - child.addComponent(new PositionComponent(10, 10)); - parent.addChild(child); - - const serialized = EntitySerializer.serialize(parent); - - expect(serialized.children.length).toBe(1); - expect(serialized.children[0].id).toBe(child.id); - expect(serialized.children[0].name).toBe('Child'); - }); - - it('should deserialize an entity', () => { - const serializedEntity = { - id: 1, - name: 'TestEntity', - tag: 5, - active: true, - enabled: true, - updateOrder: 0, - components: [ - { - type: 'Position', - version: 1, - data: { x: 100, y: 200 } - } - ], - children: [] - }; - - const registry = ComponentRegistry.getAllComponentNames() as Map; - let idCounter = 10; - const entity = EntitySerializer.deserialize( - serializedEntity, - registry, - () => idCounter++, - false, - scene - ); - - expect(entity.name).toBe('TestEntity'); - expect(entity.tag).toBe(5); - expect(entity.components.length).toBe(1); - }); - }); - - describe('Scene Serialization', () => { - let scene: Scene; - - beforeEach(() => { - scene = new Scene({ name: 'TestScene' }); - }); - - afterEach(() => { - scene.end(); - }); - - it('should serialize a scene', () => { - const entity1 = scene.createEntity('Entity1'); - entity1.addComponent(new PositionComponent(10, 20)); - - const entity2 = scene.createEntity('Entity2'); - entity2.addComponent(new PlayerComponent()); - - const saveData = scene.serialize({ format: 'json', pretty: true }); - - expect(saveData).toBeTruthy(); - expect(typeof saveData).toBe('string'); - - const parsed = JSON.parse(saveData as string); - expect(parsed.name).toBe('TestScene'); - expect(parsed.version).toBe(1); - expect(parsed.entities.length).toBe(2); - }); - - it('should deserialize a scene with replace strategy', () => { - // 创建初始实体 - const entity1 = scene.createEntity('Initial'); - entity1.addComponent(new PositionComponent(0, 0)); - - // 序列化 - const entity2 = scene.createEntity('ToSave'); - entity2.addComponent(new PositionComponent(100, 100)); - const saveData = scene.serialize(); - - // 清空并重新加载 - scene.deserialize(saveData, { - strategy: 'replace', - // componentRegistry会自动从ComponentRegistry获取 - }); - - expect(scene.entities.count).toBeGreaterThan(0); - }); - - it('should filter components during serialization', () => { - const entity = scene.createEntity('Mixed'); - entity.addComponent(new PositionComponent(1, 2)); - entity.addComponent(new PlayerComponent()); - entity.addComponent(new HealthComponent()); - - const saveData = scene.serialize({ - components: [PositionComponent, PlayerComponent], - format: 'json' - }); - - const parsed = JSON.parse(saveData as string); - expect(parsed.entities.length).toBeGreaterThan(0); - }); - - it('should preserve entity hierarchy', () => { - const parent = scene.createEntity('Parent'); - const child = scene.createEntity('Child'); - parent.addChild(child); - - parent.addComponent(new PositionComponent(0, 0)); - child.addComponent(new PositionComponent(10, 10)); - - const saveData = scene.serialize({ format: 'json' }); - const parsed = JSON.parse(saveData as string); - - // 只有父实体在顶层 - expect(parsed.entities.length).toBe(1); - expect(parsed.entities[0].children.length).toBe(1); - }); - - it('should validate save data', () => { - const entity = scene.createEntity('Test'); - entity.addComponent(new PositionComponent(5, 5)); - - const saveData = scene.serialize({ format: 'json' }); - const validation = SceneSerializer.validate(saveData as string); - - expect(validation.valid).toBe(true); - expect(validation.version).toBe(1); - }); - - it('should get save data info', () => { - const entity = scene.createEntity('InfoTest'); - entity.addComponent(new PositionComponent(1, 1)); - - const saveData = scene.serialize({ format: 'json' }); - const info = SceneSerializer.getInfo(saveData as string); - - expect(info).not.toBeNull(); - expect(info!.name).toBe('TestScene'); - expect(info!.version).toBe(1); - }); - }); - - describe('Version Migration', () => { - @ECSComponent('OldPlayer') - @Serializable({ version: 1 }) - class OldPlayerV1 extends Component { - @Serialize() - public name: string = ''; - - @Serialize() - public hp: number = 100; - } - - @ECSComponent('OldPlayer') - @Serializable({ version: 2 }) - class OldPlayerV2 extends Component { - @Serialize() - public name: string = ''; - - @Serialize() - public health: number = 100; // 重命名了字段 - - @Serialize() - public maxHealth: number = 100; // 新增字段 - } - - beforeEach(() => { - VersionMigrationManager.clearMigrations(); - }); - - it('should migrate component from v1 to v2', () => { - // 注册迁移 - VersionMigrationManager.registerComponentMigration( - 'OldPlayer', - 1, - 2, - (data) => { - return { - name: data.name, - health: data.hp, - maxHealth: data.hp - }; - } - ); - - const v1Data = { - type: 'OldPlayer', - version: 1, - data: { name: 'Hero', hp: 80 } - }; - - const migrated = VersionMigrationManager.migrateComponent(v1Data, 2); - - expect(migrated.version).toBe(2); - expect(migrated.data.health).toBe(80); - expect(migrated.data.maxHealth).toBe(80); - expect(migrated.data.hp).toBeUndefined(); - }); - - it('should use MigrationBuilder for component migration', () => { - new MigrationBuilder() - .forComponent('Player') - .fromVersionToVersion(1, 2) - .migrate((data: any) => { - data.experience = 0; - return data; - }); - - expect(VersionMigrationManager.canMigrateComponent('Player', 1, 2)).toBe(true); - }); - - it('should check migration path availability', () => { - VersionMigrationManager.registerComponentMigration('Test', 1, 2, (d) => d); - VersionMigrationManager.registerComponentMigration('Test', 2, 3, (d) => d); - - expect(VersionMigrationManager.canMigrateComponent('Test', 1, 3)).toBe(true); - expect(VersionMigrationManager.canMigrateComponent('Test', 1, 4)).toBe(false); - }); - - it('should get migration path', () => { - VersionMigrationManager.registerComponentMigration('PathTest', 1, 2, (d) => d); - VersionMigrationManager.registerComponentMigration('PathTest', 2, 3, (d) => d); - - const path = VersionMigrationManager.getComponentMigrationPath('PathTest'); - - expect(path).toEqual([1, 2]); - }); - }); - - // ComponentTypeRegistry已被移除,现在使用ComponentRegistry自动管理组件类型 - - describe('Integration Tests', () => { - it('should perform full save/load cycle', () => { - const scene1 = new Scene({ name: 'SaveTest' }); - - // 创建复杂实体 - const player = scene1.createEntity('Player'); - const playerComp = new PlayerComponent(); - playerComp.name = 'TestHero'; - playerComp.level = 10; - playerComp.inventory.set('sword', 1); - playerComp.inventory.set('shield', 1); - playerComp.tags.add('warrior'); - - player.addComponent(playerComp); - player.addComponent(new PositionComponent(100, 200)); - player.addComponent(new HealthComponent()); - - // 创建子实体 - const weapon = scene1.createEntity('Weapon'); - weapon.addComponent(new PositionComponent(5, 0)); - player.addChild(weapon); - - // 序列化 - const saveData = scene1.serialize(); - - // 新场景 - const scene2 = new Scene({ name: 'LoadTest' }); - - // 反序列化 - scene2.deserialize(saveData, { - strategy: 'replace', - // componentRegistry会自动从ComponentRegistry获取 - }); - - // 验证 - const loadedPlayer = scene2.findEntity('Player'); - expect(loadedPlayer).not.toBeNull(); - - const loadedPlayerComp = loadedPlayer!.getComponent(PlayerComponent as any) as PlayerComponent; - expect(loadedPlayerComp).not.toBeNull(); - expect(loadedPlayerComp.name).toBe('TestHero'); - expect(loadedPlayerComp.level).toBe(10); - expect(loadedPlayerComp.inventory.get('sword')).toBe(1); - expect(loadedPlayerComp.tags.has('warrior')).toBe(true); - - // 验证层级结构 - expect(loadedPlayer!.childCount).toBe(1); - - scene1.end(); - scene2.end(); - }); - - it('should serialize and deserialize scene custom data', () => { - const scene1 = new Scene({ name: 'SceneDataTest' }); - - // 设置场景自定义数据 - scene1.sceneData.set('weather', 'rainy'); - scene1.sceneData.set('timeOfDay', 14.5); - scene1.sceneData.set('difficulty', 'hard'); - scene1.sceneData.set('checkpoint', { x: 100, y: 200 }); - scene1.sceneData.set('tags', new Set(['action', 'adventure'])); - scene1.sceneData.set('metadata', new Map([['author', 'test'], ['version', '1.0']])); - - // 序列化 - const saveData = scene1.serialize(); - - // 新场景 - const scene2 = new Scene({ name: 'LoadTest' }); - - // 反序列化 - scene2.deserialize(saveData, { - strategy: 'replace', - // componentRegistry会自动从ComponentRegistry获取 - }); - - // 验证场景数据 - expect(scene2.sceneData.get('weather')).toBe('rainy'); - expect(scene2.sceneData.get('timeOfDay')).toBe(14.5); - expect(scene2.sceneData.get('difficulty')).toBe('hard'); - expect(scene2.sceneData.get('checkpoint')).toEqual({ x: 100, y: 200 }); - - const tags = scene2.sceneData.get('tags'); - expect(tags).toBeInstanceOf(Set); - expect(tags.has('action')).toBe(true); - expect(tags.has('adventure')).toBe(true); - - const metadata = scene2.sceneData.get('metadata'); - expect(metadata).toBeInstanceOf(Map); - expect(metadata.get('author')).toBe('test'); - expect(metadata.get('version')).toBe('1.0'); - - scene1.end(); - scene2.end(); - }); - - it('should serialize and deserialize using binary format', () => { - const scene1 = new Scene({ name: 'BinaryTest' }); - - // 创建测试数据 - const player = scene1.createEntity('Player'); - const playerComp = new PlayerComponent(); - playerComp.name = 'BinaryHero'; - playerComp.level = 5; - playerComp.inventory.set('sword', 1); - player.addComponent(playerComp); - player.addComponent(new PositionComponent(100, 200)); - - scene1.sceneData.set('weather', 'sunny'); - scene1.sceneData.set('score', 9999); - - // 二进制序列化 - const binaryData = scene1.serialize({ format: 'binary' }); - - // 验证是Uint8Array类型 - expect(binaryData instanceof Uint8Array).toBe(true); - expect((binaryData as Uint8Array).length).toBeGreaterThan(0); - - // 新场景反序列化二进制数据 - const scene2 = new Scene({ name: 'LoadTest' }); - scene2.deserialize(binaryData, { - strategy: 'replace', - // componentRegistry会自动从ComponentRegistry获取 - }); - - // 验证数据完整性 - const loadedPlayer = scene2.findEntity('Player'); - expect(loadedPlayer).not.toBeNull(); - - const loadedPlayerComp = loadedPlayer!.getComponent(PlayerComponent as any) as PlayerComponent; - expect(loadedPlayerComp.name).toBe('BinaryHero'); - expect(loadedPlayerComp.level).toBe(5); - expect(loadedPlayerComp.inventory.get('sword')).toBe(1); - - const loadedPos = loadedPlayer!.getComponent(PositionComponent as any) as PositionComponent; - expect(loadedPos.x).toBe(100); - expect(loadedPos.y).toBe(200); - - expect(scene2.sceneData.get('weather')).toBe('sunny'); - expect(scene2.sceneData.get('score')).toBe(9999); - - scene1.end(); - scene2.end(); - }); - - it('should handle complex nested data in binary format', () => { - const scene1 = new Scene({ name: 'NestedBinaryTest' }); - - // 复杂嵌套数据 - scene1.sceneData.set('config', { - graphics: { - quality: 'high', - resolution: { width: 1920, height: 1080 } - }, - audio: { - masterVolume: 0.8, - effects: new Map([['music', 0.7], ['sfx', 0.9]]) - }, - tags: new Set(['multiplayer', 'ranked']), - timestamp: new Date('2024-01-01') - }); - - // 二进制序列化 - const binaryData = scene1.serialize({ format: 'binary' }); - - // 反序列化 - const scene2 = new Scene({ name: 'LoadTest' }); - scene2.deserialize(binaryData, { - // componentRegistry会自动从ComponentRegistry获取 - }); - - const config = scene2.sceneData.get('config'); - expect(config.graphics.quality).toBe('high'); - expect(config.graphics.resolution.width).toBe(1920); - expect(config.audio.masterVolume).toBe(0.8); - expect(config.audio.effects.get('music')).toBe(0.7); - expect(config.tags.has('multiplayer')).toBe(true); - expect(config.timestamp).toBeInstanceOf(Date); - - scene1.end(); - scene2.end(); - }); - }); -}); diff --git a/packages/core/tests/SceneQuery.test.ts b/packages/core/tests/SceneQuery.test.ts deleted file mode 100644 index 5f2ff952..00000000 --- a/packages/core/tests/SceneQuery.test.ts +++ /dev/null @@ -1,145 +0,0 @@ -/** - * Scene查询方法测试 - */ - -import { Component } from '../src/ECS/Component'; -import { Entity } from '../src/ECS/Entity'; -import { Scene } from '../src/ECS/Scene'; -import { Core } from '../src/Core'; -import { ECSComponent } from '../src/ECS/Decorators'; -import { EntitySystem } from '../src/ECS/Systems/EntitySystem'; - -@ECSComponent('Position') -class Position extends Component { - constructor(public x: number = 0, public y: number = 0) { - super(); - } -} - -@ECSComponent('Velocity') -class Velocity extends Component { - constructor(public dx: number = 0, public dy: number = 0) { - super(); - } -} - -@ECSComponent('Disabled') -class Disabled extends Component {} - -describe('Scene查询方法', () => { - let scene: Scene; - - beforeEach(() => { - Core.create({ debug: false, enableEntitySystems: true }); - scene = new Scene(); - scene.initialize(); - }); - - afterEach(() => { - scene.end(); - }); - - describe('基础查询方法', () => { - test('queryAll 查询拥有所有组件的实体', () => { - const e1 = scene.createEntity('E1'); - e1.addComponent(new Position(10, 20)); - e1.addComponent(new Velocity(1, 2)); - - const e2 = scene.createEntity('E2'); - e2.addComponent(new Position(30, 40)); - - const result = scene.queryAll(Position, Velocity); - - expect(result.entities).toHaveLength(1); - expect(result.entities[0]).toBe(e1); - }); - - test('queryAny 查询拥有任意组件的实体', () => { - const e1 = scene.createEntity('E1'); - e1.addComponent(new Position(10, 20)); - - const e2 = scene.createEntity('E2'); - e2.addComponent(new Velocity(1, 2)); - - const e3 = scene.createEntity('E3'); - e3.addComponent(new Disabled()); - - const result = scene.queryAny(Position, Velocity); - - expect(result.entities).toHaveLength(2); - }); - - test('queryNone 查询不包含指定组件的实体', () => { - const e1 = scene.createEntity('E1'); - e1.addComponent(new Position(10, 20)); - - const e2 = scene.createEntity('E2'); - e2.addComponent(new Position(30, 40)); - e2.addComponent(new Disabled()); - - const result = scene.queryNone(Disabled); - - expect(result.entities).toHaveLength(1); - expect(result.entities[0]).toBe(e1); - }); - }); - - describe('TypedQueryBuilder', () => { - test('scene.query() 创建类型安全的查询构建器', () => { - const e1 = scene.createEntity('E1'); - e1.addComponent(new Position(10, 20)); - e1.addComponent(new Velocity(1, 2)); - - const e2 = scene.createEntity('E2'); - e2.addComponent(new Position(30, 40)); - e2.addComponent(new Velocity(3, 4)); - e2.addComponent(new Disabled()); - - // 构建查询 - const query = scene.query() - .withAll(Position, Velocity) - .withNone(Disabled); - - const matcher = query.buildMatcher(); - - // 创建System使用这个matcher - class TestSystem extends EntitySystem { - public processedCount = 0; - - constructor() { - super(matcher); - } - - protected override process(entities: readonly Entity[]): void { - this.processedCount = entities.length; - } - } - - const system = new TestSystem(); - scene.addSystem(system); - scene.update(); - - // 应该只处理e1(e2被Disabled排除) - expect(system.processedCount).toBe(1); - }); - - test('TypedQueryBuilder 支持复杂查询', () => { - const e1 = scene.createEntity('E1'); - e1.addComponent(new Position(10, 20)); - e1.tag = 100; - - const e2 = scene.createEntity('E2'); - e2.addComponent(new Position(30, 40)); - e2.tag = 200; - - const query = scene.query() - .withAll(Position) - .withTag(100); - - const condition = query.getCondition(); - - expect(condition.all).toContain(Position as any); - expect(condition.tag).toBe(100); - }); - }); -}); diff --git a/packages/core/tests/TypeInference.test.ts b/packages/core/tests/TypeInference.test.ts deleted file mode 100644 index 7682d682..00000000 --- a/packages/core/tests/TypeInference.test.ts +++ /dev/null @@ -1,205 +0,0 @@ -/** - * TypeScript类型推断测试 - * - * 验证组件类型自动推断功能 - */ - -import { Component } from '../src/ECS/Component'; -import { Entity } from '../src/ECS/Entity'; -import { Scene } from '../src/ECS/Scene'; -import { Core } from '../src/Core'; -import { ECSComponent } from '../src/ECS/Decorators'; -import { requireComponent, tryGetComponent, getComponents } from '../src/ECS/TypedEntity'; - -// 测试组件 -@ECSComponent('Position') -class Position extends Component { - constructor(public x: number = 0, public y: number = 0) { - super(); - } -} - -@ECSComponent('Velocity') -class Velocity extends Component { - constructor(public dx: number = 0, public dy: number = 0) { - super(); - } -} - -@ECSComponent('Health') -class Health extends Component { - constructor(public value: number = 100, public maxValue: number = 100) { - super(); - } -} - -describe('TypeScript类型推断', () => { - let scene: Scene; - let entity: Entity; - - beforeEach(() => { - Core.create({ debug: false, enableEntitySystems: true }); - scene = new Scene(); - scene.initialize(); - entity = scene.createEntity('TestEntity'); - }); - - afterEach(() => { - scene.end(); - }); - - describe('Entity.getComponent 类型推断', () => { - test('getComponent 应该自动推断正确的返回类型', () => { - entity.addComponent(new Position(100, 200)); - - // 类型推断为 Position | null - const position = entity.getComponent(Position); - - // TypeScript应该知道position可能为null - expect(position).not.toBeNull(); - - // 在null检查后,TypeScript应该知道position是Position类型 - if (position) { - expect(position.x).toBe(100); - expect(position.y).toBe(200); - - // 这些操作应该有完整的类型提示 - position.x += 10; - position.y += 20; - - expect(position.x).toBe(110); - expect(position.y).toBe(220); - } - }); - - test('getComponent 返回null时类型安全', () => { - // 实体没有Velocity组件 - const velocity = entity.getComponent(Velocity); - - // 应该返回null - expect(velocity).toBeNull(); - }); - - test('多个不同类型组件的类型推断', () => { - entity.addComponent(new Position(10, 20)); - entity.addComponent(new Velocity(1, 2)); - entity.addComponent(new Health(100)); - - const pos = entity.getComponent(Position); - const vel = entity.getComponent(Velocity); - const health = entity.getComponent(Health); - - // 所有组件都应该被正确推断 - if (pos && vel && health) { - // Position类型的字段 - pos.x = 50; - pos.y = 60; - - // Velocity类型的字段 - vel.dx = 5; - vel.dy = 10; - - // Health类型的字段 - health.value = 80; - health.maxValue = 150; - - expect(pos.x).toBe(50); - expect(vel.dx).toBe(5); - expect(health.value).toBe(80); - } - }); - }); - - describe('Entity.createComponent 类型推断', () => { - test('createComponent 应该自动推断返回类型', () => { - // 应该推断为Position类型(非null) - const position = entity.createComponent(Position, 100, 200); - - expect(position).toBeInstanceOf(Position); - expect(position.x).toBe(100); - expect(position.y).toBe(200); - - // 应该有完整的类型提示 - position.x = 300; - expect(position.x).toBe(300); - }); - }); - - describe('Entity.hasComponent 类型守卫', () => { - test('hasComponent 可以用作类型守卫', () => { - entity.addComponent(new Position(10, 20)); - - if (entity.hasComponent(Position)) { - // 在这个作用域内,我们知道组件存在 - const pos = entity.getComponent(Position)!; - pos.x = 100; - expect(pos.x).toBe(100); - } - }); - }); - - describe('Entity.getOrCreateComponent 类型推断', () => { - test('getOrCreateComponent 应该自动推断返回类型', () => { - // 第一次调用:创建新组件 - const position1 = entity.getOrCreateComponent(Position, 50, 60); - expect(position1.x).toBe(50); - expect(position1.y).toBe(60); - - // 第二次调用:返回已存在的组件 - const position2 = entity.getOrCreateComponent(Position, 100, 200); - - // 应该是同一个组件 - expect(position2).toBe(position1); - expect(position2.x).toBe(50); // 值未改变 - }); - }); - - describe('TypedEntity工具函数类型推断', () => { - test('requireComponent 返回非空类型', () => { - entity.addComponent(new Position(100, 200)); - - // requireComponent 返回非null类型 - const position = requireComponent(entity, Position); - - // 不需要null检查 - expect(position.x).toBe(100); - position.x = 300; - expect(position.x).toBe(300); - }); - - test('tryGetComponent 返回可选类型', () => { - entity.addComponent(new Position(50, 50)); - - const position = tryGetComponent(entity, Position); - - // 应该返回组件 - expect(position).toBeDefined(); - if (position) { - expect(position.x).toBe(50); - } - - // 不存在的组件返回undefined - const velocity = tryGetComponent(entity, Velocity); - expect(velocity).toBeUndefined(); - }); - - test('getComponents 批量获取组件', () => { - entity.addComponent(new Position(10, 20)); - entity.addComponent(new Velocity(1, 2)); - entity.addComponent(new Health(100)); - - const [pos, vel, health] = getComponents(entity, Position, Velocity, Health); - - // 应该推断为数组类型 - expect(pos).not.toBeNull(); - expect(vel).not.toBeNull(); - expect(health).not.toBeNull(); - - if (pos && vel && health) { - expect(pos.x).toBe(10); - expect(vel.dx).toBe(1); - expect(health.value).toBe(100); - } - }); - }); -}); diff --git a/packages/core/tests/Utils/Debug/EntityDataCollector.test.ts b/packages/core/tests/Utils/Debug/EntityDataCollector.test.ts new file mode 100644 index 00000000..32f63661 --- /dev/null +++ b/packages/core/tests/Utils/Debug/EntityDataCollector.test.ts @@ -0,0 +1,410 @@ +import { EntityDataCollector } from '../../../src/Utils/Debug/EntityDataCollector'; +import { Scene } from '../../../src/ECS/Scene'; +import { Component } from '../../../src/ECS/Component'; +import { HierarchySystem } from '../../../src/ECS/Systems/HierarchySystem'; +import { HierarchyComponent } from '../../../src/ECS/Components/HierarchyComponent'; +import { ECSComponent } from '../../../src/ECS/Decorators'; + +@ECSComponent('TestPosition') +class PositionComponent extends Component { + public x: number = 0; + public y: number = 0; + + constructor(x: number = 0, y: number = 0) { + super(); + this.x = x; + this.y = y; + } +} + +@ECSComponent('TestVelocity') +class VelocityComponent extends Component { + public vx: number = 0; + public vy: number = 0; +} + +describe('EntityDataCollector', () => { + let collector: EntityDataCollector; + let scene: Scene; + + beforeEach(() => { + collector = new EntityDataCollector(); + scene = new Scene({ name: 'TestScene' }); + }); + + afterEach(() => { + scene.end(); + }); + + describe('collectEntityData', () => { + test('should return empty data when scene is null', () => { + const data = collector.collectEntityData(null); + + expect(data.totalEntities).toBe(0); + expect(data.activeEntities).toBe(0); + expect(data.pendingAdd).toBe(0); + expect(data.pendingRemove).toBe(0); + expect(data.entitiesPerArchetype).toEqual([]); + expect(data.topEntitiesByComponents).toEqual([]); + }); + + test('should return empty data when scene is undefined', () => { + const data = collector.collectEntityData(undefined); + + expect(data.totalEntities).toBe(0); + expect(data.activeEntities).toBe(0); + }); + + test('should collect entity data from scene', () => { + const entity1 = scene.createEntity('Entity1'); + entity1.addComponent(new PositionComponent(10, 20)); + + const entity2 = scene.createEntity('Entity2'); + entity2.addComponent(new VelocityComponent()); + + const data = collector.collectEntityData(scene); + + expect(data.totalEntities).toBe(2); + expect(data.activeEntities).toBeGreaterThanOrEqual(0); + }); + + test('should collect archetype distribution', () => { + const entity1 = scene.createEntity('Entity1'); + entity1.addComponent(new PositionComponent()); + + const entity2 = scene.createEntity('Entity2'); + entity2.addComponent(new PositionComponent()); + + const entity3 = scene.createEntity('Entity3'); + entity3.addComponent(new VelocityComponent()); + + const data = collector.collectEntityData(scene); + + expect(data.entitiesPerArchetype.length).toBeGreaterThan(0); + }); + }); + + describe('getRawEntityList', () => { + test('should return empty array when scene is null', () => { + const list = collector.getRawEntityList(null); + expect(list).toEqual([]); + }); + + test('should return raw entity list from scene', () => { + const entity1 = scene.createEntity('Entity1'); + entity1.addComponent(new PositionComponent(10, 20)); + entity1.tag = 0x01; + + const entity2 = scene.createEntity('Entity2'); + entity2.addComponent(new VelocityComponent()); + + const list = collector.getRawEntityList(scene); + + expect(list.length).toBe(2); + expect(list[0].name).toBe('Entity1'); + expect(list[0].componentCount).toBe(1); + expect(list[0].tag).toBe(0x01); + }); + + test('should include hierarchy information', () => { + const hierarchySystem = new HierarchySystem(); + scene.addSystem(hierarchySystem); + + const parent = scene.createEntity('Parent'); + const child = scene.createEntity('Child'); + + hierarchySystem.setParent(child, parent); + + const list = collector.getRawEntityList(scene); + const childInfo = list.find(e => e.name === 'Child'); + + expect(childInfo).toBeDefined(); + expect(childInfo!.parentId).toBe(parent.id); + expect(childInfo!.depth).toBe(1); + }); + }); + + describe('getEntityDetails', () => { + test('should return null when scene is null', () => { + const details = collector.getEntityDetails(1, null); + expect(details).toBeNull(); + }); + + test('should return null when entity not found', () => { + const details = collector.getEntityDetails(9999, scene); + expect(details).toBeNull(); + }); + + test('should return entity details', () => { + const entity = scene.createEntity('TestEntity'); + entity.addComponent(new PositionComponent(100, 200)); + entity.tag = 42; + + const details = collector.getEntityDetails(entity.id, scene); + + expect(details).not.toBeNull(); + expect(details.componentCount).toBe(1); + expect(details.scene).toBeDefined(); + }); + + test('should handle errors gracefully', () => { + const details = collector.getEntityDetails(-1, scene); + expect(details).toBeNull(); + }); + }); + + describe('collectEntityDataWithMemory', () => { + test('should return empty data when scene is null', () => { + const data = collector.collectEntityDataWithMemory(null); + + expect(data.totalEntities).toBe(0); + expect(data.entityHierarchy).toEqual([]); + expect(data.entityDetailsMap).toEqual({}); + }); + + test('should collect entity data with memory information', () => { + const entity = scene.createEntity('Entity'); + entity.addComponent(new PositionComponent(10, 20)); + + const data = collector.collectEntityDataWithMemory(scene); + + expect(data.totalEntities).toBe(1); + expect(data.topEntitiesByComponents.length).toBeGreaterThan(0); + }); + + test('should include entity details map', () => { + const entity = scene.createEntity('Entity'); + entity.addComponent(new PositionComponent()); + + const data = collector.collectEntityDataWithMemory(scene); + + expect(data.entityDetailsMap).toBeDefined(); + expect(data.entityDetailsMap![entity.id]).toBeDefined(); + }); + + test('should build entity hierarchy tree', () => { + const hierarchySystem = new HierarchySystem(); + scene.addSystem(hierarchySystem); + + const root = scene.createEntity('Root'); + root.addComponent(new HierarchyComponent()); + + const child = scene.createEntity('Child'); + hierarchySystem.setParent(child, root); + + const data = collector.collectEntityDataWithMemory(scene); + + expect(data.entityHierarchy).toBeDefined(); + expect(data.entityHierarchy!.length).toBe(1); + expect(data.entityHierarchy![0].name).toBe('Root'); + }); + }); + + describe('estimateEntityMemoryUsage', () => { + test('should estimate memory for entity', () => { + const entity = scene.createEntity('Entity'); + entity.addComponent(new PositionComponent(10, 20)); + + const memory = collector.estimateEntityMemoryUsage(entity); + + expect(memory).toBeGreaterThanOrEqual(0); + expect(typeof memory).toBe('number'); + }); + + test('should return 0 for invalid entity', () => { + const memory = collector.estimateEntityMemoryUsage(null); + expect(memory).toBe(0); + }); + + test('should handle errors and return 0', () => { + const badEntity = { components: null }; + const memory = collector.estimateEntityMemoryUsage(badEntity); + expect(memory).toBeGreaterThanOrEqual(0); + }); + }); + + describe('calculateObjectSize', () => { + test('should return 0 for null/undefined', () => { + expect(collector.calculateObjectSize(null)).toBe(0); + expect(collector.calculateObjectSize(undefined)).toBe(0); + }); + + test('should calculate size for simple object', () => { + const obj = { x: 10, y: 20, name: 'test' }; + const size = collector.calculateObjectSize(obj); + + expect(size).toBeGreaterThan(0); + }); + + test('should respect exclude keys', () => { + const obj = { x: 10, excluded: 'large string'.repeat(100) }; + const sizeWithExclude = collector.calculateObjectSize(obj, ['excluded']); + const sizeWithoutExclude = collector.calculateObjectSize(obj); + + expect(sizeWithExclude).toBeLessThan(sizeWithoutExclude); + }); + + test('should handle nested objects with limited depth', () => { + const obj = { + level1: { + level2: { + level3: { + value: 42 + } + } + } + }; + + const size = collector.calculateObjectSize(obj); + expect(size).toBeGreaterThan(0); + }); + }); + + describe('extractComponentDetails', () => { + test('should extract component details', () => { + const component = new PositionComponent(100, 200); + const details = collector.extractComponentDetails([component]); + + expect(details.length).toBe(1); + expect(details[0].typeName).toBe('TestPosition'); + expect(details[0].properties.x).toBe(100); + expect(details[0].properties.y).toBe(200); + }); + + test('should handle empty components array', () => { + const details = collector.extractComponentDetails([]); + expect(details).toEqual([]); + }); + + test('should skip private properties', () => { + class ComponentWithPrivate extends Component { + public publicValue: number = 1; + private _privateValue: number = 2; + } + + const component = new ComponentWithPrivate(); + const details = collector.extractComponentDetails([component]); + + expect(details[0].properties.publicValue).toBe(1); + expect(details[0].properties._privateValue).toBeUndefined(); + }); + }); + + describe('getComponentProperties', () => { + test('should return empty object when scene is null', () => { + const props = collector.getComponentProperties(1, 0, null); + expect(props).toEqual({}); + }); + + test('should return empty object when entity not found', () => { + const props = collector.getComponentProperties(9999, 0, scene); + expect(props).toEqual({}); + }); + + test('should return empty object when component index is out of bounds', () => { + const entity = scene.createEntity('Entity'); + entity.addComponent(new PositionComponent()); + + const props = collector.getComponentProperties(entity.id, 99, scene); + expect(props).toEqual({}); + }); + + test('should return component properties', () => { + const entity = scene.createEntity('Entity'); + entity.addComponent(new PositionComponent(50, 75)); + + const props = collector.getComponentProperties(entity.id, 0, scene); + + expect(props.x).toBe(50); + expect(props.y).toBe(75); + }); + }); + + describe('expandLazyObject', () => { + test('should return null when scene is null', () => { + const result = collector.expandLazyObject(1, 0, 'path', null); + expect(result).toBeNull(); + }); + + test('should return null when entity not found', () => { + const result = collector.expandLazyObject(9999, 0, 'path', scene); + expect(result).toBeNull(); + }); + + test('should return null when component index is out of bounds', () => { + const entity = scene.createEntity('Entity'); + entity.addComponent(new PositionComponent()); + + const result = collector.expandLazyObject(entity.id, 99, '', scene); + expect(result).toBeNull(); + }); + + test('should expand object at path', () => { + class ComponentWithNested extends Component { + public nested = { value: 42 }; + } + + const entity = scene.createEntity('Entity'); + entity.addComponent(new ComponentWithNested()); + + const result = collector.expandLazyObject(entity.id, 0, 'nested', scene); + + expect(result).toBeDefined(); + expect(result.value).toBe(42); + }); + + test('should handle array index in path', () => { + class ComponentWithArray extends Component { + public items = [{ id: 1 }, { id: 2 }]; + } + + const entity = scene.createEntity('Entity'); + entity.addComponent(new ComponentWithArray()); + + const result = collector.expandLazyObject(entity.id, 0, 'items[1]', scene); + + expect(result).toBeDefined(); + expect(result.id).toBe(2); + }); + }); + + describe('edge cases', () => { + test('should handle scene without entities buffer', () => { + const mockScene = { + entities: null, + getSystem: () => null + }; + + const data = collector.collectEntityData(mockScene as any); + expect(data.totalEntities).toBe(0); + }); + + test('should handle entity with long string properties', () => { + class ComponentWithLongString extends Component { + public longText = 'x'.repeat(300); + } + + const entity = scene.createEntity('Entity'); + entity.addComponent(new ComponentWithLongString()); + + const details = collector.extractComponentDetails(entity.components); + + expect(details[0].properties.longText).toContain('[长字符串:'); + }); + + test('should handle entity with large arrays', () => { + class ComponentWithLargeArray extends Component { + public items = Array.from({ length: 20 }, (_, i) => i); + } + + const entity = scene.createEntity('Entity'); + entity.addComponent(new ComponentWithLargeArray()); + + const details = collector.extractComponentDetails(entity.components); + + expect(details[0].properties.items).toBeDefined(); + expect(details[0].properties.items._isLazyArray).toBe(true); + expect(details[0].properties.items._arrayLength).toBe(20); + }); + }); +}); diff --git a/packages/core/tests/Utils/Profiler/AutoProfiler.test.ts b/packages/core/tests/Utils/Profiler/AutoProfiler.test.ts new file mode 100644 index 00000000..a159be10 --- /dev/null +++ b/packages/core/tests/Utils/Profiler/AutoProfiler.test.ts @@ -0,0 +1,417 @@ +import { AutoProfiler, Profile, ProfileClass } from '../../../src/Utils/Profiler/AutoProfiler'; +import { ProfilerSDK } from '../../../src/Utils/Profiler/ProfilerSDK'; +import { ProfileCategory } from '../../../src/Utils/Profiler/ProfilerTypes'; + +describe('AutoProfiler', () => { + beforeEach(() => { + AutoProfiler.resetInstance(); + ProfilerSDK.reset(); + ProfilerSDK.setEnabled(true); + }); + + afterEach(() => { + AutoProfiler.resetInstance(); + ProfilerSDK.reset(); + }); + + describe('getInstance', () => { + test('should return singleton instance', () => { + const instance1 = AutoProfiler.getInstance(); + const instance2 = AutoProfiler.getInstance(); + expect(instance1).toBe(instance2); + }); + + test('should accept custom config', () => { + const instance = AutoProfiler.getInstance({ minDuration: 1.0 }); + expect(instance).toBeDefined(); + }); + }); + + describe('resetInstance', () => { + test('should reset the singleton instance', () => { + const instance1 = AutoProfiler.getInstance(); + AutoProfiler.resetInstance(); + const instance2 = AutoProfiler.getInstance(); + expect(instance1).not.toBe(instance2); + }); + }); + + describe('setEnabled', () => { + test('should enable/disable auto profiling', () => { + AutoProfiler.setEnabled(false); + const instance = AutoProfiler.getInstance(); + expect(instance).toBeDefined(); + + AutoProfiler.setEnabled(true); + expect(instance).toBeDefined(); + }); + }); + + describe('wrapFunction', () => { + test('should wrap a synchronous function', () => { + ProfilerSDK.beginFrame(); + + const originalFn = (a: number, b: number) => a + b; + const wrappedFn = AutoProfiler.wrapFunction(originalFn, 'add', ProfileCategory.Custom); + + const result = wrappedFn(2, 3); + expect(result).toBe(5); + + ProfilerSDK.endFrame(); + }); + + test('should preserve function behavior', () => { + const originalFn = (x: number) => x * 2; + const wrappedFn = AutoProfiler.wrapFunction(originalFn, 'double', ProfileCategory.Script); + + ProfilerSDK.beginFrame(); + expect(wrappedFn(5)).toBe(10); + expect(wrappedFn(0)).toBe(0); + expect(wrappedFn(-3)).toBe(-6); + ProfilerSDK.endFrame(); + }); + + test('should handle async functions', async () => { + const asyncFn = async (x: number) => { + await new Promise((resolve) => setTimeout(resolve, 1)); + return x * 2; + }; + + const wrappedFn = AutoProfiler.wrapFunction(asyncFn, 'asyncDouble', ProfileCategory.Script); + + ProfilerSDK.beginFrame(); + const result = await wrappedFn(5); + expect(result).toBe(10); + ProfilerSDK.endFrame(); + }); + + test('should handle function that throws error', () => { + const errorFn = () => { + throw new Error('Test error'); + }; + + const wrappedFn = AutoProfiler.wrapFunction(errorFn, 'errorFn', ProfileCategory.Script); + + ProfilerSDK.beginFrame(); + expect(() => wrappedFn()).toThrow('Test error'); + ProfilerSDK.endFrame(); + }); + + test('should return original function when disabled', () => { + AutoProfiler.setEnabled(false); + + const originalFn = (x: number) => x + 1; + const wrappedFn = AutoProfiler.wrapFunction(originalFn, 'increment', ProfileCategory.Script); + + expect(wrappedFn(5)).toBe(6); + }); + }); + + describe('wrapInstance', () => { + test('should wrap all methods of an object', () => { + class Calculator { + add(a: number, b: number): number { + return a + b; + } + subtract(a: number, b: number): number { + return a - b; + } + } + + const calc = new Calculator(); + AutoProfiler.wrapInstance(calc, 'Calculator', ProfileCategory.Script); + + ProfilerSDK.beginFrame(); + expect(calc.add(5, 3)).toBe(8); + expect(calc.subtract(5, 3)).toBe(2); + ProfilerSDK.endFrame(); + }); + + test('should not wrap already wrapped objects', () => { + class MyClass { + getValue(): number { + return 42; + } + } + + const obj = new MyClass(); + AutoProfiler.wrapInstance(obj, 'MyClass', ProfileCategory.Custom); + AutoProfiler.wrapInstance(obj, 'MyClass', ProfileCategory.Custom); + + ProfilerSDK.beginFrame(); + expect(obj.getValue()).toBe(42); + ProfilerSDK.endFrame(); + }); + + test('should return object unchanged when disabled', () => { + AutoProfiler.setEnabled(false); + + class MyClass { + getValue(): number { + return 42; + } + } + + const obj = new MyClass(); + const wrapped = AutoProfiler.wrapInstance(obj, 'MyClass', ProfileCategory.Custom); + + expect(wrapped).toBe(obj); + }); + + test('should exclude methods matching exclude patterns', () => { + class MyClass { + getValue(): number { + return 42; + } + _privateMethod(): number { + return 1; + } + getName(): string { + return 'test'; + } + isValid(): boolean { + return true; + } + hasData(): boolean { + return true; + } + } + + const obj = new MyClass(); + AutoProfiler.wrapInstance(obj, 'MyClass', ProfileCategory.Custom); + + ProfilerSDK.beginFrame(); + expect(obj.getValue()).toBe(42); + expect(obj._privateMethod()).toBe(1); + expect(obj.getName()).toBe('test'); + expect(obj.isValid()).toBe(true); + expect(obj.hasData()).toBe(true); + ProfilerSDK.endFrame(); + }); + }); + + describe('registerClass', () => { + test('should register a class for auto profiling', () => { + class MySystem { + update(): void { + // Do something + } + } + + const RegisteredClass = AutoProfiler.registerClass(MySystem, ProfileCategory.ECS); + + ProfilerSDK.beginFrame(); + const instance = new RegisteredClass(); + instance.update(); + ProfilerSDK.endFrame(); + + expect(instance).toBeInstanceOf(MySystem); + }); + + test('should accept custom class name', () => { + class MySystem { + process(): number { + return 1; + } + } + + const RegisteredClass = AutoProfiler.registerClass(MySystem, ProfileCategory.ECS, 'CustomSystem'); + + ProfilerSDK.beginFrame(); + const instance = new RegisteredClass(); + expect(instance.process()).toBe(1); + ProfilerSDK.endFrame(); + }); + }); + + describe('sampling profiler', () => { + test('should start and stop sampling', () => { + AutoProfiler.startSampling(); + const samples = AutoProfiler.stopSampling(); + + expect(Array.isArray(samples)).toBe(true); + }); + + test('should return empty array when sampling was never started', () => { + const samples = AutoProfiler.stopSampling(); + expect(samples).toEqual([]); + }); + + test('should collect samples during execution', async () => { + AutoProfiler.startSampling(); + + // Do some work + for (let i = 0; i < 100; i++) { + Math.sqrt(i); + } + + // Wait a bit for samples to accumulate + await new Promise((resolve) => setTimeout(resolve, 50)); + + const samples = AutoProfiler.stopSampling(); + expect(Array.isArray(samples)).toBe(true); + }); + }); + + describe('dispose', () => { + test('should clean up resources', () => { + const instance = AutoProfiler.getInstance(); + instance.startSampling(); + instance.dispose(); + + // After dispose, stopping sampling should return empty array + const samples = instance.stopSampling(); + expect(samples).toEqual([]); + }); + }); + + describe('minDuration filtering', () => { + test('should respect minDuration setting', () => { + AutoProfiler.resetInstance(); + const instance = AutoProfiler.getInstance({ minDuration: 1000 }); + + const quickFn = () => 1; + const wrappedFn = instance.wrapFunction(quickFn, 'quickFn', ProfileCategory.Script); + + ProfilerSDK.beginFrame(); + expect(wrappedFn()).toBe(1); + ProfilerSDK.endFrame(); + }); + }); +}); + +describe('@Profile decorator', () => { + beforeEach(() => { + ProfilerSDK.reset(); + ProfilerSDK.setEnabled(true); + }); + + afterEach(() => { + ProfilerSDK.reset(); + }); + + test('should profile decorated methods', () => { + class TestClass { + @Profile() + calculate(): number { + return 42; + } + } + + const instance = new TestClass(); + + ProfilerSDK.beginFrame(); + const result = instance.calculate(); + ProfilerSDK.endFrame(); + + expect(result).toBe(42); + }); + + test('should use custom name when provided', () => { + class TestClass { + @Profile('CustomMethodName', ProfileCategory.Physics) + compute(): number { + return 100; + } + } + + const instance = new TestClass(); + + ProfilerSDK.beginFrame(); + expect(instance.compute()).toBe(100); + ProfilerSDK.endFrame(); + }); + + test('should handle async methods', async () => { + class TestClass { + @Profile() + async asyncMethod(): Promise { + await new Promise((resolve) => setTimeout(resolve, 1)); + return 99; + } + } + + const instance = new TestClass(); + + ProfilerSDK.beginFrame(); + const result = await instance.asyncMethod(); + ProfilerSDK.endFrame(); + + expect(result).toBe(99); + }); + + test('should handle method that throws error', () => { + class TestClass { + @Profile() + throwingMethod(): void { + throw new Error('Decorated error'); + } + } + + const instance = new TestClass(); + + ProfilerSDK.beginFrame(); + expect(() => instance.throwingMethod()).toThrow('Decorated error'); + ProfilerSDK.endFrame(); + }); + + test('should skip profiling when ProfilerSDK is disabled', () => { + ProfilerSDK.setEnabled(false); + + class TestClass { + @Profile() + simpleMethod(): number { + return 1; + } + } + + const instance = new TestClass(); + expect(instance.simpleMethod()).toBe(1); + }); +}); + +describe('@ProfileClass decorator', () => { + beforeEach(() => { + AutoProfiler.resetInstance(); + ProfilerSDK.reset(); + ProfilerSDK.setEnabled(true); + }); + + afterEach(() => { + AutoProfiler.resetInstance(); + ProfilerSDK.reset(); + }); + + test('should profile all methods of decorated class', () => { + @ProfileClass(ProfileCategory.ECS) + class GameSystem { + update(): void { + // Update logic + } + + render(): number { + return 1; + } + } + + ProfilerSDK.beginFrame(); + const system = new GameSystem(); + system.update(); + expect(system.render()).toBe(1); + ProfilerSDK.endFrame(); + }); + + test('should use default category when not specified', () => { + @ProfileClass() + class DefaultSystem { + process(): boolean { + return true; + } + } + + ProfilerSDK.beginFrame(); + const system = new DefaultSystem(); + expect(system.process()).toBe(true); + ProfilerSDK.endFrame(); + }); +}); diff --git a/packages/ecs-engine-bindgen/package.json b/packages/ecs-engine-bindgen/package.json index 42ca2939..5f1699d3 100644 --- a/packages/ecs-engine-bindgen/package.json +++ b/packages/ecs-engine-bindgen/package.json @@ -2,25 +2,23 @@ "name": "@esengine/ecs-engine-bindgen", "version": "0.1.0", "description": "Bridge layer between ECS Framework and Rust Engine | ECS框架与Rust引擎之间的桥接层", - "main": "bin/index.js", - "module": "bin/index.js", - "types": "bin/index.d.ts", + "main": "dist/index.js", + "module": "dist/index.js", + "types": "dist/index.d.ts", + "type": "module", "exports": { ".": { - "types": "./bin/index.d.ts", - "import": "./bin/index.js", - "require": "./bin/index.js", - "default": "./bin/index.js" + "types": "./dist/index.d.ts", + "import": "./dist/index.js" } }, "files": [ - "bin", "dist" ], "scripts": { - "build": "tsc", - "build:watch": "tsc --watch", - "clean": "rimraf bin dist" + "build": "tsup", + "build:watch": "tsup --watch", + "clean": "rimraf dist" }, "repository": { "type": "git", @@ -36,15 +34,16 @@ ], "author": "ESEngine Team", "license": "MIT", - "peerDependencies": { - "@esengine/ecs-framework": "workspace:*", - "@esengine/ecs-components": "workspace:*", - "@esengine/asset-system": "workspace:*" - }, "optionalDependencies": { "es-engine": "file:../engine/pkg" }, "devDependencies": { + "@esengine/ecs-framework": "workspace:*", + "@esengine/engine-core": "workspace:*", + "@esengine/sprite": "workspace:*", + "@esengine/camera": "workspace:*", + "@esengine/asset-system": "workspace:*", + "tsup": "^8.5.1", "typescript": "^5.8.0", "rimraf": "^5.0.0" } diff --git a/packages/ecs-engine-bindgen/src/core/SpriteRenderHelper.ts b/packages/ecs-engine-bindgen/src/core/SpriteRenderHelper.ts index 73214220..d00b6ac9 100644 --- a/packages/ecs-engine-bindgen/src/core/SpriteRenderHelper.ts +++ b/packages/ecs-engine-bindgen/src/core/SpriteRenderHelper.ts @@ -6,7 +6,7 @@ import { Entity, Component } from '@esengine/ecs-framework'; import type { EngineBridge } from './EngineBridge'; import { RenderBatcher } from './RenderBatcher'; -import { SpriteComponent } from '@esengine/ecs-components'; +import { SpriteComponent } from '@esengine/sprite'; import type { SpriteRenderData } from '../types'; /** @@ -20,6 +20,12 @@ export interface ITransformComponent { position: { x: number; y: number; z?: number }; rotation: number | { x: number; y: number; z: number }; scale: { x: number; y: number; z?: number }; + /** 世界位置(由 TransformSystem 计算,考虑父级变换) */ + worldPosition?: { x: number; y: number; z?: number }; + /** 世界旋转(由 TransformSystem 计算,考虑父级变换) */ + worldRotation?: { x: number; y: number; z: number }; + /** 世界缩放(由 TransformSystem 计算,考虑父级变换) */ + worldScale?: { x: number; y: number; z?: number }; } /** diff --git a/packages/ecs-engine-bindgen/src/index.ts b/packages/ecs-engine-bindgen/src/index.ts index af231e0f..51abf0d1 100644 --- a/packages/ecs-engine-bindgen/src/index.ts +++ b/packages/ecs-engine-bindgen/src/index.ts @@ -5,9 +5,11 @@ * @packageDocumentation */ -export { EngineBridge, EngineBridgeConfig } from './core/EngineBridge'; +export { EngineBridge } from './core/EngineBridge'; +export type { EngineBridgeConfig } from './core/EngineBridge'; export { RenderBatcher } from './core/RenderBatcher'; -export { SpriteRenderHelper, ITransformComponent } from './core/SpriteRenderHelper'; +export { SpriteRenderHelper } from './core/SpriteRenderHelper'; +export type { ITransformComponent } from './core/SpriteRenderHelper'; export { EngineRenderSystem, type TransformComponentType, type IRenderDataProvider, type IUIRenderDataProvider, type GizmoDataProviderFn, type HasGizmoProviderFn, type ProviderRenderData } from './systems/EngineRenderSystem'; export { CameraSystem } from './systems/CameraSystem'; export * from './types'; diff --git a/packages/ecs-engine-bindgen/src/systems/CameraSystem.ts b/packages/ecs-engine-bindgen/src/systems/CameraSystem.ts index 6281ce3e..a2663505 100644 --- a/packages/ecs-engine-bindgen/src/systems/CameraSystem.ts +++ b/packages/ecs-engine-bindgen/src/systems/CameraSystem.ts @@ -4,7 +4,7 @@ */ import { EntitySystem, Matcher, Entity, ECSSystem } from '@esengine/ecs-framework'; -import { CameraComponent } from '@esengine/ecs-components'; +import { CameraComponent } from '@esengine/camera'; import type { EngineBridge } from '../core/EngineBridge'; @ECSSystem('Camera', { updateOrder: -100 }) diff --git a/packages/ecs-engine-bindgen/src/systems/EngineRenderSystem.ts b/packages/ecs-engine-bindgen/src/systems/EngineRenderSystem.ts index cd0fd251..fef29376 100644 --- a/packages/ecs-engine-bindgen/src/systems/EngineRenderSystem.ts +++ b/packages/ecs-engine-bindgen/src/systems/EngineRenderSystem.ts @@ -4,7 +4,9 @@ */ import { EntitySystem, Matcher, Entity, ComponentType, ECSSystem, Component, Core } from '@esengine/ecs-framework'; -import { SpriteComponent, CameraComponent, TransformComponent } from '@esengine/ecs-components'; +import { TransformComponent } from '@esengine/engine-core'; +import { SpriteComponent } from '@esengine/sprite'; +import { CameraComponent } from '@esengine/camera'; import type { EngineBridge } from '../core/EngineBridge'; import { RenderBatcher } from '../core/RenderBatcher'; import type { SpriteRenderData } from '../types'; @@ -269,10 +271,12 @@ export class EngineRenderSystem extends EntitySystem { } } - // Handle rotation as number or Vector3 (use z for 2D) - const rotation = typeof transform.rotation === 'number' - ? transform.rotation - : transform.rotation.z; + // 使用世界变换(由 TransformSystem 计算,考虑父级变换),回退到本地变换 + const pos = transform.worldPosition ?? transform.position; + const scl = transform.worldScale ?? transform.scale; + const rot = transform.worldRotation + ? transform.worldRotation.z + : (typeof transform.rotation === 'number' ? transform.rotation : transform.rotation.z); // Convert hex color string to packed RGBA | 将十六进制颜色字符串转换为打包的RGBA const color = this.hexToPackedColor(sprite.color, sprite.alpha); @@ -286,14 +290,14 @@ export class EngineRenderSystem extends EntitySystem { textureId = this.bridge.getOrLoadTextureByPath(sprite.texture); } - // Pass actual display dimensions (sprite size * transform scale) - // 传递实际显示尺寸(sprite尺寸 * 变换缩放) + // Pass actual display dimensions (sprite size * world transform scale) + // 传递实际显示尺寸(sprite尺寸 * 世界变换缩放) const renderData: SpriteRenderData = { - x: transform.position.x, - y: transform.position.y, - rotation, - scaleX: sprite.width * transform.scale.x, - scaleY: sprite.height * transform.scale.y, + x: pos.x, + y: pos.y, + rotation: rot, + scaleX: sprite.width * scl.x, + scaleY: sprite.height * scl.y, originX: sprite.anchorX, originY: sprite.anchorY, textureId, diff --git a/packages/ecs-engine-bindgen/tsconfig.build.json b/packages/ecs-engine-bindgen/tsconfig.build.json new file mode 100644 index 00000000..0aa96c5a --- /dev/null +++ b/packages/ecs-engine-bindgen/tsconfig.build.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "composite": false, + "declaration": true, + "declarationMap": true, + "outDir": "./dist" + }, + "exclude": ["node_modules", "dist", "bin", "**/*.test.ts"] +} diff --git a/packages/ecs-engine-bindgen/tsconfig.json b/packages/ecs-engine-bindgen/tsconfig.json index fb4d04e9..aeab6ae6 100644 --- a/packages/ecs-engine-bindgen/tsconfig.json +++ b/packages/ecs-engine-bindgen/tsconfig.json @@ -1,22 +1,9 @@ { + "extends": "../../tsconfig.base.json", "compilerOptions": { - "target": "ES2020", - "module": "ES2020", - "lib": ["ES2020", "DOM"], - "declaration": true, - "declarationMap": true, - "sourceMap": true, - "outDir": "./bin", - "rootDir": "./src", - "strict": true, "composite": true, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "moduleResolution": "node", - "resolveJsonModule": true, - "experimentalDecorators": true, - "emitDecoratorMetadata": true + "outDir": "./dist", + "rootDir": "./src" }, "include": ["src/**/*"], "exclude": ["node_modules", "bin", "dist"] diff --git a/packages/ecs-engine-bindgen/tsup.config.ts b/packages/ecs-engine-bindgen/tsup.config.ts new file mode 100644 index 00000000..eb7dbc8c --- /dev/null +++ b/packages/ecs-engine-bindgen/tsup.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'tsup'; +import { runtimeOnlyPreset } from '../build-config/src/presets/plugin-tsup'; + +export default defineConfig({ + ...runtimeOnlyPreset({ + external: ['es-engine'] + }), + tsconfig: 'tsconfig.build.json' +}); diff --git a/packages/editor-app/package.json b/packages/editor-app/package.json index 92fe15d2..0d478ece 100644 --- a/packages/editor-app/package.json +++ b/packages/editor-app/package.json @@ -17,16 +17,25 @@ "dependencies": { "@esengine/asset-system": "workspace:*", "@esengine/behavior-tree": "workspace:*", + "@esengine/behavior-tree-editor": "workspace:*", "@esengine/blueprint": "workspace:*", + "@esengine/blueprint-editor": "workspace:*", "@esengine/editor-runtime": "workspace:*", - "@esengine/ecs-components": "workspace:*", + "@esengine/engine-core": "workspace:*", + "@esengine/sprite": "workspace:*", + "@esengine/camera": "workspace:*", + "@esengine/audio": "workspace:*", "@esengine/physics-rapier2d": "workspace:*", + "@esengine/physics-rapier2d-editor": "workspace:*", "@esengine/tilemap": "workspace:*", + "@esengine/tilemap-editor": "workspace:*", "@esengine/ui": "workspace:*", + "@esengine/ui-editor": "workspace:*", "@esengine/ecs-engine-bindgen": "workspace:*", "@esengine/ecs-framework": "workspace:*", "@esengine/editor-core": "workspace:*", "@esengine/engine": "workspace:*", + "@esengine/runtime-core": "workspace:*", "@monaco-editor/react": "^4.7.0", "@tauri-apps/api": "^2.2.0", "@tauri-apps/plugin-cli": "^2.4.1", diff --git a/packages/editor-app/src-tauri/src/profiler_ws.rs b/packages/editor-app/src-tauri/src/profiler_ws.rs index f9110c7c..889272d2 100644 --- a/packages/editor-app/src-tauri/src/profiler_ws.rs +++ b/packages/editor-app/src-tauri/src/profiler_ws.rs @@ -43,7 +43,6 @@ impl ProfilerServer { result = listener.accept() => { match result { Ok((stream, peer_addr)) => { - println!("[ProfilerServer] New connection from: {}", peer_addr); let tx = tx.clone(); tokio::spawn(handle_connection(stream, peer_addr, tx)); } @@ -98,7 +97,12 @@ async fn handle_connection( let ws_stream = match accept_async(stream).await { Ok(ws) => ws, Err(e) => { - eprintln!("[ProfilerServer] WebSocket error: {}", e); + // 忽略非 WebSocket 连接的错误(如普通 HTTP 请求、健康检查等) + // 这些是正常现象,不需要输出错误日志 + let error_str = e.to_string(); + if !error_str.contains("Connection: upgrade") && !error_str.contains("protocol error") { + eprintln!("[ProfilerServer] WebSocket error: {}", e); + } return; } }; diff --git a/packages/editor-app/src/App.tsx b/packages/editor-app/src/App.tsx index 224ffa29..01536f42 100644 --- a/packages/editor-app/src/App.tsx +++ b/packages/editor-app/src/App.tsx @@ -34,9 +34,7 @@ import { ProjectCreationWizard } from './components/ProjectCreationWizard'; import { SceneHierarchy } from './components/SceneHierarchy'; import { Inspector } from './components/inspectors/Inspector'; import { AssetBrowser } from './components/AssetBrowser'; -import { ConsolePanel } from './components/ConsolePanel'; import { Viewport } from './components/Viewport'; -import { ProfilerWindow } from './components/ProfilerWindow'; import { AdvancedProfilerWindow } from './components/AdvancedProfilerWindow'; import { PortManager } from './components/PortManager'; import { SettingsWindow } from './components/SettingsWindow'; @@ -110,7 +108,6 @@ function App() { const [panels, setPanels] = useState([]); const [pluginUpdateTrigger, setPluginUpdateTrigger] = useState(0); const [isRemoteConnected, setIsRemoteConnected] = useState(false); - const [isProfilerMode, setIsProfilerMode] = useState(false); const [showProjectWizard, setShowProjectWizard] = useState(false); const { @@ -372,6 +369,9 @@ function App() { await projectService.openProject(projectPath); + // 注意:插件配置会在引擎初始化后加载和激活 + // Note: Plugin config will be loaded and activated after engine initialization + // 设置 Tauri project:// 协议的基础路径(用于加载插件等项目文件) await TauriAPI.setProjectBasePath(projectPath); @@ -392,6 +392,19 @@ function App() { throw new Error(locale === 'zh' ? '引擎初始化超时' : 'Engine initialization timeout'); } + // 加载项目插件配置并激活插件(在引擎初始化后、模块系统初始化前) + // Load project plugin config and activate plugins (after engine init, before module system init) + if (pluginManager) { + const pluginSettings = projectService.getPluginSettings(); + console.log('[App] Plugin settings from project:', pluginSettings); + if (pluginSettings && pluginSettings.enabledPlugins.length > 0) { + console.log('[App] Loading plugin config:', pluginSettings.enabledPlugins); + await pluginManager.loadConfig({ enabledPlugins: pluginSettings.enabledPlugins }); + } else { + console.log('[App] No plugin settings found in project config'); + } + } + // 初始化模块系统(所有插件的 runtimeModule 会在 PluginManager 安装时自动注册) await engineService.initializeModuleSystems(); @@ -520,13 +533,6 @@ function App() { } }; - const handleProfilerMode = async () => { - setIsProfilerMode(true); - setIsRemoteConnected(true); - setProjectLoaded(true); - setStatus(t('header.status.profilerMode') || 'Profiler Mode - Waiting for connection...'); - }; - const handleNewScene = async () => { if (!sceneManager) { console.error('SceneManagerService not available'); @@ -631,7 +637,6 @@ function App() { setProjectLoaded(false); setCurrentProjectPath(null); - setIsProfilerMode(false); setStatus(t('header.status.ready')); }; @@ -705,45 +710,26 @@ function App() { useEffect(() => { if (projectLoaded && entityStore && messageHub && logService && uiRegistry && pluginManager) { - let corePanels: FlexDockPanel[]; - - if (isProfilerMode) { - corePanels = [ - { - id: 'scene-hierarchy', - title: locale === 'zh' ? '场景层级' : 'Scene Hierarchy', - content: , - closable: false - }, - { - id: 'inspector', - title: locale === 'zh' ? '检视器' : 'Inspector', - content: , - closable: false - } - ]; - } else { - corePanels = [ - { - id: 'scene-hierarchy', - title: locale === 'zh' ? '场景层级' : 'Scene Hierarchy', - content: , - closable: false - }, - { - id: 'viewport', - title: locale === 'zh' ? '视口' : 'Viewport', - content: , - closable: false - }, - { - id: 'inspector', - title: locale === 'zh' ? '检视器' : 'Inspector', - content: , - closable: false - } - ]; - } + const corePanels: FlexDockPanel[] = [ + { + id: 'scene-hierarchy', + title: locale === 'zh' ? '场景层级' : 'Scene Hierarchy', + content: , + closable: false + }, + { + id: 'viewport', + title: locale === 'zh' ? '视口' : 'Viewport', + content: , + closable: false + }, + { + id: 'inspector', + title: locale === 'zh' ? '检视器' : 'Inspector', + content: , + closable: false + } + ]; // 获取启用的插件面板 const pluginPanels: FlexDockPanel[] = uiRegistry.getAllPanels() @@ -797,7 +783,7 @@ function App() { setPanels([...corePanels, ...pluginPanels, ...dynamicPanels]); } - }, [projectLoaded, entityStore, messageHub, logService, uiRegistry, pluginManager, locale, currentProjectPath, t, pluginUpdateTrigger, isProfilerMode, handleOpenSceneByPath, activeDynamicPanels, dynamicPanelTitles]); + }, [projectLoaded, entityStore, messageHub, logService, uiRegistry, pluginManager, locale, currentProjectPath, t, pluginUpdateTrigger, handleOpenSceneByPath, activeDynamicPanels, dynamicPanelTitles]); if (!initialized) { @@ -819,7 +805,6 @@ function App() { onOpenProject={handleOpenProject} onCreateProject={handleCreateProject} onOpenRecentProject={handleOpenRecentProject} - onProfilerMode={handleProfilerMode} onLocaleChange={handleLocaleChange} recentProjects={recentProjects} locale={locale} @@ -947,12 +932,11 @@ function App() { /> - {showProfiler && ( - setShowProfiler(false)} /> - )} - - {showAdvancedProfiler && ( - setShowAdvancedProfiler(false)} /> + {(showProfiler || showAdvancedProfiler) && ( + { + setShowProfiler(false); + setShowAdvancedProfiler(false); + }} /> )} {showPortManager && ( diff --git a/packages/editor-app/src/app/managers/PluginInstaller.ts b/packages/editor-app/src/app/managers/PluginInstaller.ts index df8ff967..37108a12 100644 --- a/packages/editor-app/src/app/managers/PluginInstaller.ts +++ b/packages/editor-app/src/app/managers/PluginInstaller.ts @@ -1,6 +1,9 @@ /** * 插件安装器 * Plugin Installer + * + * 现在所有插件都使用统一的 IPlugin 接口,无需适配器。 + * Now all plugins use the unified IPlugin interface, no adapter needed. */ import type { PluginManager } from '@esengine/editor-core'; @@ -13,12 +16,12 @@ import { EditorAppearancePlugin } from '../../plugins/builtin/EditorAppearancePl import { PluginConfigPlugin } from '../../plugins/builtin/PluginConfigPlugin'; import { ProjectSettingsPlugin } from '../../plugins/builtin/ProjectSettingsPlugin'; -// 统一模块插件(CSS 已内联到 JS 中,导入时自动注入) -import { TilemapPlugin } from '@esengine/tilemap'; -import { UIPlugin } from '@esengine/ui'; -import { BehaviorTreePlugin } from '@esengine/behavior-tree'; -import { Physics2DPlugin } from '@esengine/physics-rapier2d'; -import { BlueprintPlugin } from '@esengine/blueprint/editor'; +// 统一模块插件(从编辑器包导入完整插件,包含 runtime + editor) +import { BehaviorTreePlugin } from '@esengine/behavior-tree-editor'; +import { Physics2DPlugin } from '@esengine/physics-rapier2d-editor'; +import { TilemapPlugin } from '@esengine/tilemap-editor'; +import { UIPlugin } from '@esengine/ui-editor'; +import { BlueprintPlugin } from '@esengine/blueprint-editor'; export class PluginInstaller { /** @@ -61,13 +64,13 @@ export class PluginInstaller { console.error(`[PluginInstaller] ${name} is invalid: missing descriptor`, plugin); continue; } + // 详细日志,检查 editorModule 是否存在 + console.log(`[PluginInstaller] ${name}: descriptor.id=${plugin.descriptor.id}, hasRuntimeModule=${!!plugin.runtimeModule}, hasEditorModule=${!!plugin.editorModule}`); try { pluginManager.register(plugin); } catch (error) { console.error(`[PluginInstaller] Failed to register ${name}:`, error); } } - - // All builtin plugins registered } } diff --git a/packages/editor-app/src/app/managers/ServiceRegistry.ts b/packages/editor-app/src/app/managers/ServiceRegistry.ts index 19240107..d25f4e4e 100644 --- a/packages/editor-app/src/app/managers/ServiceRegistry.ts +++ b/packages/editor-app/src/app/managers/ServiceRegistry.ts @@ -14,6 +14,7 @@ import { SceneManagerService, SceneTemplateRegistry, FileActionRegistry, + IFileActionRegistry, EntityCreationRegistry, PluginManager, IPluginManager, @@ -28,14 +29,11 @@ import { CompilerRegistry, ICompilerRegistry } from '@esengine/editor-core'; -import { - TransformComponent, - SpriteComponent, - SpriteAnimatorComponent, - TextComponent, - CameraComponent, - AudioSourceComponent -} from '@esengine/ecs-components'; +import { TransformComponent } from '@esengine/engine-core'; +import { SpriteComponent, SpriteAnimatorComponent } from '@esengine/sprite'; +import { CameraComponent } from '@esengine/camera'; +import { AudioSourceComponent } from '@esengine/audio'; +import { UITextComponent } from '@esengine/ui'; import { BehaviorTreeRuntimeComponent } from '@esengine/behavior-tree'; import { TauriFileAPI } from '../../adapters/TauriFileAPI'; import { DIContainer } from '../../core/di/DIContainer'; @@ -110,7 +108,7 @@ export class ServiceRegistry { { name: 'TransformComponent', type: TransformComponent, editorName: 'Transform', category: 'components.category.core', description: 'components.transform.description', icon: 'Move3d' }, { name: 'SpriteComponent', type: SpriteComponent, editorName: 'Sprite', category: 'components.category.rendering', description: 'components.sprite.description', icon: 'Image' }, { name: 'SpriteAnimatorComponent', type: SpriteAnimatorComponent, editorName: 'SpriteAnimator', category: 'components.category.rendering', description: 'components.spriteAnimator.description', icon: 'Film' }, - { name: 'TextComponent', type: TextComponent, editorName: 'Text', category: 'components.category.rendering', description: 'components.text.description', icon: 'Type' }, + { name: 'UITextComponent', type: UITextComponent, editorName: 'UIText', category: 'components.category.ui', description: 'components.text.description', icon: 'Type' }, { name: 'CameraComponent', type: CameraComponent, editorName: 'Camera', category: 'components.category.rendering', description: 'components.camera.description', icon: 'Camera' }, { name: 'AudioSourceComponent', type: AudioSourceComponent, editorName: 'AudioSource', category: 'components.category.audio', description: 'components.audioSource.description', icon: 'Volume2' }, { name: 'BehaviorTreeRuntimeComponent', type: BehaviorTreeRuntimeComponent, editorName: 'BehaviorTreeRuntime', category: 'components.category.ai', description: 'components.behaviorTreeRuntime.description', icon: 'GitBranch' } @@ -154,6 +152,7 @@ export class ServiceRegistry { Core.services.registerInstance(SettingsRegistry, settingsRegistry); Core.services.registerInstance(SceneManagerService, sceneManager); Core.services.registerInstance(FileActionRegistry, fileActionRegistry); + Core.services.registerInstance(IFileActionRegistry, fileActionRegistry); // Symbol 注册用于跨包插件访问 Core.services.registerInstance(EntityCreationRegistry, entityCreationRegistry); Core.services.registerInstance(ComponentActionRegistry, componentActionRegistry); Core.services.registerInstance(ComponentInspectorRegistry, componentInspectorRegistry); diff --git a/packages/editor-app/src/application/commands/entity/CreateAnimatedSpriteEntityCommand.ts b/packages/editor-app/src/application/commands/entity/CreateAnimatedSpriteEntityCommand.ts index e782b526..1790f724 100644 --- a/packages/editor-app/src/application/commands/entity/CreateAnimatedSpriteEntityCommand.ts +++ b/packages/editor-app/src/application/commands/entity/CreateAnimatedSpriteEntityCommand.ts @@ -1,6 +1,7 @@ -import { Core, Entity } from '@esengine/ecs-framework'; +import { Core, Entity, HierarchySystem, HierarchyComponent } from '@esengine/ecs-framework'; import { EntityStoreService, MessageHub } from '@esengine/editor-core'; -import { TransformComponent, SpriteComponent, SpriteAnimatorComponent } from '@esengine/ecs-components'; +import { TransformComponent } from '@esengine/engine-core'; +import { SpriteComponent, SpriteAnimatorComponent } from '@esengine/sprite'; import { BaseCommand } from '../BaseCommand'; /** @@ -28,13 +29,15 @@ export class CreateAnimatedSpriteEntityCommand extends BaseCommand { this.entity = scene.createEntity(this.entityName); this.entityId = this.entity.id; - // 添加Transform、Sprite和Animator组件 + // 添加 Transform、Sprite、Animator 和 Hierarchy 组件 this.entity.addComponent(new TransformComponent()); this.entity.addComponent(new SpriteComponent()); this.entity.addComponent(new SpriteAnimatorComponent()); + this.entity.addComponent(new HierarchyComponent()); if (this.parentEntity) { - this.parentEntity.addChild(this.entity); + const hierarchySystem = scene.getSystem(HierarchySystem); + hierarchySystem?.setParent(this.entity, this.parentEntity); } this.entityStore.addEntity(this.entity, this.parentEntity); diff --git a/packages/editor-app/src/application/commands/entity/CreateCameraEntityCommand.ts b/packages/editor-app/src/application/commands/entity/CreateCameraEntityCommand.ts index 2fbdb722..5e04d8cf 100644 --- a/packages/editor-app/src/application/commands/entity/CreateCameraEntityCommand.ts +++ b/packages/editor-app/src/application/commands/entity/CreateCameraEntityCommand.ts @@ -1,6 +1,7 @@ -import { Core, Entity } from '@esengine/ecs-framework'; +import { Core, Entity, HierarchySystem, HierarchyComponent } from '@esengine/ecs-framework'; import { EntityStoreService, MessageHub } from '@esengine/editor-core'; -import { TransformComponent, CameraComponent } from '@esengine/ecs-components'; +import { TransformComponent } from '@esengine/engine-core'; +import { CameraComponent } from '@esengine/camera'; import { BaseCommand } from '../BaseCommand'; /** @@ -28,12 +29,14 @@ export class CreateCameraEntityCommand extends BaseCommand { this.entity = scene.createEntity(this.entityName); this.entityId = this.entity.id; - // 添加Transform和Camera组件 + // 添加 Transform、Camera 和 Hierarchy 组件 this.entity.addComponent(new TransformComponent()); this.entity.addComponent(new CameraComponent()); + this.entity.addComponent(new HierarchyComponent()); if (this.parentEntity) { - this.parentEntity.addChild(this.entity); + const hierarchySystem = scene.getSystem(HierarchySystem); + hierarchySystem?.setParent(this.entity, this.parentEntity); } this.entityStore.addEntity(this.entity, this.parentEntity); diff --git a/packages/editor-app/src/application/commands/entity/CreateEntityCommand.ts b/packages/editor-app/src/application/commands/entity/CreateEntityCommand.ts index 88bae7ea..467799c0 100644 --- a/packages/editor-app/src/application/commands/entity/CreateEntityCommand.ts +++ b/packages/editor-app/src/application/commands/entity/CreateEntityCommand.ts @@ -1,6 +1,6 @@ -import { Core, Entity } from '@esengine/ecs-framework'; +import { Core, Entity, HierarchySystem, HierarchyComponent } from '@esengine/ecs-framework'; import { EntityStoreService, MessageHub } from '@esengine/editor-core'; -import { TransformComponent } from '@esengine/ecs-components'; +import { TransformComponent } from '@esengine/engine-core'; import { BaseCommand } from '../BaseCommand'; /** @@ -28,11 +28,15 @@ export class CreateEntityCommand extends BaseCommand { this.entity = scene.createEntity(this.entityName); this.entityId = this.entity.id; - // 自动添加Transform组件 + // 自动添加 Transform 组件 this.entity.addComponent(new TransformComponent()); + // 添加 HierarchyComponent 支持层级结构 + this.entity.addComponent(new HierarchyComponent()); + if (this.parentEntity) { - this.parentEntity.addChild(this.entity); + const hierarchySystem = scene.getSystem(HierarchySystem); + hierarchySystem?.setParent(this.entity, this.parentEntity); } this.entityStore.addEntity(this.entity, this.parentEntity); diff --git a/packages/editor-app/src/application/commands/entity/CreateSpriteEntityCommand.ts b/packages/editor-app/src/application/commands/entity/CreateSpriteEntityCommand.ts index 25cfffcb..c5054c38 100644 --- a/packages/editor-app/src/application/commands/entity/CreateSpriteEntityCommand.ts +++ b/packages/editor-app/src/application/commands/entity/CreateSpriteEntityCommand.ts @@ -1,6 +1,7 @@ -import { Core, Entity } from '@esengine/ecs-framework'; +import { Core, Entity, HierarchySystem, HierarchyComponent } from '@esengine/ecs-framework'; import { EntityStoreService, MessageHub } from '@esengine/editor-core'; -import { TransformComponent, SpriteComponent } from '@esengine/ecs-components'; +import { TransformComponent } from '@esengine/engine-core'; +import { SpriteComponent } from '@esengine/sprite'; import { BaseCommand } from '../BaseCommand'; /** @@ -28,12 +29,14 @@ export class CreateSpriteEntityCommand extends BaseCommand { this.entity = scene.createEntity(this.entityName); this.entityId = this.entity.id; - // 添加Transform和Sprite组件 + // 添加 Transform、Sprite 和 Hierarchy 组件 this.entity.addComponent(new TransformComponent()); this.entity.addComponent(new SpriteComponent()); + this.entity.addComponent(new HierarchyComponent()); if (this.parentEntity) { - this.parentEntity.addChild(this.entity); + const hierarchySystem = scene.getSystem(HierarchySystem); + hierarchySystem?.setParent(this.entity, this.parentEntity); } this.entityStore.addEntity(this.entity, this.parentEntity); diff --git a/packages/editor-app/src/application/commands/entity/CreateTilemapEntityCommand.ts b/packages/editor-app/src/application/commands/entity/CreateTilemapEntityCommand.ts index 79d1a5b7..74ca0356 100644 --- a/packages/editor-app/src/application/commands/entity/CreateTilemapEntityCommand.ts +++ b/packages/editor-app/src/application/commands/entity/CreateTilemapEntityCommand.ts @@ -1,6 +1,6 @@ -import { Core, Entity } from '@esengine/ecs-framework'; +import { Core, Entity, HierarchySystem, HierarchyComponent } from '@esengine/ecs-framework'; import { EntityStoreService, MessageHub } from '@esengine/editor-core'; -import { TransformComponent } from '@esengine/ecs-components'; +import { TransformComponent } from '@esengine/engine-core'; import { TilemapComponent } from '@esengine/tilemap'; import { BaseCommand } from '../BaseCommand'; @@ -48,8 +48,9 @@ export class CreateTilemapEntityCommand extends BaseCommand { this.entity = scene.createEntity(this.entityName); this.entityId = this.entity.id; - // 添加Transform组件 + // 添加 Transform 和 Hierarchy 组件 this.entity.addComponent(new TransformComponent()); + this.entity.addComponent(new HierarchyComponent()); // 创建并配置Tilemap组件 const tilemapComponent = new TilemapComponent(); @@ -79,7 +80,8 @@ export class CreateTilemapEntityCommand extends BaseCommand { this.entity.addComponent(tilemapComponent); if (this.parentEntity) { - this.parentEntity.addChild(this.entity); + const hierarchySystem = scene.getSystem(HierarchySystem); + hierarchySystem?.setParent(this.entity, this.parentEntity); } this.entityStore.addEntity(this.entity, this.parentEntity); diff --git a/packages/editor-app/src/application/commands/entity/DeleteEntityCommand.ts b/packages/editor-app/src/application/commands/entity/DeleteEntityCommand.ts index fb904754..be216093 100644 --- a/packages/editor-app/src/application/commands/entity/DeleteEntityCommand.ts +++ b/packages/editor-app/src/application/commands/entity/DeleteEntityCommand.ts @@ -1,4 +1,4 @@ -import { Core, Entity, Component } from '@esengine/ecs-framework'; +import { Core, Entity, Component, HierarchySystem, HierarchyComponent } from '@esengine/ecs-framework'; import { EntityStoreService, MessageHub } from '@esengine/editor-core'; import { BaseCommand } from '../BaseCommand'; @@ -8,9 +8,9 @@ import { BaseCommand } from '../BaseCommand'; export class DeleteEntityCommand extends BaseCommand { private entityId: number; private entityName: string; - private parentEntity: Entity | null; + private parentEntityId: number | null; private components: Component[] = []; - private childEntities: Entity[] = []; + private childEntityIds: number[] = []; constructor( private entityStore: EntityStoreService, @@ -20,18 +20,28 @@ export class DeleteEntityCommand extends BaseCommand { super(); this.entityId = entity.id; this.entityName = entity.name; - this.parentEntity = entity.parent; + + // 通过 HierarchyComponent 获取父实体 ID + const hierarchy = entity.getComponent(HierarchyComponent); + this.parentEntityId = hierarchy?.parentId ?? null; // 保存组件状态用于撤销 this.components = [...entity.components]; - // 保存子实体 - this.childEntities = [...entity.children]; + + // 保存子实体 ID + this.childEntityIds = hierarchy?.childIds ? [...hierarchy.childIds] : []; } execute(): void { + const scene = Core.scene; + if (!scene) return; + // 先移除子实体 - for (const child of this.childEntities) { - this.entityStore.removeEntity(child); + for (const childId of this.childEntityIds) { + const child = scene.findEntityById(childId); + if (child) { + this.entityStore.removeEntity(child); + } } this.entityStore.removeEntity(this.entity); @@ -46,12 +56,17 @@ export class DeleteEntityCommand extends BaseCommand { throw new Error('场景未初始化'); } + const hierarchySystem = scene.getSystem(HierarchySystem); + // 重新创建实体 const newEntity = scene.createEntity(this.entityName); // 设置父实体 - if (this.parentEntity) { - this.parentEntity.addChild(newEntity); + if (this.parentEntityId !== null && hierarchySystem) { + const parentEntity = scene.findEntityById(this.parentEntityId); + if (parentEntity) { + hierarchySystem.setParent(newEntity, parentEntity); + } } // 恢复组件 @@ -71,12 +86,20 @@ export class DeleteEntityCommand extends BaseCommand { } // 恢复子实体 - for (const child of this.childEntities) { - newEntity.addChild(child); - this.entityStore.addEntity(child, newEntity); + for (const childId of this.childEntityIds) { + const child = scene.findEntityById(childId); + if (child && hierarchySystem) { + hierarchySystem.setParent(child, newEntity); + this.entityStore.addEntity(child, newEntity); + } } - this.entityStore.addEntity(newEntity, this.parentEntity ?? undefined); + // 获取父实体 + const parentEntity = this.parentEntityId !== null + ? scene.findEntityById(this.parentEntityId) ?? undefined + : undefined; + + this.entityStore.addEntity(newEntity, parentEntity); this.entityStore.selectEntity(newEntity); // 更新引用 diff --git a/packages/editor-app/src/application/commands/entity/ReparentEntityCommand.ts b/packages/editor-app/src/application/commands/entity/ReparentEntityCommand.ts new file mode 100644 index 00000000..b2cc3942 --- /dev/null +++ b/packages/editor-app/src/application/commands/entity/ReparentEntityCommand.ts @@ -0,0 +1,194 @@ +import { Core, Entity, HierarchySystem, HierarchyComponent } from '@esengine/ecs-framework'; +import { EntityStoreService, MessageHub } from '@esengine/editor-core'; +import { BaseCommand } from '../BaseCommand'; + +/** + * 拖放位置类型 + */ +export enum DropPosition { + /** 在目标之前 */ + BEFORE = 'before', + /** 在目标内部(作为子级) */ + INSIDE = 'inside', + /** 在目标之后 */ + AFTER = 'after' +} + +/** + * 重新设置实体父级命令 + * + * 支持拖拽重排功能,可以将实体移动到: + * - 另一个实体之前 (BEFORE) + * - 另一个实体内部作为子级 (INSIDE) + * - 另一个实体之后 (AFTER) + */ +export class ReparentEntityCommand extends BaseCommand { + private oldParentId: number | null; + private oldSiblingIndex: number; + + constructor( + private entityStore: EntityStoreService, + private messageHub: MessageHub, + private entity: Entity, + private targetEntity: Entity, + private dropPosition: DropPosition + ) { + super(); + + // 保存原始状态用于撤销 + const hierarchy = entity.getComponent(HierarchyComponent); + this.oldParentId = hierarchy?.parentId ?? null; + + // 获取在兄弟列表中的原始索引 + this.oldSiblingIndex = this.getSiblingIndex(entity); + } + + execute(): void { + const scene = Core.scene; + if (!scene) { + console.warn('[ReparentEntityCommand] No scene available'); + return; + } + + const hierarchySystem = scene.getSystem(HierarchySystem); + if (!hierarchySystem) { + console.warn('[ReparentEntityCommand] No HierarchySystem found'); + return; + } + + // 确保目标实体有 HierarchyComponent + if (!this.targetEntity.getComponent(HierarchyComponent)) { + this.targetEntity.addComponent(new HierarchyComponent()); + } + + console.log(`[ReparentEntityCommand] Moving ${this.entity.name} to ${this.targetEntity.name} (${this.dropPosition})`); + + switch (this.dropPosition) { + case DropPosition.INSIDE: + // 移动到目标实体内部作为最后一个子级 + hierarchySystem.setParent(this.entity, this.targetEntity); + break; + + case DropPosition.BEFORE: + case DropPosition.AFTER: + // 移动到目标实体的同级 + this.moveToSibling(hierarchySystem); + break; + } + + this.entityStore.syncFromScene(); + this.messageHub.publish('entity:reparented', { + entityId: this.entity.id, + targetId: this.targetEntity.id, + position: this.dropPosition + }); + } + + undo(): void { + const scene = Core.scene; + if (!scene) return; + + const hierarchySystem = scene.getSystem(HierarchySystem); + if (!hierarchySystem) return; + + // 恢复到原始父级 + const oldParent = this.oldParentId !== null + ? scene.findEntityById(this.oldParentId) + : null; + + if (oldParent) { + // 恢复到原始父级的指定位置 + hierarchySystem.insertChildAt(oldParent, this.entity, this.oldSiblingIndex); + } else { + // 恢复到根级 + hierarchySystem.setParent(this.entity, null); + } + + this.entityStore.syncFromScene(); + this.messageHub.publish('entity:reparented', { + entityId: this.entity.id, + targetId: null, + position: 'undo' + }); + } + + getDescription(): string { + const positionText = this.dropPosition === DropPosition.INSIDE + ? '移入' + : this.dropPosition === DropPosition.BEFORE ? '移动到前面' : '移动到后面'; + return `${positionText}: ${this.entity.name} -> ${this.targetEntity.name}`; + } + + /** + * 移动到目标的同级位置 + */ + private moveToSibling(hierarchySystem: HierarchySystem): void { + const targetHierarchy = this.targetEntity.getComponent(HierarchyComponent); + const targetParentId = targetHierarchy?.parentId ?? null; + + const scene = Core.scene; + if (!scene) return; + + // 获取目标的父实体 + const targetParent = targetParentId !== null + ? scene.findEntityById(targetParentId) + : null; + + // 获取目标在兄弟列表中的索引 + let targetIndex = this.getSiblingIndex(this.targetEntity); + + // 根据放置位置调整索引 + if (this.dropPosition === DropPosition.AFTER) { + targetIndex++; + } + + // 如果移动到同一父级下,需要考虑原位置对索引的影响 + const entityHierarchy = this.entity.getComponent(HierarchyComponent); + const entityParentId = entityHierarchy?.parentId ?? null; + + const bSameParent = entityParentId === targetParentId; + if (bSameParent) { + const currentIndex = this.getSiblingIndex(this.entity); + if (currentIndex < targetIndex) { + targetIndex--; + } + } + + console.log(`[ReparentEntityCommand] moveToSibling: targetParent=${targetParent?.name ?? 'ROOT'}, targetIndex=${targetIndex}`); + + if (targetParent) { + // 有父级,插入到父级的指定位置 + hierarchySystem.insertChildAt(targetParent, this.entity, targetIndex); + } else { + // 目标在根级 + // 先确保实体移动到根级 + if (entityParentId !== null) { + hierarchySystem.setParent(this.entity, null); + } + // 然后调整根级顺序 + this.entityStore.reorderEntity(this.entity.id, targetIndex); + } + } + + /** + * 获取实体在兄弟列表中的索引 + */ + private getSiblingIndex(entity: Entity): number { + const scene = Core.scene; + if (!scene) return 0; + + const hierarchy = entity.getComponent(HierarchyComponent); + const parentId = hierarchy?.parentId; + + if (parentId === null || parentId === undefined) { + // 根级实体,从 EntityStoreService 获取 + return this.entityStore.getRootEntityIds().indexOf(entity.id); + } + + const parent = scene.findEntityById(parentId); + if (!parent) return 0; + + const parentHierarchy = parent.getComponent(HierarchyComponent); + return parentHierarchy?.childIds.indexOf(entity.id) ?? 0; + } +} diff --git a/packages/editor-app/src/application/commands/entity/index.ts b/packages/editor-app/src/application/commands/entity/index.ts index 61eebbfa..ac52ea8c 100644 --- a/packages/editor-app/src/application/commands/entity/index.ts +++ b/packages/editor-app/src/application/commands/entity/index.ts @@ -4,4 +4,5 @@ export { CreateAnimatedSpriteEntityCommand } from './CreateAnimatedSpriteEntityC export { CreateCameraEntityCommand } from './CreateCameraEntityCommand'; export { CreateTilemapEntityCommand } from './CreateTilemapEntityCommand'; export { DeleteEntityCommand } from './DeleteEntityCommand'; +export { ReparentEntityCommand, DropPosition } from './ReparentEntityCommand'; diff --git a/packages/editor-app/src/components/AdvancedProfiler.tsx b/packages/editor-app/src/components/AdvancedProfiler.tsx index 34bfc0cf..0b959992 100644 --- a/packages/editor-app/src/components/AdvancedProfiler.tsx +++ b/packages/editor-app/src/components/AdvancedProfiler.tsx @@ -8,6 +8,19 @@ import '../styles/AdvancedProfiler.css'; /** * 高级性能数据接口(与 Core 的 IAdvancedProfilerData 对应) */ +interface HotspotItem { + name: string; + category: string; + inclusiveTime: number; + inclusiveTimePercent: number; + exclusiveTime: number; + exclusiveTimePercent: number; + callCount: number; + avgCallTime: number; + depth: number; + children?: HotspotItem[]; +} + interface AdvancedProfilerData { currentFrame: { frameNumber: number; @@ -41,16 +54,7 @@ interface AdvancedProfilerData { percentOfFrame: number; }>; }>; - hotspots: Array<{ - name: string; - category: string; - inclusiveTime: number; - inclusiveTimePercent: number; - exclusiveTime: number; - exclusiveTimePercent: number; - callCount: number; - avgCallTime: number; - }>; + hotspots: HotspotItem[]; callGraph: { currentFunction: string | null; callers: Array<{ @@ -120,18 +124,72 @@ const CATEGORY_COLORS: Record = { 'Custom': '#64748b' }; +type DataMode = 'oneframe' | 'average' | 'maximum'; + export function AdvancedProfiler({ profilerService }: AdvancedProfilerProps) { const [data, setData] = useState(null); const [isPaused, setIsPaused] = useState(false); const [searchTerm, setSearchTerm] = useState(''); const [selectedFunction, setSelectedFunction] = useState(null); const [expandedCategories, setExpandedCategories] = useState>(new Set(['ECS'])); + const [expandedHotspots, setExpandedHotspots] = useState>(new Set()); const [sortColumn, setSortColumn] = useState('incTime'); const [sortDirection, setSortDirection] = useState('desc'); const [viewMode, setViewMode] = useState<'hierarchical' | 'flat'>('hierarchical'); + const [dataMode, setDataMode] = useState('average'); const canvasRef = useRef(null); const frameHistoryRef = useRef>([]); const lastDataRef = useRef(null); + // 用于计算平均值和最大值的历史数据 + const hotspotHistoryRef = useRef>(new Map()); + + // 更新历史数据 + const updateHotspotHistory = useCallback((hotspots: HotspotItem[]) => { + const updateItem = (item: HotspotItem) => { + const history = hotspotHistoryRef.current.get(item.name) || { times: [], maxTime: 0 }; + history.times.push(item.inclusiveTime); + // 保留最近 60 帧的数据 + if (history.times.length > 60) { + history.times.shift(); + } + history.maxTime = Math.max(history.maxTime, item.inclusiveTime); + hotspotHistoryRef.current.set(item.name, history); + + if (item.children) { + item.children.forEach(updateItem); + } + }; + hotspots.forEach(updateItem); + }, []); + + // 根据数据模式处理 hotspots + const processHotspotsWithDataMode = useCallback((hotspots: HotspotItem[], mode: DataMode): HotspotItem[] => { + if (mode === 'oneframe') { + return hotspots; + } + + const processItem = (item: HotspotItem): HotspotItem => { + const history = hotspotHistoryRef.current.get(item.name); + let processedTime = item.inclusiveTime; + + if (history && history.times.length > 0) { + if (mode === 'average') { + processedTime = history.times.reduce((a, b) => a + b, 0) / history.times.length; + } else if (mode === 'maximum') { + processedTime = history.maxTime; + } + } + + return { + ...item, + inclusiveTime: processedTime, + avgCallTime: item.callCount > 0 ? processedTime / item.callCount : 0, + children: item.children ? item.children.map(processItem) : undefined + }; + }; + + return hotspots.map(processItem); + }, []); // 订阅数据更新 useEffect(() => { @@ -142,18 +200,21 @@ export function AdvancedProfiler({ profilerService }: AdvancedProfilerProps) { // 解析高级性能数据 if (rawData.advancedProfiler) { + // 更新历史数据 + updateHotspotHistory(rawData.advancedProfiler.hotspots); setData(rawData.advancedProfiler); lastDataRef.current = rawData.advancedProfiler; } else if (rawData.performance) { // 从传统数据构建 const advancedData = buildFromLegacyData(rawData); + updateHotspotHistory(advancedData.hotspots); setData(advancedData); lastDataRef.current = advancedData; } }); return unsubscribe; - }, [profilerService, isPaused]); + }, [profilerService, isPaused, updateHotspotHistory]); // 当选中函数变化时,通知服务端 useEffect(() => { @@ -317,44 +378,90 @@ export function AdvancedProfiler({ profilerService }: AdvancedProfilerProps) { return percent.toFixed(1) + '%'; }; + // 展平层级数据用于显示 + const flattenHotspots = (items: HotspotItem[], result: HotspotItem[] = []): HotspotItem[] => { + for (const item of items) { + // 搜索过滤 + const matchesSearch = searchTerm === '' || item.name.toLowerCase().includes(searchTerm.toLowerCase()); + + if (viewMode === 'flat') { + // 扁平模式:显示所有层级的项目 + if (matchesSearch) { + result.push({ ...item, depth: 0 }); // 扁平模式下深度都是0 + } + if (item.children) { + flattenHotspots(item.children, result); + } + } else { + // 层级模式:根据展开状态显示 + if (matchesSearch || (item.children && item.children.some(c => c.name.toLowerCase().includes(searchTerm.toLowerCase())))) { + result.push(item); + } + if (item.children && expandedHotspots.has(item.name)) { + flattenHotspots(item.children, result); + } + } + } + return result; + }; + + // 切换展开状态 + const toggleHotspotExpand = (name: string) => { + setExpandedHotspots(prev => { + const next = new Set(prev); + if (next.has(name)) { + next.delete(name); + } else { + next.add(name); + } + return next; + }); + }; + // 排序数据 - const getSortedHotspots = () => { + const getSortedHotspots = (): HotspotItem[] => { if (!data) return []; - const filtered = data.hotspots.filter(h => - searchTerm === '' || h.name.toLowerCase().includes(searchTerm.toLowerCase()) - ); + // 先根据数据模式处理 hotspots + const processedHotspots = processHotspotsWithDataMode(data.hotspots, dataMode); + const flattened = flattenHotspots(processedHotspots); - return [...filtered].sort((a, b) => { - let comparison = 0; - switch (sortColumn) { - case 'name': - comparison = a.name.localeCompare(b.name); - break; - case 'incTime': - comparison = a.inclusiveTime - b.inclusiveTime; - break; - case 'incPercent': - comparison = a.inclusiveTimePercent - b.inclusiveTimePercent; - break; - case 'excTime': - comparison = a.exclusiveTime - b.exclusiveTime; - break; - case 'excPercent': - comparison = a.exclusiveTimePercent - b.exclusiveTimePercent; - break; - case 'calls': - comparison = a.callCount - b.callCount; - break; - case 'avgTime': - comparison = a.avgCallTime - b.avgCallTime; - break; - case 'framePercent': - comparison = a.inclusiveTimePercent - b.inclusiveTimePercent; - break; - } - return sortDirection === 'asc' ? comparison : -comparison; - }); + // 扁平模式下排序 + if (viewMode === 'flat') { + return [...flattened].sort((a, b) => { + let comparison = 0; + switch (sortColumn) { + case 'name': + comparison = a.name.localeCompare(b.name); + break; + case 'incTime': + comparison = a.inclusiveTime - b.inclusiveTime; + break; + case 'incPercent': + comparison = a.inclusiveTimePercent - b.inclusiveTimePercent; + break; + case 'excTime': + comparison = a.exclusiveTime - b.exclusiveTime; + break; + case 'excPercent': + comparison = a.exclusiveTimePercent - b.exclusiveTimePercent; + break; + case 'calls': + comparison = a.callCount - b.callCount; + break; + case 'avgTime': + comparison = a.avgCallTime - b.avgCallTime; + break; + case 'framePercent': + comparison = a.inclusiveTimePercent - b.inclusiveTimePercent; + break; + } + return sortDirection === 'asc' ? comparison : -comparison; + }); + } + + // 层级模式下保持原有层级顺序 + return flattened; }; const renderSortIcon = (column: SortColumn) => { @@ -512,7 +619,11 @@ export function AdvancedProfiler({ profilerService }: AdvancedProfilerProps) { Call Graph
- setDataMode(e.target.value as DataMode)} + > @@ -654,49 +765,67 @@ export function AdvancedProfiler({ profilerService }: AdvancedProfilerProps) {
- {getSortedHotspots().map((item, index) => ( -
setSelectedFunction(item.name)} - > -
- - - {item.name} -
-
- {formatTime(item.inclusiveTime)} -
-
-
-
50 ? 'critical' : item.inclusiveTimePercent > 25 ? 'warning' : ''}`} - style={{ width: `${Math.min(item.inclusiveTimePercent, 100)}%` }} + {getSortedHotspots().map((item, index) => { + const hasChildren = item.children && item.children.length > 0; + const isExpanded = expandedHotspots.has(item.name); + const indentPadding = viewMode === 'hierarchical' ? item.depth * 16 : 0; + + return ( +
setSelectedFunction(item.name)} + > +
+ {hasChildren && viewMode === 'hierarchical' ? ( + { + e.stopPropagation(); + toggleHotspotExpand(item.name); + }} + > + {isExpanded ? : } + + ) : ( + + )} + - {formatPercent(item.inclusiveTimePercent)} + {item.name} +
+
+ {formatTime(item.inclusiveTime)} +
+
+
+
50 ? 'critical' : item.inclusiveTimePercent > 25 ? 'warning' : ''}`} + style={{ width: `${Math.min(item.inclusiveTimePercent, 100)}%` }} + /> + {formatPercent(item.inclusiveTimePercent)} +
+
+
+ {formatTime(item.exclusiveTime)} +
+
+ {formatPercent(item.exclusiveTimePercent)} +
+
+ {item.callCount} +
+
+ {formatTime(item.avgCallTime)} +
+
+ {formatPercent(item.inclusiveTimePercent)}
-
- {formatTime(item.exclusiveTime)} -
-
- {formatPercent(item.exclusiveTimePercent)} -
-
- {item.callCount} -
-
- {formatTime(item.avgCallTime)} -
-
- {formatPercent(item.inclusiveTimePercent)} -
-
- ))} + ); + })}
@@ -716,7 +845,7 @@ function buildFromLegacyData(rawData: any): AdvancedProfilerData { const fps = frameTime > 0 ? Math.round(1000 / frameTime) : 0; // 构建 hotspots - const hotspots = systems.map((sys: any) => ({ + const hotspots: HotspotItem[] = systems.map((sys: any) => ({ name: sys.name || sys.type || 'Unknown', category: 'ECS', inclusiveTime: sys.executionTime || 0, @@ -724,7 +853,8 @@ function buildFromLegacyData(rawData: any): AdvancedProfilerData { exclusiveTime: sys.executionTime || 0, exclusiveTimePercent: frameTime > 0 ? (sys.executionTime / frameTime) * 100 : 0, callCount: 1, - avgCallTime: sys.executionTime || 0 + avgCallTime: sys.executionTime || 0, + depth: 0 })); // 构建 categoryStats diff --git a/packages/editor-app/src/components/AdvancedProfilerWindow.tsx b/packages/editor-app/src/components/AdvancedProfilerWindow.tsx index 6b9fca30..e25d8f74 100644 --- a/packages/editor-app/src/components/AdvancedProfilerWindow.tsx +++ b/packages/editor-app/src/components/AdvancedProfilerWindow.tsx @@ -1,5 +1,5 @@ -import { useState, useEffect } from 'react'; -import { X, BarChart3 } from 'lucide-react'; +import { useState, useEffect, useCallback } from 'react'; +import { X, BarChart3, Maximize2, Minimize2 } from 'lucide-react'; import { ProfilerService } from '../services/ProfilerService'; import { AdvancedProfiler } from './AdvancedProfiler'; import '../styles/ProfilerWindow.css'; @@ -15,6 +15,7 @@ interface WindowWithProfiler extends Window { export function AdvancedProfilerWindow({ onClose }: AdvancedProfilerWindowProps) { const [profilerService, setProfilerService] = useState(null); const [isConnected, setIsConnected] = useState(false); + const [isFullscreen, setIsFullscreen] = useState(false); useEffect(() => { const service = (window as WindowWithProfiler).__PROFILER_SERVICE__; @@ -36,12 +37,35 @@ export function AdvancedProfilerWindow({ onClose }: AdvancedProfilerWindowProps) return () => clearInterval(interval); }, [profilerService]); + // 处理 ESC 键退出全屏 + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Escape' && isFullscreen) { + setIsFullscreen(false); + } + }; + + window.addEventListener('keydown', handleKeyDown); + return () => window.removeEventListener('keydown', handleKeyDown); + }, [isFullscreen]); + + const toggleFullscreen = useCallback(() => { + setIsFullscreen(prev => !prev); + }, []); + + const windowStyle = isFullscreen + ? { width: '100vw', height: '100vh', maxWidth: 'none', borderRadius: 0 } + : { width: '90vw', height: '85vh', maxWidth: '1600px' }; + return ( -
+
e.stopPropagation()} - style={{ width: '90vw', height: '85vh', maxWidth: '1600px' }} + style={windowStyle} >
@@ -53,9 +77,18 @@ export function AdvancedProfilerWindow({ onClose }: AdvancedProfilerWindowProps) )}
- +
+ + +
diff --git a/packages/editor-app/src/components/ConsolePanel.tsx b/packages/editor-app/src/components/ConsolePanel.tsx deleted file mode 100644 index 59223a0b..00000000 --- a/packages/editor-app/src/components/ConsolePanel.tsx +++ /dev/null @@ -1,331 +0,0 @@ -import { useState, useEffect, useRef, useMemo, memo } from 'react'; -import { LogService, LogEntry } from '@esengine/editor-core'; -import { LogLevel } from '@esengine/ecs-framework'; -import { Trash2, AlertCircle, Info, AlertTriangle, XCircle, Bug, Search, Wifi } from 'lucide-react'; -import { JsonViewer } from './JsonViewer'; -import '../styles/ConsolePanel.css'; - -interface ConsolePanelProps { - logService: LogService; -} - -const MAX_LOGS = 1000; - -// 提取JSON检测和格式化逻辑 -function tryParseJSON(message: string): { isJSON: boolean; parsed?: unknown } { - try { - const parsed: unknown = JSON.parse(message); - return { isJSON: true, parsed }; - } catch { - return { isJSON: false }; - } -} - -// 格式化时间 -function formatTime(date: Date): string { - const hours = date.getHours().toString().padStart(2, '0'); - const minutes = date.getMinutes().toString().padStart(2, '0'); - const seconds = date.getSeconds().toString().padStart(2, '0'); - const ms = date.getMilliseconds().toString().padStart(3, '0'); - return `${hours}:${minutes}:${seconds}.${ms}`; -} - -// 日志等级图标 -function getLevelIcon(level: LogLevel) { - switch (level) { - case LogLevel.Debug: - return ; - case LogLevel.Info: - return ; - case LogLevel.Warn: - return ; - case LogLevel.Error: - case LogLevel.Fatal: - return ; - default: - return ; - } -} - -// 日志等级样式类 -function getLevelClass(level: LogLevel): string { - switch (level) { - case LogLevel.Debug: - return 'log-entry-debug'; - case LogLevel.Info: - return 'log-entry-info'; - case LogLevel.Warn: - return 'log-entry-warn'; - case LogLevel.Error: - case LogLevel.Fatal: - return 'log-entry-error'; - default: - return ''; - } -} - -// 单个日志条目组件 -const LogEntryItem = memo(({ log, onOpenJsonViewer }: { - log: LogEntry; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - onOpenJsonViewer: (data: any) => void; -}) => { - const { isJSON, parsed } = useMemo(() => tryParseJSON(log.message), [log.message]); - const shouldTruncate = log.message.length > 200; - const [isExpanded, setIsExpanded] = useState(false); - - return ( -
-
- {getLevelIcon(log.level)} -
-
- {formatTime(log.timestamp)} -
-
- [{log.source === 'remote' ? '🌐 Remote' : log.source}] -
- {log.clientId && ( -
- {log.clientId} -
- )} -
-
-
- {shouldTruncate && !isExpanded ? ( - <> - - {log.message.substring(0, 200)}... - - - - ) : ( - <> - {log.message} - {shouldTruncate && ( - - )} - - )} -
- {isJSON && parsed !== undefined && ( - - )} -
-
-
- ); -}); - -LogEntryItem.displayName = 'LogEntryItem'; - -export function ConsolePanel({ logService }: ConsolePanelProps) { - // 状态管理 - const [logs, setLogs] = useState(() => logService.getLogs().slice(-MAX_LOGS)); - const [filter, setFilter] = useState(''); - const [levelFilter, setLevelFilter] = useState>(new Set([ - LogLevel.Debug, - LogLevel.Info, - LogLevel.Warn, - LogLevel.Error, - LogLevel.Fatal - ])); - const [showRemoteOnly, setShowRemoteOnly] = useState(false); - const [autoScroll, setAutoScroll] = useState(true); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const [jsonViewerData, setJsonViewerData] = useState(null); - const logContainerRef = useRef(null); - - // 订阅日志更新 - useEffect(() => { - const unsubscribe = logService.subscribe((entry) => { - setLogs((prev) => { - const newLogs = [...prev, entry]; - return newLogs.length > MAX_LOGS ? newLogs.slice(-MAX_LOGS) : newLogs; - }); - }); - - return unsubscribe; - }, [logService]); - - // 自动滚动 - useEffect(() => { - if (autoScroll && logContainerRef.current) { - logContainerRef.current.scrollTop = logContainerRef.current.scrollHeight; - } - }, [logs, autoScroll]); - - // 处理滚动 - const handleScroll = () => { - if (logContainerRef.current) { - const { scrollTop, scrollHeight, clientHeight } = logContainerRef.current; - const isAtBottom = Math.abs(scrollHeight - scrollTop - clientHeight) < 10; - setAutoScroll(isAtBottom); - } - }; - - // 清空日志 - const handleClear = () => { - logService.clear(); - setLogs([]); - }; - - // 切换等级过滤 - const toggleLevelFilter = (level: LogLevel) => { - const newFilter = new Set(levelFilter); - if (newFilter.has(level)) { - newFilter.delete(level); - } else { - newFilter.add(level); - } - setLevelFilter(newFilter); - }; - - // 过滤日志 - const filteredLogs = useMemo(() => { - return logs.filter((log) => { - if (!levelFilter.has(log.level)) return false; - if (showRemoteOnly && log.source !== 'remote') return false; - if (filter && !log.message.toLowerCase().includes(filter.toLowerCase())) { - return false; - } - return true; - }); - }, [logs, levelFilter, showRemoteOnly, filter]); - - // 统计各等级日志数量 - const levelCounts = useMemo(() => ({ - [LogLevel.Debug]: logs.filter((l) => l.level === LogLevel.Debug).length, - [LogLevel.Info]: logs.filter((l) => l.level === LogLevel.Info).length, - [LogLevel.Warn]: logs.filter((l) => l.level === LogLevel.Warn).length, - [LogLevel.Error]: logs.filter((l) => l.level === LogLevel.Error || l.level === LogLevel.Fatal).length - }), [logs]); - - const remoteLogCount = useMemo(() => - logs.filter((l) => l.source === 'remote').length - , [logs]); - - return ( -
-
-
- -
- - setFilter(e.target.value)} - /> -
-
-
- - - - - -
-
-
- {filteredLogs.length === 0 ? ( -
- -

No logs to display

-
- ) : ( - filteredLogs.map((log, index) => ( - - )) - )} -
- {jsonViewerData && ( - setJsonViewerData(null)} - /> - )} - {!autoScroll && ( - - )} -
- ); -} diff --git a/packages/editor-app/src/components/ContentBrowser.tsx b/packages/editor-app/src/components/ContentBrowser.tsx index 512d48b8..476b90e3 100644 --- a/packages/editor-app/src/components/ContentBrowser.tsx +++ b/packages/editor-app/src/components/ContentBrowser.tsx @@ -26,7 +26,15 @@ import { Trash2, Edit3, ExternalLink, - PanelRightClose + PanelRightClose, + Tag, + Link, + FileSearch, + Globe, + Package, + Clipboard, + RefreshCw, + Settings } from 'lucide-react'; import { Core } from '@esengine/ecs-framework'; import { MessageHub, FileActionRegistry, type FileCreationTemplate } from '@esengine/editor-core'; @@ -564,9 +572,191 @@ export function ContentBrowser({ icon: , onClick: () => handleAssetDoubleClick(asset) }); + items.push({ label: '', separator: true, onClick: () => {} }); + + // 保存 + items.push({ + label: locale === 'zh' ? '保存' : 'Save', + icon: , + shortcut: 'Ctrl+S', + onClick: () => { + console.log('Save file:', asset.path); + } + }); } + // 重命名 + items.push({ + label: locale === 'zh' ? '重命名' : 'Rename', + icon: , + shortcut: 'F2', + onClick: () => { + setRenameDialog({ asset, newName: asset.name }); + setContextMenu(null); + } + }); + + // 批量重命名 + items.push({ + label: locale === 'zh' ? '批量重命名' : 'Batch Rename', + icon: , + shortcut: 'Shift+F2', + disabled: true, + onClick: () => { + console.log('Batch rename'); + } + }); + + // 复制 + items.push({ + label: locale === 'zh' ? '复制' : 'Duplicate', + icon: , + shortcut: 'Ctrl+D', + onClick: () => { + console.log('Duplicate:', asset.path); + } + }); + + // 删除 + items.push({ + label: locale === 'zh' ? '删除' : 'Delete', + icon: , + shortcut: 'Delete', + onClick: () => { + setDeleteConfirmDialog(asset); + setContextMenu(null); + } + }); + + items.push({ label: '', separator: true, onClick: () => {} }); + + // 资产操作子菜单 + items.push({ + label: locale === 'zh' ? '资产操作' : 'Asset Actions', + icon: , + onClick: () => {}, + children: [ + { + label: locale === 'zh' ? '重新导入' : 'Reimport', + icon: , + onClick: () => { + console.log('Reimport asset:', asset.path); + } + }, + { + label: locale === 'zh' ? '导出...' : 'Export...', + icon: , + onClick: () => { + console.log('Export asset:', asset.path); + } + }, + { label: '', separator: true, onClick: () => {} }, + { + label: locale === 'zh' ? '迁移资产' : 'Migrate Asset', + icon: , + onClick: () => { + console.log('Migrate asset:', asset.path); + } + } + ] + }); + + // 资产本地化子菜单 + items.push({ + label: locale === 'zh' ? '资产本地化' : 'Asset Localization', + icon: , + onClick: () => {}, + children: [ + { + label: locale === 'zh' ? '创建本地化资产' : 'Create Localized Asset', + onClick: () => { + console.log('Create localized asset:', asset.path); + } + }, + { + label: locale === 'zh' ? '导入翻译' : 'Import Translation', + onClick: () => { + console.log('Import translation:', asset.path); + } + }, + { + label: locale === 'zh' ? '导出翻译' : 'Export Translation', + onClick: () => { + console.log('Export translation:', asset.path); + } + } + ] + }); + + items.push({ label: '', separator: true, onClick: () => {} }); + + // 标签管理 + items.push({ + label: locale === 'zh' ? '管理标签' : 'Manage Tags', + icon: , + shortcut: 'Ctrl+T', + onClick: () => { + console.log('Manage tags:', asset.path); + } + }); + + items.push({ label: '', separator: true, onClick: () => {} }); + + // 路径复制选项 + items.push({ + label: locale === 'zh' ? '复制引用' : 'Copy Reference', + icon: , + shortcut: 'Ctrl+C', + onClick: () => { + navigator.clipboard.writeText(asset.path); + } + }); + + items.push({ + label: locale === 'zh' ? '拷贝Object路径' : 'Copy Object Path', + icon: , + shortcut: 'Ctrl+Shift+C', + onClick: () => { + const objectPath = asset.path.replace(/\\/g, '/'); + navigator.clipboard.writeText(objectPath); + } + }); + + items.push({ + label: locale === 'zh' ? '拷贝包路径' : 'Copy Package Path', + icon: , + shortcut: 'Ctrl+Alt+C', + onClick: () => { + const packagePath = '/' + asset.path.replace(/\\/g, '/').split('/').slice(-2).join('/'); + navigator.clipboard.writeText(packagePath); + } + }); + + items.push({ label: '', separator: true, onClick: () => {} }); + + // 引用查看器 + items.push({ + label: locale === 'zh' ? '引用查看器' : 'Reference Viewer', + icon: , + shortcut: 'Alt+Shift+R', + onClick: () => { + console.log('Open reference viewer:', asset.path); + } + }); + + items.push({ + label: locale === 'zh' ? '尺寸信息图' : 'Size Map', + icon: , + shortcut: 'Alt+Shift+D', + onClick: () => { + console.log('Show size map:', asset.path); + } + }); + + items.push({ label: '', separator: true, onClick: () => {} }); + + // 在文件管理器中显示 items.push({ label: locale === 'zh' ? '在文件管理器中显示' : 'Show in Explorer', icon: , @@ -579,34 +769,8 @@ export function ContentBrowser({ } }); - items.push({ - label: locale === 'zh' ? '复制路径' : 'Copy Path', - icon: , - onClick: () => navigator.clipboard.writeText(asset.path) - }); - - items.push({ label: '', separator: true, onClick: () => {} }); - - items.push({ - label: locale === 'zh' ? '重命名' : 'Rename', - icon: , - onClick: () => { - setRenameDialog({ asset, newName: asset.name }); - setContextMenu(null); - } - }); - - items.push({ - label: locale === 'zh' ? '删除' : 'Delete', - icon: , - onClick: () => { - setDeleteConfirmDialog(asset); - setContextMenu(null); - } - }); - return items; - }, [currentPath, fileActionRegistry, handleAssetDoubleClick, loadAssets, locale, t.newFolder]); + }, [currentPath, fileActionRegistry, handleAssetDoubleClick, loadAssets, locale, t.newFolder, setRenameDialog, setDeleteConfirmDialog, setContextMenu, setCreateFileDialog]); // Render folder tree node const renderFolderNode = useCallback((node: FolderNode, depth: number = 0) => { @@ -818,7 +982,10 @@ export function ContentBrowser({ className={`cb-asset-item ${selectedPaths.has(asset.path) ? 'selected' : ''}`} onClick={(e) => handleAssetClick(asset, e)} onDoubleClick={() => handleAssetDoubleClick(asset)} - onContextMenu={(e) => handleContextMenu(e, asset)} + onContextMenu={(e) => { + e.stopPropagation(); + handleContextMenu(e, asset); + }} draggable={asset.type === 'file'} onDragStart={(e) => { if (asset.type === 'file') { diff --git a/packages/editor-app/src/components/ContextMenu.tsx b/packages/editor-app/src/components/ContextMenu.tsx index 1ac4c72b..31ee18b7 100644 --- a/packages/editor-app/src/components/ContextMenu.tsx +++ b/packages/editor-app/src/components/ContextMenu.tsx @@ -1,4 +1,5 @@ -import { useEffect, useRef, useState } from 'react'; +import { useEffect, useRef, useState, useCallback } from 'react'; +import { ChevronRight } from 'lucide-react'; import '../styles/ContextMenu.css'; export interface ContextMenuItem { @@ -7,6 +8,10 @@ export interface ContextMenuItem { onClick: () => void; disabled?: boolean; separator?: boolean; + /** 快捷键提示文本 */ + shortcut?: string; + /** 子菜单项 */ + children?: ContextMenuItem[]; } interface ContextMenuProps { @@ -15,9 +20,113 @@ interface ContextMenuProps { onClose: () => void; } +interface SubMenuProps { + items: ContextMenuItem[]; + parentRect: DOMRect; + onClose: () => void; +} + +/** + * 子菜单组件 + */ +function SubMenu({ items, parentRect, onClose }: SubMenuProps) { + const menuRef = useRef(null); + const [position, setPosition] = useState({ x: 0, y: 0 }); + const [activeSubmenuIndex, setActiveSubmenuIndex] = useState(null); + const [submenuRect, setSubmenuRect] = useState(null); + + useEffect(() => { + if (menuRef.current) { + const menu = menuRef.current; + const rect = menu.getBoundingClientRect(); + const viewportWidth = window.innerWidth; + const viewportHeight = window.innerHeight; + + // 默认在父菜单右侧显示 + let x = parentRect.right; + let y = parentRect.top; + + // 如果右侧空间不足,显示在左侧 + if (x + rect.width > viewportWidth) { + x = parentRect.left - rect.width; + } + + // 如果底部空间不足,向上调整 + if (y + rect.height > viewportHeight) { + y = Math.max(0, viewportHeight - rect.height - 10); + } + + setPosition({ x, y }); + } + }, [parentRect]); + + const handleItemMouseEnter = useCallback((index: number, item: ContextMenuItem, e: React.MouseEvent) => { + if (item.children && item.children.length > 0) { + setActiveSubmenuIndex(index); + const itemRect = (e.currentTarget as HTMLElement).getBoundingClientRect(); + setSubmenuRect(itemRect); + } else { + setActiveSubmenuIndex(null); + setSubmenuRect(null); + } + }, []); + + return ( +
+ {items.map((item, index) => { + if (item.separator) { + return
; + } + + const hasChildren = item.children && item.children.length > 0; + + return ( +
{ + if (!item.disabled && !hasChildren) { + item.onClick(); + onClose(); + } + }} + onMouseEnter={(e) => handleItemMouseEnter(index, item, e)} + onMouseLeave={() => { + if (!item.children) { + setActiveSubmenuIndex(null); + } + }} + > + {item.icon && {item.icon}} + {item.label} + {item.shortcut && {item.shortcut}} + {hasChildren && } + {activeSubmenuIndex === index && submenuRect && item.children && ( + + )} +
+ ); + })} +
+ ); +} + export function ContextMenu({ items, position, onClose }: ContextMenuProps) { const menuRef = useRef(null); const [adjustedPosition, setAdjustedPosition] = useState(position); + const [activeSubmenuIndex, setActiveSubmenuIndex] = useState(null); + const [submenuRect, setSubmenuRect] = useState(null); useEffect(() => { if (menuRef.current) { @@ -65,6 +174,17 @@ export function ContextMenu({ items, position, onClose }: ContextMenuProps) { }; }, [onClose]); + const handleItemMouseEnter = useCallback((index: number, item: ContextMenuItem, e: React.MouseEvent) => { + if (item.children && item.children.length > 0) { + setActiveSubmenuIndex(index); + const itemRect = (e.currentTarget as HTMLElement).getBoundingClientRect(); + setSubmenuRect(itemRect); + } else { + setActiveSubmenuIndex(null); + setSubmenuRect(null); + } + }, []); + return (
; } + const hasChildren = item.children && item.children.length > 0; + return (
{ - if (!item.disabled) { + if (!item.disabled && !hasChildren) { item.onClick(); onClose(); } }} + onMouseEnter={(e) => handleItemMouseEnter(index, item, e)} + onMouseLeave={() => { + if (!item.children) { + setActiveSubmenuIndex(null); + } + }} > {item.icon && {item.icon}} {item.label} + {item.shortcut && {item.shortcut}} + {hasChildren && } + {activeSubmenuIndex === index && submenuRect && item.children && ( + + )}
); })} diff --git a/packages/editor-app/src/components/FileTree.tsx b/packages/editor-app/src/components/FileTree.tsx index 03395fee..74c589fd 100644 --- a/packages/editor-app/src/components/FileTree.tsx +++ b/packages/editor-app/src/components/FileTree.tsx @@ -1,6 +1,9 @@ import { useState, useEffect, forwardRef, useImperativeHandle } from 'react'; import * as LucideIcons from 'lucide-react'; -import { Folder, ChevronRight, ChevronDown, File, Edit3, Trash2, FolderOpen, Copy, FileText, FolderPlus, Plus } from 'lucide-react'; +import { + Folder, ChevronRight, ChevronDown, File, Edit3, Trash2, FolderOpen, Copy, FileText, FolderPlus, Plus, + Save, Tag, Link, FileSearch, Globe, Package, Clipboard, RefreshCw, Settings +} from 'lucide-react'; import { TauriAPI, DirectoryEntry } from '../api/tauri'; import { MessageHub, FileActionRegistry } from '@esengine/editor-core'; import { Core } from '@esengine/ecs-framework'; @@ -614,25 +617,187 @@ export const FileTree = forwardRef(({ rootPath, o } } } + + items.push({ label: '', separator: true, onClick: () => {} }); + + // 文件操作菜单项 + items.push({ + label: '保存', + icon: , + shortcut: 'Ctrl+S', + onClick: () => { + // TODO: 实现保存功能 + console.log('Save file:', node.path); + } + }); } items.push({ label: '重命名', icon: , + shortcut: 'F2', onClick: () => { setRenamingNode(node.path); setNewName(node.name); } }); + items.push({ + label: '批量重命名', + icon: , + shortcut: 'Shift+F2', + disabled: true, // TODO: 实现批量重命名 + onClick: () => { + console.log('Batch rename'); + } + }); + + items.push({ + label: '复制', + icon: , + shortcut: 'Ctrl+D', + onClick: () => { + // TODO: 实现复制功能 + console.log('Duplicate:', node.path); + } + }); + items.push({ label: '删除', icon: , + shortcut: 'Delete', onClick: () => handleDeleteClick(node) }); items.push({ label: '', separator: true, onClick: () => {} }); + // 资产操作子菜单 + items.push({ + label: '资产操作', + icon: , + onClick: () => {}, + children: [ + { + label: '重新导入', + icon: , + onClick: () => { + console.log('Reimport asset:', node.path); + } + }, + { + label: '导出...', + icon: , + onClick: () => { + console.log('Export asset:', node.path); + } + }, + { label: '', separator: true, onClick: () => {} }, + { + label: '迁移资产', + icon: , + onClick: () => { + console.log('Migrate asset:', node.path); + } + } + ] + }); + + // 资产本地化子菜单 + items.push({ + label: '资产本地化', + icon: , + onClick: () => {}, + children: [ + { + label: '创建本地化资产', + onClick: () => { + console.log('Create localized asset:', node.path); + } + }, + { + label: '导入翻译', + onClick: () => { + console.log('Import translation:', node.path); + } + }, + { + label: '导出翻译', + onClick: () => { + console.log('Export translation:', node.path); + } + } + ] + }); + + items.push({ label: '', separator: true, onClick: () => {} }); + + // 标签和引用 + items.push({ + label: '管理标签', + icon: , + shortcut: 'Ctrl+T', + onClick: () => { + console.log('Manage tags:', node.path); + } + }); + + items.push({ label: '', separator: true, onClick: () => {} }); + + // 路径复制选项 + items.push({ + label: '复制引用', + icon: , + shortcut: 'Ctrl+C', + onClick: () => { + navigator.clipboard.writeText(node.path); + } + }); + + items.push({ + label: '拷贝Object路径', + icon: , + shortcut: 'Ctrl+Shift+C', + onClick: () => { + // 生成对象路径格式 + const objectPath = node.path.replace(/\\/g, '/'); + navigator.clipboard.writeText(objectPath); + } + }); + + items.push({ + label: '拷贝包路径', + icon: , + shortcut: 'Ctrl+Alt+C', + onClick: () => { + // 生成包路径格式 + const packagePath = '/' + node.path.replace(/\\/g, '/').split('/').slice(-2).join('/'); + navigator.clipboard.writeText(packagePath); + } + }); + + items.push({ label: '', separator: true, onClick: () => {} }); + + // 引用查看器 + items.push({ + label: '引用查看器', + icon: , + shortcut: 'Alt+Shift+R', + onClick: () => { + console.log('Open reference viewer:', node.path); + } + }); + + items.push({ + label: '尺寸信息图', + icon: , + shortcut: 'Alt+Shift+D', + onClick: () => { + console.log('Show size map:', node.path); + } + }); + + items.push({ label: '', separator: true, onClick: () => {} }); + if (node.type === 'folder') { items.push({ label: '新建文件', @@ -675,14 +840,6 @@ export const FileTree = forwardRef(({ rootPath, o } }); - items.push({ - label: '复制路径', - icon: , - onClick: () => { - navigator.clipboard.writeText(node.path); - } - }); - return items; }; diff --git a/packages/editor-app/src/components/OutputLogPanel.tsx b/packages/editor-app/src/components/OutputLogPanel.tsx index 71bfc910..a3b22e74 100644 --- a/packages/editor-app/src/components/OutputLogPanel.tsx +++ b/packages/editor-app/src/components/OutputLogPanel.tsx @@ -3,9 +3,8 @@ import { LogService, LogEntry } from '@esengine/editor-core'; import { LogLevel } from '@esengine/ecs-framework'; import { Search, Filter, Settings, X, Trash2, ChevronDown, - Bug, Info, AlertTriangle, XCircle, AlertCircle, Wifi, Pause, Play + Bug, Info, AlertTriangle, XCircle, AlertCircle, Wifi, Pause, Play, Copy } from 'lucide-react'; -import { JsonViewer } from './JsonViewer'; import '../styles/OutputLogPanel.css'; interface OutputLogPanelProps { @@ -16,15 +15,6 @@ interface OutputLogPanelProps { const MAX_LOGS = 1000; -function tryParseJSON(message: string): { isJSON: boolean; parsed?: unknown } { - try { - const parsed: unknown = JSON.parse(message); - return { isJSON: true, parsed }; - } catch { - return { isJSON: false }; - } -} - function formatTime(date: Date): string { const hours = date.getHours().toString().padStart(2, '0'); const minutes = date.getMinutes().toString().padStart(2, '0'); @@ -33,103 +23,121 @@ function formatTime(date: Date): string { return `${hours}:${minutes}:${seconds}.${ms}`; } -function getLevelIcon(level: LogLevel) { +function getLevelIcon(level: LogLevel, size: number = 14) { switch (level) { case LogLevel.Debug: - return ; + return ; case LogLevel.Info: - return ; + return ; case LogLevel.Warn: - return ; + return ; case LogLevel.Error: case LogLevel.Fatal: - return ; + return ; default: - return ; + return ; } } function getLevelClass(level: LogLevel): string { switch (level) { case LogLevel.Debug: - return 'log-entry-debug'; + return 'output-log-entry-debug'; case LogLevel.Info: - return 'log-entry-info'; + return 'output-log-entry-info'; case LogLevel.Warn: - return 'log-entry-warn'; + return 'output-log-entry-warn'; case LogLevel.Error: case LogLevel.Fatal: - return 'log-entry-error'; + return 'output-log-entry-error'; default: return ''; } } -const LogEntryItem = memo(({ log, onOpenJsonViewer }: { +/** + * 尝试从消息中提取堆栈信息 + */ +function extractStackTrace(message: string): { message: string; stack: string | null } { + const stackPattern = /\n\s*at\s+/; + if (stackPattern.test(message)) { + const lines = message.split('\n'); + const messageLines: string[] = []; + const stackLines: string[] = []; + let inStack = false; + + for (const line of lines) { + if (line.trim().startsWith('at ') || inStack) { + inStack = true; + stackLines.push(line); + } else { + messageLines.push(line); + } + } + + return { + message: messageLines.join('\n').trim(), + stack: stackLines.length > 0 ? stackLines.join('\n') : null + }; + } + + return { message, stack: null }; +} + +const LogEntryItem = memo(({ log, isExpanded, onToggle, onCopy }: { log: LogEntry; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - onOpenJsonViewer: (data: any) => void; + isExpanded: boolean; + onToggle: () => void; + onCopy: () => void; }) => { - const { isJSON, parsed } = useMemo(() => tryParseJSON(log.message), [log.message]); - const shouldTruncate = log.message.length > 200; - const [isExpanded, setIsExpanded] = useState(false); + // 优先使用 log.stack,否则尝试从 message 中提取 + const { message, stack } = useMemo(() => { + if (log.stack) { + return { message: log.message, stack: log.stack }; + } + return extractStackTrace(log.message); + }, [log.message, log.stack]); + + const hasStack = !!stack; return ( -
-
- {getLevelIcon(log.level)} +
+
+
+ {getLevelIcon(log.level)} +
+
+ {formatTime(log.timestamp)} +
+
+ [{log.source === 'remote' ? 'Remote' : log.source}] +
+
+ {message} +
+
-
- {formatTime(log.timestamp)} -
-
- [{log.source === 'remote' ? '🌐 Remote' : log.source}] -
- {log.clientId && ( -
- {log.clientId} + {isExpanded && stack && ( +
+
调用堆栈:
+ {stack.split('\n').filter(line => line.trim()).map((line, index) => ( +
+ {line} +
+ ))}
)} -
-
-
- {shouldTruncate && !isExpanded ? ( - <> - - {log.message.substring(0, 200)}... - - - - ) : ( - <> - {log.message} - {shouldTruncate && ( - - )} - - )} -
- {isJSON && parsed !== undefined && ( - - )} -
-
); }); @@ -150,10 +158,7 @@ export function OutputLogPanel({ logService, locale = 'en', onClose }: OutputLog const [autoScroll, setAutoScroll] = useState(true); const [showFilterMenu, setShowFilterMenu] = useState(false); const [showSettingsMenu, setShowSettingsMenu] = useState(false); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const [jsonViewerData, setJsonViewerData] = useState(null); - const [showTimestamp, setShowTimestamp] = useState(true); - const [showSource, setShowSource] = useState(true); + const [expandedLogIds, setExpandedLogIds] = useState>(new Set()); const logContainerRef = useRef(null); const filterMenuRef = useRef(null); const settingsMenuRef = useRef(null); @@ -174,7 +179,6 @@ export function OutputLogPanel({ logService, locale = 'en', onClose }: OutputLog } }, [logs, autoScroll]); - // Close menus on outside click useEffect(() => { const handleClickOutside = (e: MouseEvent) => { if (filterMenuRef.current && !filterMenuRef.current.contains(e.target as Node)) { @@ -199,6 +203,7 @@ export function OutputLogPanel({ logService, locale = 'en', onClose }: OutputLog const handleClear = useCallback(() => { logService.clear(); setLogs([]); + setExpandedLogIds(new Set()); }, [logService]); const toggleLevelFilter = useCallback((level: LogLevel) => { @@ -213,6 +218,22 @@ export function OutputLogPanel({ logService, locale = 'en', onClose }: OutputLog }); }, []); + const toggleLogExpanded = useCallback((logId: string) => { + setExpandedLogIds(prev => { + const newSet = new Set(prev); + if (newSet.has(logId)) { + newSet.delete(logId); + } else { + newSet.add(logId); + } + return newSet; + }); + }, []); + + const handleCopyLog = useCallback((log: LogEntry) => { + navigator.clipboard.writeText(log.message); + }, []); + const filteredLogs = useMemo(() => { return logs.filter((log) => { if (!levelFilter.has(log.level)) return false; @@ -376,26 +397,6 @@ export function OutputLogPanel({ logService, locale = 'en', onClose }: OutputLog {showSettingsMenu && (
-
- {locale === 'zh' ? '显示选项' : 'Display Options'} -
- - -
)}
- - {/* JSON Viewer Modal */} - {jsonViewerData && ( - setJsonViewerData(null)} - /> - )}
); } diff --git a/packages/editor-app/src/components/PluginListSetting.tsx b/packages/editor-app/src/components/PluginListSetting.tsx index f4a2ec2c..5b8d3ccc 100644 --- a/packages/editor-app/src/components/PluginListSetting.tsx +++ b/packages/editor-app/src/components/PluginListSetting.tsx @@ -11,8 +11,8 @@ import { useState, useEffect } from 'react'; import { Core } from '@esengine/ecs-framework'; -import { PluginManager, type RegisteredPlugin, type PluginCategory } from '@esengine/editor-core'; -import { Check, Lock, RefreshCw, Package } from 'lucide-react'; +import { PluginManager, type RegisteredPlugin, type PluginCategory, ProjectService } from '@esengine/editor-core'; +import { Check, Lock, Package } from 'lucide-react'; import { NotificationService } from '../services/NotificationService'; import '../styles/PluginListSetting.css'; @@ -30,14 +30,14 @@ const categoryLabels: Record = { networking: { zh: '网络', en: 'Networking' }, tools: { zh: '工具', en: 'Tools' }, scripting: { zh: '脚本', en: 'Scripting' }, - content: { zh: '内容', en: 'Content' } + content: { zh: '内容', en: 'Content' }, + tilemap: { zh: '瓦片地图', en: 'Tilemap' } }; -const categoryOrder: PluginCategory[] = ['core', 'rendering', 'ui', 'ai', 'scripting', 'physics', 'audio', 'networking', 'tools', 'content']; +const categoryOrder: PluginCategory[] = ['core', 'rendering', 'ui', 'ai', 'scripting', 'physics', 'audio', 'networking', 'tilemap', 'tools', 'content']; export function PluginListSetting({ pluginManager }: PluginListSettingProps) { const [plugins, setPlugins] = useState([]); - const [pendingChanges, setPendingChanges] = useState>(new Map()); useEffect(() => { loadPlugins(); @@ -55,11 +55,11 @@ export function PluginListSetting({ pluginManager }: PluginListSettingProps) { } }; - const handleToggle = (pluginId: string) => { - const plugin = plugins.find(p => p.loader.descriptor.id === pluginId); + const handleToggle = async (pluginId: string) => { + const plugin = plugins.find(p => p.plugin.descriptor.id === pluginId); if (!plugin) return; - const descriptor = plugin.loader.descriptor; + const descriptor = plugin.plugin.descriptor; // 核心插件不可禁用 if (descriptor.isCore) { @@ -69,11 +69,11 @@ export function PluginListSetting({ pluginManager }: PluginListSettingProps) { const newEnabled = !plugin.enabled; - // 检查依赖 + // 检查依赖(启用时) if (newEnabled) { const deps = descriptor.dependencies || []; const missingDeps = deps.filter(dep => { - const depPlugin = plugins.find(p => p.loader.descriptor.id === dep.id); + const depPlugin = plugins.find(p => p.plugin.descriptor.id === dep.id); return depPlugin && !depPlugin.enabled; }); @@ -81,44 +81,76 @@ export function PluginListSetting({ pluginManager }: PluginListSettingProps) { showWarning(`需要先启用依赖插件: ${missingDeps.map(d => d.id).join(', ')}`); return; } - } else { - // 检查是否有其他插件依赖此插件 - const dependents = plugins.filter(p => { - if (!p.enabled || p.loader.descriptor.id === pluginId) return false; - const deps = p.loader.descriptor.dependencies || []; - return deps.some(d => d.id === pluginId); - }); - - if (dependents.length > 0) { - showWarning(`以下插件依赖此插件: ${dependents.map(p => p.loader.descriptor.name).join(', ')}`); - return; - } } - // 记录待处理的更改 - const newPendingChanges = new Map(pendingChanges); - newPendingChanges.set(pluginId, newEnabled); - setPendingChanges(newPendingChanges); + // 调用 PluginManager 的动态启用/禁用方法 + console.log(`[PluginListSetting] ${newEnabled ? 'Enabling' : 'Disabling'} plugin: ${pluginId}`); + let success: boolean; + if (newEnabled) { + success = await pluginManager.enable(pluginId); + } else { + success = await pluginManager.disable(pluginId); + } + console.log(`[PluginListSetting] ${newEnabled ? 'Enable' : 'Disable'} result: ${success}`); + + if (!success) { + showWarning(newEnabled ? '启用插件失败' : '禁用插件失败'); + return; + } // 更新本地状态 setPlugins(plugins.map(p => { - if (p.loader.descriptor.id === pluginId) { + if (p.plugin.descriptor.id === pluginId) { return { ...p, enabled: newEnabled }; } return p; })); - // 调用 PluginManager 的启用/禁用方法 - if (newEnabled) { - pluginManager.enable(pluginId); - } else { - pluginManager.disable(pluginId); + // 保存到项目配置 + savePluginConfigToProject(); + + // 通知用户(如果有编辑器模块变更) + const hasEditorModule = !!plugin.plugin.editorModule; + if (hasEditorModule) { + const notificationService = Core.services.tryResolve(NotificationService) as NotificationService | null; + if (notificationService) { + notificationService.show( + newEnabled ? `已启用插件: ${descriptor.name}` : `已禁用插件: ${descriptor.name}`, + 'success', + 2000 + ); + } + } + }; + + /** + * 保存插件配置到项目文件 + */ + const savePluginConfigToProject = async () => { + const projectService = Core.services.tryResolve(ProjectService); + if (!projectService || !projectService.isProjectOpen()) { + console.warn('[PluginListSetting] Cannot save: project not open'); + return; + } + + // 获取当前启用的插件列表(排除核心插件) + const enabledPlugins = pluginManager.getEnabledPlugins() + .filter(p => !p.plugin.descriptor.isCore) + .map(p => p.plugin.descriptor.id); + + console.log('[PluginListSetting] Saving enabled plugins:', enabledPlugins); + + try { + await projectService.setEnabledPlugins(enabledPlugins); + console.log('[PluginListSetting] Plugin config saved successfully'); + } catch (error) { + console.error('[PluginListSetting] Failed to save plugin config:', error); } }; // 按类别分组并排序 const groupedPlugins = plugins.reduce((acc, plugin) => { - const category = plugin.loader.descriptor.category; + const category = plugin.plugin.descriptor.category; if (!acc[category]) { acc[category] = []; } @@ -131,13 +163,6 @@ export function PluginListSetting({ pluginManager }: PluginListSettingProps) { return (
- {pendingChanges.size > 0 && ( -
- - 部分更改需要重启编辑器后生效 -
- )} - {sortedCategories.map(category => (
@@ -145,9 +170,9 @@ export function PluginListSetting({ pluginManager }: PluginListSettingProps) {
{groupedPlugins[category].map(plugin => { - const descriptor = plugin.loader.descriptor; - const hasRuntime = !!plugin.loader.runtimeModule; - const hasEditor = !!plugin.loader.editorModule; + const descriptor = plugin.plugin.descriptor; + const hasRuntime = !!plugin.plugin.runtimeModule; + const hasEditor = !!plugin.plugin.editorModule; return (
{ const settings = SettingsService.getInstance(); - setServerPort(settings.get('profiler.port', '8080')); + const savedPort = settings.get('profiler.port', 8080); + console.log('[PortManager] Initial port from settings:', savedPort); + setServerPort(String(savedPort)); const handleSettingsChange = ((event: CustomEvent) => { + console.log('[PortManager] settings:changed event received:', event.detail); const newPort = event.detail['profiler.port']; - if (newPort) { - setServerPort(newPort); + if (newPort !== undefined) { + console.log('[PortManager] Updating port to:', newPort); + setServerPort(String(newPort)); } }) as EventListener; diff --git a/packages/editor-app/src/components/SceneHierarchy.tsx b/packages/editor-app/src/components/SceneHierarchy.tsx index 10bc5c28..706acd00 100644 --- a/packages/editor-app/src/components/SceneHierarchy.tsx +++ b/packages/editor-app/src/components/SceneHierarchy.tsx @@ -1,16 +1,16 @@ -import { useState, useEffect, useRef } from 'react'; -import { Entity, Core } from '@esengine/ecs-framework'; +import { useState, useEffect, useRef, useCallback, useMemo } from 'react'; +import { Entity, Core, HierarchySystem, HierarchyComponent, EntityTags, isFolder } from '@esengine/ecs-framework'; import { EntityStoreService, MessageHub, SceneManagerService, CommandManager, EntityCreationRegistry, EntityCreationTemplate } from '@esengine/editor-core'; import { useLocale } from '../hooks/useLocale'; import * as LucideIcons from 'lucide-react'; import { Box, Wifi, Search, Plus, Trash2, Monitor, Globe, ChevronRight, ChevronDown, Eye, Star, Lock, Settings, Filter, Folder, Sun, Cloud, Mountain, Flag, - SquareStack + SquareStack, FolderPlus } from 'lucide-react'; import { ProfilerService, RemoteEntity } from '../services/ProfilerService'; import { confirm } from '@tauri-apps/plugin-dialog'; -import { CreateEntityCommand, DeleteEntityCommand } from '../application/commands/entity'; +import { CreateEntityCommand, DeleteEntityCommand, ReparentEntityCommand, DropPosition } from '../application/commands/entity'; import '../styles/SceneHierarchy.css'; function getIconComponent(iconName: string | undefined, size: number = 14): React.ReactNode { @@ -61,8 +61,19 @@ interface SceneHierarchyProps { interface EntityNode { entity: Entity; children: EntityNode[]; - isExpanded: boolean; depth: number; + bIsFolder: boolean; + hasChildren: boolean; +} + +/** + * 拖放指示器位置 + */ +enum DropIndicator { + NONE = 'none', + BEFORE = 'before', + INSIDE = 'inside', + AFTER = 'after' } export function SceneHierarchy({ entityStore, messageHub, commandManager, isProfilerMode = false }: SceneHierarchyProps) { @@ -78,9 +89,9 @@ export function SceneHierarchy({ entityStore, messageHub, commandManager, isProf const [isSceneModified, setIsSceneModified] = useState(false); const [contextMenu, setContextMenu] = useState<{ x: number; y: number; entityId: number | null } | null>(null); const [draggedEntityId, setDraggedEntityId] = useState(null); - const [dropTargetIndex, setDropTargetIndex] = useState(null); + const [dropTarget, setDropTarget] = useState<{ entityId: number; indicator: DropIndicator } | null>(null); const [pluginTemplates, setPluginTemplates] = useState([]); - const [expandedFolders, setExpandedFolders] = useState>(new Set()); + const [expandedIds, setExpandedIds] = useState>(new Set([-1])); // -1 is scene root const [sortColumn, setSortColumn] = useState('name'); const [sortDirection, setSortDirection] = useState('asc'); const [showFilterMenu, setShowFilterMenu] = useState(false); @@ -89,6 +100,68 @@ export function SceneHierarchy({ entityStore, messageHub, commandManager, isProf const isShowingRemote = viewMode === 'remote' && isRemoteConnected; const selectedId = selectedIds.size > 0 ? Array.from(selectedIds)[0] : null; + /** + * 构建层级树结构 + */ + const buildEntityTree = useCallback((rootEntities: Entity[]): EntityNode[] => { + const scene = Core.scene; + if (!scene) return []; + + const buildNode = (entity: Entity, depth: number): EntityNode => { + const hierarchy = entity.getComponent(HierarchyComponent); + const childIds = hierarchy?.childIds ?? []; + const bIsEntityFolder = isFolder(entity.tag); + + const children: EntityNode[] = []; + for (const childId of childIds) { + const childEntity = scene.findEntityById(childId); + if (childEntity) { + children.push(buildNode(childEntity, depth + 1)); + } + } + + return { + entity, + children, + depth, + bIsFolder: bIsEntityFolder, + hasChildren: children.length > 0 + }; + }; + + return rootEntities.map((entity) => buildNode(entity, 1)); + }, []); + + /** + * 扁平化树为带深度信息的列表(用于渲染) + */ + const flattenTree = useCallback((nodes: EntityNode[], expandedSet: Set): EntityNode[] => { + const result: EntityNode[] = []; + + const traverse = (nodeList: EntityNode[]) => { + for (const node of nodeList) { + result.push(node); + + const bIsExpanded = expandedSet.has(node.entity.id); + if (bIsExpanded && node.children.length > 0) { + traverse(node.children); + } + } + }; + + traverse(nodes); + return result; + }, []); + + /** + * 层级树和扁平化列表 + */ + const entityTree = useMemo(() => buildEntityTree(entities), [entities, buildEntityTree]); + const flattenedEntities = useMemo( + () => expandedIds.has(-1) ? flattenTree(entityTree, expandedIds) : [], + [entityTree, expandedIds, flattenTree] + ); + // Get entity creation templates from plugins useEffect(() => { const updateTemplates = () => { @@ -171,7 +244,9 @@ export function SceneHierarchy({ entityStore, messageHub, commandManager, isProf const unsubSelect = messageHub.subscribe('entity:selected', handleSelection); const unsubSceneLoaded = messageHub.subscribe('scene:loaded', updateEntities); const unsubSceneNew = messageHub.subscribe('scene:new', updateEntities); + const unsubSceneRestored = messageHub.subscribe('scene:restored', updateEntities); const unsubReordered = messageHub.subscribe('entity:reordered', updateEntities); + const unsubReparented = messageHub.subscribe('entity:reparented', updateEntities); return () => { unsubAdd(); @@ -180,7 +255,9 @@ export function SceneHierarchy({ entityStore, messageHub, commandManager, isProf unsubSelect(); unsubSceneLoaded(); unsubSceneNew(); + unsubSceneRestored(); unsubReordered(); + unsubReparented(); }; }, [entityStore, messageHub]); @@ -258,35 +335,110 @@ export function SceneHierarchy({ entityStore, messageHub, commandManager, isProf } }; - const handleDragStart = (e: React.DragEvent, entityId: number) => { + const handleDragStart = useCallback((e: React.DragEvent, entityId: number) => { setDraggedEntityId(entityId); e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.setData('text/plain', entityId.toString()); - }; + }, []); - const handleDragOver = (e: React.DragEvent, index: number) => { + /** + * 根据鼠标位置计算拖放指示器位置 + * 上20%区域 = BEFORE, 中间60% = INSIDE, 下20% = AFTER + * 所有实体都支持作为父节点接收子节点 + */ + const calculateDropIndicator = useCallback((e: React.DragEvent, _targetNode: EntityNode): DropIndicator => { + const rect = (e.currentTarget as HTMLElement).getBoundingClientRect(); + const y = e.clientY - rect.top; + const height = rect.height; + + if (y < height * 0.2) { + return DropIndicator.BEFORE; + } else if (y > height * 0.8) { + return DropIndicator.AFTER; + } else { + return DropIndicator.INSIDE; + } + }, []); + + const handleDragOver = useCallback((e: React.DragEvent, targetNode: EntityNode) => { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; - setDropTargetIndex(index); - }; - const handleDragLeave = () => { - setDropTargetIndex(null); - }; - - const handleDrop = (e: React.DragEvent, targetIndex: number) => { - e.preventDefault(); - if (draggedEntityId !== null) { - entityStore.reorderEntity(draggedEntityId, targetIndex); + // 不能拖放到自己 + if (draggedEntityId === targetNode.entity.id) { + setDropTarget(null); + return; } - setDraggedEntityId(null); - setDropTargetIndex(null); - }; - const handleDragEnd = () => { + // 检查是否拖到自己的子节点 + const scene = Core.scene; + if (scene && draggedEntityId !== null) { + const hierarchySystem = scene.getSystem(HierarchySystem); + const draggedEntity = scene.findEntityById(draggedEntityId); + if (draggedEntity && hierarchySystem?.isAncestorOf(draggedEntity, targetNode.entity)) { + setDropTarget(null); + return; + } + } + + const indicator = calculateDropIndicator(e, targetNode); + setDropTarget({ entityId: targetNode.entity.id, indicator }); + }, [draggedEntityId, calculateDropIndicator]); + + const handleDragLeave = useCallback(() => { + setDropTarget(null); + }, []); + + const handleDrop = useCallback((e: React.DragEvent, targetNode: EntityNode) => { + e.preventDefault(); + + if (draggedEntityId === null || !dropTarget) { + setDraggedEntityId(null); + setDropTarget(null); + return; + } + + const scene = Core.scene; + if (!scene) return; + + const draggedEntity = scene.findEntityById(draggedEntityId); + if (!draggedEntity) return; + + // 转换 DropIndicator 到 DropPosition + let dropPosition: DropPosition; + switch (dropTarget.indicator) { + case DropIndicator.BEFORE: + dropPosition = DropPosition.BEFORE; + break; + case DropIndicator.INSIDE: + dropPosition = DropPosition.INSIDE; + // 自动展开目标节点 + setExpandedIds(prev => new Set([...prev, targetNode.entity.id])); + break; + case DropIndicator.AFTER: + dropPosition = DropPosition.AFTER; + break; + default: + dropPosition = DropPosition.AFTER; + } + + const command = new ReparentEntityCommand( + entityStore, + messageHub, + draggedEntity, + targetNode.entity, + dropPosition + ); + commandManager.execute(command); + setDraggedEntityId(null); - setDropTargetIndex(null); - }; + setDropTarget(null); + }, [draggedEntityId, dropTarget, entityStore, messageHub, commandManager]); + + const handleDragEnd = useCallback(() => { + setDraggedEntityId(null); + setDropTarget(null); + }, []); const handleRemoteEntityClick = (entity: RemoteEntity) => { setSelectedIds(new Set([entity.id])); @@ -373,8 +525,8 @@ export function SceneHierarchy({ entityStore, messageHub, commandManager, isProf return () => window.removeEventListener('keydown', handleKeyDown); }, [selectedId, isShowingRemote]); - const toggleFolderExpand = (entityId: number) => { - setExpandedFolders(prev => { + const toggleExpand = useCallback((entityId: number) => { + setExpandedIds(prev => { const next = new Set(prev); if (next.has(entityId)) { next.delete(entityId); @@ -383,7 +535,29 @@ export function SceneHierarchy({ entityStore, messageHub, commandManager, isProf } return next; }); - }; + }, []); + + /** + * 创建文件夹实体 + */ + const handleCreateFolder = useCallback(() => { + const entityCount = entityStore.getAllEntities().length; + const folderName = locale === 'zh' ? `文件夹 ${entityCount + 1}` : `Folder ${entityCount + 1}`; + + const scene = Core.scene; + if (!scene) return; + + const entity = scene.createEntity(folderName); + entity.tag = EntityTags.FOLDER; + + // 添加 HierarchyComponent 支持层级结构 + entity.addComponent(new HierarchyComponent()); + + entityStore.addEntity(entity); + entityStore.selectEntity(entity); + messageHub.publish('entity:added', { entity }); + messageHub.publish('scene:modified', {}); + }, [entityStore, messageHub, locale]); const handleSortClick = (column: SortColumn) => { if (sortColumn === column) { @@ -394,20 +568,33 @@ export function SceneHierarchy({ entityStore, messageHub, commandManager, isProf } }; - // Get entity type for display - const getEntityType = (entity: Entity): string => { + /** + * 获取实体类型显示名称 + */ + const getEntityType = useCallback((entity: Entity): string => { + if (isFolder(entity.tag)) { + return 'Folder'; + } + const components = entity.components || []; if (components.length > 0) { const firstComponent = components[0]; return firstComponent?.constructor?.name || 'Entity'; } return 'Entity'; - }; + }, []); - // Get icon for entity type - const getEntityIcon = (entityType: string): React.ReactNode => { + /** + * 获取实体类型图标 + */ + const getEntityIcon = useCallback((entity: Entity): React.ReactNode => { + if (isFolder(entity.tag)) { + return ; + } + + const entityType = getEntityType(entity); return entityTypeIcons[entityType] || ; - }; + }, [getEntityType]); // Filter entities based on search query const filterRemoteEntities = (entityList: RemoteEntity[]): RemoteEntity[] => { @@ -443,13 +630,10 @@ export function SceneHierarchy({ entityStore, messageHub, commandManager, isProf }); }; - const displayEntities = isShowingRemote - ? filterRemoteEntities(remoteEntities) - : filterLocalEntities(entities); const showRemoteIndicator = isShowingRemote && remoteEntities.length > 0; const displaySceneName = isShowingRemote && remoteSceneName ? remoteSceneName : sceneName; - const totalCount = displayEntities.length; + const totalCount = isShowingRemote ? remoteEntities.length : entityStore.getAllEntities().length; const selectedCount = selectedIds.size; return ( @@ -479,13 +663,22 @@ export function SceneHierarchy({ entityStore, messageHub, commandManager, isProf
{!isShowingRemote && ( - + <> + + + )} + + {sortedCategories.length > 0 &&
} {sortedCategories.map(([category, templates]) => ( diff --git a/packages/editor-app/src/components/SettingsWindow.tsx b/packages/editor-app/src/components/SettingsWindow.tsx index d31320dc..3e5a5739 100644 --- a/packages/editor-app/src/components/SettingsWindow.tsx +++ b/packages/editor-app/src/components/SettingsWindow.tsx @@ -148,9 +148,14 @@ export function SettingsWindow({ onClose, settingsRegistry, initialCategoryId }: } else { const value = settings.get(key, descriptor.defaultValue); initialValues.set(key, value); + if (key.startsWith('profiler.')) { + console.log(`[SettingsWindow] Loading ${key}: stored=${settings.get(key, undefined)}, default=${descriptor.defaultValue}, using=${value}`); + } } } + console.log('[SettingsWindow] Initial values for profiler:', + Array.from(initialValues.entries()).filter(([k]) => k.startsWith('profiler.'))); setValues(initialValues); }, [settingsRegistry, initialCategoryId]); @@ -162,10 +167,24 @@ export function SettingsWindow({ onClose, settingsRegistry, initialCategoryId }: const newErrors = new Map(errors); if (!settingsRegistry.validateSetting(descriptor, value)) { newErrors.set(key, descriptor.validator?.errorMessage || '无效值'); + setErrors(newErrors); + return; // 验证失败,不保存 } else { newErrors.delete(key); } setErrors(newErrors); + + // 实时保存设置 + const settings = SettingsService.getInstance(); + if (!key.startsWith('project.')) { + settings.set(key, value); + console.log(`[SettingsWindow] Saved ${key}:`, value); + + // 触发设置变更事件 + window.dispatchEvent(new CustomEvent('settings:changed', { + detail: { [key]: value } + })); + } }; const handleSave = async () => { @@ -208,6 +227,7 @@ export function SettingsWindow({ onClose, settingsRegistry, initialCategoryId }: await projectService.setUIDesignResolution({ width: newWidth, height: newHeight }); } + console.log('[SettingsWindow] Saving settings, changedSettings:', changedSettings); window.dispatchEvent(new CustomEvent('settings:changed', { detail: changedSettings })); diff --git a/packages/editor-app/src/components/StartupPage.tsx b/packages/editor-app/src/components/StartupPage.tsx index 4c3077ee..f39e1541 100644 --- a/packages/editor-app/src/components/StartupPage.tsx +++ b/packages/editor-app/src/components/StartupPage.tsx @@ -11,7 +11,6 @@ interface StartupPageProps { onOpenProject: () => void; onCreateProject: () => void; onOpenRecentProject?: (projectPath: string) => void; - onProfilerMode?: () => void; onLocaleChange?: (locale: Locale) => void; recentProjects?: string[]; locale: string; @@ -22,7 +21,7 @@ const LANGUAGES = [ { code: 'zh', name: '中文' } ]; -export function StartupPage({ onOpenProject, onCreateProject, onOpenRecentProject, onProfilerMode, onLocaleChange, recentProjects = [], locale }: StartupPageProps) { +export function StartupPage({ onOpenProject, onCreateProject, onOpenRecentProject, onLocaleChange, recentProjects = [], locale }: StartupPageProps) { const [showLogo, setShowLogo] = useState(true); const [hoveredProject, setHoveredProject] = useState(null); const [appVersion, setAppVersion] = useState(''); @@ -62,10 +61,8 @@ export function StartupPage({ onOpenProject, onCreateProject, onOpenRecentProjec subtitle: 'Professional Game Development Tool', openProject: 'Open Project', createProject: 'Create Project', - profilerMode: 'Profiler Mode', recentProjects: 'Recent Projects', noRecentProjects: 'No recent projects', - comingSoon: 'Coming Soon', updateAvailable: 'New version available', updateNow: 'Update Now', installing: 'Installing...', @@ -76,10 +73,8 @@ export function StartupPage({ onOpenProject, onCreateProject, onOpenRecentProjec subtitle: '专业游戏开发工具', openProject: '打开项目', createProject: '创建新项目', - profilerMode: '性能分析模式', recentProjects: '最近的项目', noRecentProjects: '没有最近的项目', - comingSoon: '即将推出', updateAvailable: '发现新版本', updateNow: '立即更新', installing: '正在安装...', @@ -126,13 +121,6 @@ export function StartupPage({ onOpenProject, onCreateProject, onOpenRecentProjec {t.createProject} - -
diff --git a/packages/editor-app/src/components/Viewport.tsx b/packages/editor-app/src/components/Viewport.tsx index 18b31c6c..32c04d95 100644 --- a/packages/editor-app/src/components/Viewport.tsx +++ b/packages/editor-app/src/components/Viewport.tsx @@ -8,8 +8,9 @@ import '../styles/Viewport.css'; import { useEngine } from '../hooks/useEngine'; import { EngineService } from '../services/EngineService'; import { Core, Entity, SceneSerializer } from '@esengine/ecs-framework'; -import { MessageHub } from '@esengine/editor-core'; -import { TransformComponent, CameraComponent } from '@esengine/ecs-components'; +import { MessageHub, ProjectService, AssetRegistryService } from '@esengine/editor-core'; +import { TransformComponent } from '@esengine/engine-core'; +import { CameraComponent } from '@esengine/camera'; import { UITransformComponent } from '@esengine/ui'; import { TauriAPI } from '../api/tauri'; import { open } from '@tauri-apps/plugin-shell'; @@ -59,7 +60,8 @@ function generateRuntimeHtml(): string { const runtime = ECSRuntime.create({ canvasId: 'runtime-canvas', width: window.innerWidth, - height: window.innerHeight + height: window.innerHeight, + projectConfigUrl: '/ecs-editor.config.json' }); await runtime.initialize(esEngine); @@ -354,11 +356,13 @@ export function Viewport({ locale = 'en', messageHub }: ViewportProps) { if (messageHubRef.current) { const propertyName = mode === 'move' ? 'position' : mode === 'rotate' ? 'rotation' : 'scale'; + const value = propertyName === 'position' ? transform.position : + propertyName === 'rotation' ? transform.rotation : transform.scale; messageHubRef.current.publish('component:property:changed', { entity, component: transform, propertyName, - value: transform[propertyName] + value }); } } @@ -373,16 +377,29 @@ export function Viewport({ locale = 'en', messageHub }: ViewportProps) { const rotationSpeed = 0.01; uiTransform.rotation += deltaX * rotationSpeed; } else if (mode === 'scale') { - const width = uiTransform.width * uiTransform.scaleX; - const height = uiTransform.height * uiTransform.scaleY; - const centerX = uiTransform.x + width * uiTransform.pivotX; - const centerY = uiTransform.y + height * uiTransform.pivotY; - const startDist = Math.sqrt((worldStart.x - centerX) ** 2 + (worldStart.y - centerY) ** 2); - const endDist = Math.sqrt((worldEnd.x - centerX) ** 2 + (worldEnd.y - centerY) ** 2); + const oldWidth = uiTransform.width * uiTransform.scaleX; + const oldHeight = uiTransform.height * uiTransform.scaleY; + + // pivot点的世界坐标(缩放前) + const pivotWorldX = uiTransform.x + oldWidth * uiTransform.pivotX; + const pivotWorldY = uiTransform.y + oldHeight * uiTransform.pivotY; + + const startDist = Math.sqrt((worldStart.x - pivotWorldX) ** 2 + (worldStart.y - pivotWorldY) ** 2); + const endDist = Math.sqrt((worldEnd.x - pivotWorldX) ** 2 + (worldEnd.y - pivotWorldY) ** 2); + if (startDist > 0) { const scaleFactor = endDist / startDist; - uiTransform.scaleX *= scaleFactor; - uiTransform.scaleY *= scaleFactor; + const newScaleX = uiTransform.scaleX * scaleFactor; + const newScaleY = uiTransform.scaleY * scaleFactor; + + const newWidth = uiTransform.width * newScaleX; + const newHeight = uiTransform.height * newScaleY; + + // 调整位置使pivot点保持不动 + uiTransform.x = pivotWorldX - newWidth * uiTransform.pivotX; + uiTransform.y = pivotWorldY - newHeight * uiTransform.pivotY; + uiTransform.scaleX = newScaleX; + uiTransform.scaleY = newScaleY; } } @@ -689,46 +706,118 @@ export function Viewport({ locale = 'en', messageHub }: ViewportProps) { // Write scene data and HTML (always update) await TauriAPI.writeFileContent(`${runtimeDir}/scene.json`, sceneData); - // Copy texture assets referenced in the scene - // 复制场景中引用的纹理资产 - const sceneObj = JSON.parse(sceneData); - const texturePathSet = new Set(); - - // Find all texture paths in sprite components - if (sceneObj.entities) { - for (const entity of sceneObj.entities) { - if (entity.components) { - for (const comp of entity.components) { - if (comp.type === 'Sprite' && comp.data?.texture) { - texturePathSet.add(comp.data.texture); - } - } - } + // Copy project config file (for plugin settings) + // 复制项目配置文件(用于插件设置) + const projectService = Core.services.tryResolve(ProjectService); + const projectPath = projectService?.getCurrentProject()?.path; + if (projectPath) { + const configPath = `${projectPath}\\ecs-editor.config.json`; + const configExists = await TauriAPI.pathExists(configPath); + if (configExists) { + await TauriAPI.copyFile(configPath, `${runtimeDir}\\ecs-editor.config.json`); + console.log('[Viewport] Copied project config to runtime dir'); } } - // Create assets directory and copy textures + // Create assets directory + // 创建资产目录 const assetsDir = `${runtimeDir}\\assets`; const assetsDirExists = await TauriAPI.pathExists(assetsDir); if (!assetsDirExists) { await TauriAPI.createDirectory(assetsDir); } - for (const texturePath of texturePathSet) { - if (texturePath && (texturePath.includes(':\\') || texturePath.startsWith('/'))) { - try { - const filename = texturePath.split(/[/\\]/).pop() || ''; - const destPath = `${assetsDir}\\${filename}`; - const exists = await TauriAPI.pathExists(texturePath); - if (exists) { - await TauriAPI.copyFile(texturePath, destPath); + // Collect all asset paths from scene + // 从场景中收集所有资产路径 + const sceneObj = JSON.parse(sceneData); + const assetPaths = new Set(); + + // Scan all components for asset references + if (sceneObj.entities) { + for (const entity of sceneObj.entities) { + if (entity.components) { + for (const comp of entity.components) { + // Sprite textures + if (comp.type === 'Sprite' && comp.data?.texture) { + assetPaths.add(comp.data.texture); + } + // Behavior tree assets + if (comp.type === 'BehaviorTreeRuntime' && comp.data?.treeAssetId) { + assetPaths.add(comp.data.treeAssetId); + } + // Tilemap assets + if (comp.type === 'Tilemap' && comp.data?.tmxPath) { + assetPaths.add(comp.data.tmxPath); + } + // Audio assets + if (comp.type === 'AudioSource' && comp.data?.clip) { + assetPaths.add(comp.data.clip); + } } - } catch (error) { - console.error(`Failed to copy texture ${texturePath}:`, error); } } } + // Build asset catalog and copy files + // 构建资产目录并复制文件 + const catalogEntries: Record = {}; + + for (const assetPath of assetPaths) { + if (!assetPath || (!assetPath.includes(':\\') && !assetPath.startsWith('/'))) continue; + + try { + const exists = await TauriAPI.pathExists(assetPath); + if (!exists) { + console.warn(`[Viewport] Asset not found: ${assetPath}`); + continue; + } + + // Get filename and determine relative path + const filename = assetPath.split(/[/\\]/).pop() || ''; + const destPath = `${assetsDir}\\${filename}`; + const relativePath = `assets/${filename}`; + + // Copy file + await TauriAPI.copyFile(assetPath, destPath); + + // Determine asset type from extension + const ext = filename.substring(filename.lastIndexOf('.')).toLowerCase(); + const typeMap: Record = { + '.png': 'texture', '.jpg': 'texture', '.jpeg': 'texture', '.webp': 'texture', + '.btree': 'btree', + '.tmx': 'tilemap', '.tsx': 'tileset', + '.mp3': 'audio', '.ogg': 'audio', '.wav': 'audio', + '.json': 'json' + }; + const assetType = typeMap[ext] || 'binary'; + + // Generate simple GUID based on path + const guid = assetPath.replace(/[^a-zA-Z0-9]/g, '-').substring(0, 36); + + catalogEntries[guid] = { + guid, + path: relativePath, + type: assetType, + size: 0, + hash: '' + }; + + console.log(`[Viewport] Copied asset: ${filename}`); + } catch (error) { + console.error(`[Viewport] Failed to copy asset ${assetPath}:`, error); + } + } + + // Write asset catalog + // 写入资产目录 + const assetCatalog = { + version: '1.0.0', + createdAt: Date.now(), + entries: catalogEntries + }; + await TauriAPI.writeFileContent(`${runtimeDir}/asset-catalog.json`, JSON.stringify(assetCatalog, null, 2)); + console.log(`[Viewport] Asset catalog created with ${Object.keys(catalogEntries).length} entries`); + const runtimeHtml = generateRuntimeHtml(); await TauriAPI.writeFileContent(`${runtimeDir}/index.html`, runtimeHtml); @@ -781,6 +870,19 @@ export function Viewport({ locale = 'en', messageHub }: ViewportProps) { await runtimeResolver.initialize(); await runtimeResolver.prepareRuntimeFiles(runtimeDir); + // Copy project config file (for plugin settings) + const projectService = Core.services.tryResolve(ProjectService); + if (projectService) { + const currentProject = projectService.getCurrentProject(); + if (currentProject?.path) { + const configPath = `${currentProject.path}\\ecs-editor.config.json`; + const configExists = await TauriAPI.pathExists(configPath); + if (configExists) { + await TauriAPI.copyFile(configPath, `${runtimeDir}\\ecs-editor.config.json`); + } + } + } + // Write scene data and HTML const sceneDataStr = typeof sceneData === 'string' ? sceneData : new TextDecoder().decode(sceneData); await TauriAPI.writeFileContent(`${runtimeDir}/scene.json`, sceneDataStr); diff --git a/packages/editor-app/src/components/inspectors/Inspector.tsx b/packages/editor-app/src/components/inspectors/Inspector.tsx index 1cae1c2d..75a3480e 100644 --- a/packages/editor-app/src/components/inspectors/Inspector.tsx +++ b/packages/editor-app/src/components/inspectors/Inspector.tsx @@ -124,7 +124,15 @@ export function Inspector({ entityStore: _entityStore, messageHub, inspectorRegi setComponentVersion((prev) => prev + 1); }; + const handleSceneRestored = () => { + // 场景恢复后,清除当前选中的实体(因为旧引用已无效) + // 用户需要重新选择实体 + setTarget(null); + setComponentVersion(0); + }; + const unsubEntitySelect = messageHub.subscribe('entity:selected', handleEntitySelection); + const unsubSceneRestored = messageHub.subscribe('scene:restored', handleSceneRestored); const unsubRemoteSelect = messageHub.subscribe('remote-entity:selected', handleRemoteEntitySelection); const unsubNodeSelect = messageHub.subscribe('behavior-tree:node-selected', handleExtensionSelection); const unsubAssetFileSelect = messageHub.subscribe('asset-file:selected', handleAssetFileSelection); @@ -136,6 +144,7 @@ export function Inspector({ entityStore: _entityStore, messageHub, inspectorRegi return () => { unsubEntitySelect(); + unsubSceneRestored(); unsubRemoteSelect(); unsubNodeSelect(); unsubAssetFileSelect(); diff --git a/packages/editor-app/src/components/inspectors/component-inspectors/TransformComponentInspector.tsx b/packages/editor-app/src/components/inspectors/component-inspectors/TransformComponentInspector.tsx index e93dedeb..68b84e11 100644 --- a/packages/editor-app/src/components/inspectors/component-inspectors/TransformComponentInspector.tsx +++ b/packages/editor-app/src/components/inspectors/component-inspectors/TransformComponentInspector.tsx @@ -1,7 +1,7 @@ import React, { useState, useEffect, useRef } from 'react'; import { Component } from '@esengine/ecs-framework'; import { IComponentInspector, ComponentInspectorContext } from '@esengine/editor-core'; -import { TransformComponent } from '@esengine/ecs-components'; +import { TransformComponent } from '@esengine/engine-core'; import { ChevronDown, Lock, Unlock } from 'lucide-react'; import '../../../styles/TransformInspector.css'; diff --git a/packages/editor-app/src/gizmos/SpriteGizmo.ts b/packages/editor-app/src/gizmos/SpriteGizmo.ts index a70e4959..aa906b6d 100644 --- a/packages/editor-app/src/gizmos/SpriteGizmo.ts +++ b/packages/editor-app/src/gizmos/SpriteGizmo.ts @@ -11,7 +11,8 @@ import type { Entity } from '@esengine/ecs-framework'; import type { IGizmoRenderData, IRectGizmoData, GizmoColor } from '@esengine/editor-core'; import { GizmoColors, GizmoRegistry } from '@esengine/editor-core'; -import { SpriteComponent, TransformComponent } from '@esengine/ecs-components'; +import { TransformComponent } from '@esengine/engine-core'; +import { SpriteComponent } from '@esengine/sprite'; /** * Gizmo provider function for SpriteComponent. diff --git a/packages/editor-app/src/infrastructure/field-editors/AnimationClipsFieldEditor.tsx b/packages/editor-app/src/infrastructure/field-editors/AnimationClipsFieldEditor.tsx index e4948b96..26460c4f 100644 --- a/packages/editor-app/src/infrastructure/field-editors/AnimationClipsFieldEditor.tsx +++ b/packages/editor-app/src/infrastructure/field-editors/AnimationClipsFieldEditor.tsx @@ -2,7 +2,7 @@ import React, { useState, useCallback, useEffect } from 'react'; import { IFieldEditor, FieldEditorProps, MessageHub } from '@esengine/editor-core'; import { Core } from '@esengine/ecs-framework'; import { Plus, Trash2, ChevronDown, ChevronRight, Film, Upload, Star, Play, Square } from 'lucide-react'; -import type { AnimationClip, AnimationFrame, SpriteAnimatorComponent } from '@esengine/ecs-components'; +import type { AnimationClip, AnimationFrame, SpriteAnimatorComponent } from '@esengine/sprite'; import { AssetField } from '../../components/inspectors/fields/AssetField'; import { EngineService } from '../../services/EngineService'; diff --git a/packages/editor-app/src/locales/en.ts b/packages/editor-app/src/locales/en.ts index d4e0366e..e4d57e8f 100644 --- a/packages/editor-app/src/locales/en.ts +++ b/packages/editor-app/src/locales/en.ts @@ -15,8 +15,7 @@ export const en: Translations = { ready: 'Editor Ready', failed: 'Initialization Failed', projectOpened: 'Project Opened', - remoteConnected: 'Remote Game Connected', - profilerMode: 'Profiler Mode - Waiting for connection...' + remoteConnected: 'Remote Game Connected' } }, hierarchy: { diff --git a/packages/editor-app/src/locales/zh.ts b/packages/editor-app/src/locales/zh.ts index db117b63..8151a083 100644 --- a/packages/editor-app/src/locales/zh.ts +++ b/packages/editor-app/src/locales/zh.ts @@ -15,8 +15,7 @@ export const zh: Translations = { ready: '编辑器就绪', failed: '初始化失败', projectOpened: '项目已打开', - remoteConnected: '远程游戏已连接', - profilerMode: '性能分析模式 - 等待连接...' + remoteConnected: '远程游戏已连接' } }, hierarchy: { diff --git a/packages/editor-app/src/plugins/builtin/EditorAppearancePlugin.tsx b/packages/editor-app/src/plugins/builtin/EditorAppearancePlugin.tsx index 91561f83..da4ed07b 100644 --- a/packages/editor-app/src/plugins/builtin/EditorAppearancePlugin.tsx +++ b/packages/editor-app/src/plugins/builtin/EditorAppearancePlugin.tsx @@ -5,7 +5,7 @@ import type { ServiceContainer } from '@esengine/ecs-framework'; import { createLogger } from '@esengine/ecs-framework'; -import type { IPluginLoader, IEditorModuleLoader, PluginDescriptor } from '@esengine/editor-core'; +import type { IPlugin, IEditorModuleLoader, PluginDescriptor } from '@esengine/editor-core'; import { SettingsRegistry } from '@esengine/editor-core'; import { SettingsService } from '../../services/SettingsService'; @@ -122,7 +122,7 @@ const descriptor: PluginDescriptor = { ] }; -export const EditorAppearancePlugin: IPluginLoader = { +export const EditorAppearancePlugin: IPlugin = { descriptor, editorModule: new EditorAppearanceEditorModule() }; diff --git a/packages/editor-app/src/plugins/builtin/GizmoPlugin.ts b/packages/editor-app/src/plugins/builtin/GizmoPlugin.ts index df44343b..13184883 100644 --- a/packages/editor-app/src/plugins/builtin/GizmoPlugin.ts +++ b/packages/editor-app/src/plugins/builtin/GizmoPlugin.ts @@ -4,7 +4,7 @@ */ import type { ServiceContainer } from '@esengine/ecs-framework'; -import type { IPluginLoader, IEditorModuleLoader, PluginDescriptor, GizmoProviderRegistration } from '@esengine/editor-core'; +import type { IPlugin, IEditorModuleLoader, PluginDescriptor, GizmoProviderRegistration } from '@esengine/editor-core'; import { registerSpriteGizmo } from '../../gizmos'; /** @@ -44,7 +44,7 @@ const descriptor: PluginDescriptor = { ] }; -export const GizmoPlugin: IPluginLoader = { +export const GizmoPlugin: IPlugin = { descriptor, editorModule: new GizmoEditorModule() }; diff --git a/packages/editor-app/src/plugins/builtin/PluginConfigPlugin.tsx b/packages/editor-app/src/plugins/builtin/PluginConfigPlugin.tsx index f7701891..26e3c54c 100644 --- a/packages/editor-app/src/plugins/builtin/PluginConfigPlugin.tsx +++ b/packages/editor-app/src/plugins/builtin/PluginConfigPlugin.tsx @@ -5,7 +5,7 @@ import type { ServiceContainer } from '@esengine/ecs-framework'; import { createLogger } from '@esengine/ecs-framework'; -import type { IPluginLoader, IEditorModuleLoader, PluginDescriptor } from '@esengine/editor-core'; +import type { IPlugin, IEditorModuleLoader, PluginDescriptor } from '@esengine/editor-core'; import { SettingsRegistry } from '@esengine/editor-core'; const logger = createLogger('PluginConfigPlugin'); @@ -71,7 +71,7 @@ const descriptor: PluginDescriptor = { ] }; -export const PluginConfigPlugin: IPluginLoader = { +export const PluginConfigPlugin: IPlugin = { descriptor, editorModule: new PluginConfigEditorModule() }; diff --git a/packages/editor-app/src/plugins/builtin/ProfilerPlugin.tsx b/packages/editor-app/src/plugins/builtin/ProfilerPlugin.tsx index c48e3c14..7bdbcc62 100644 --- a/packages/editor-app/src/plugins/builtin/ProfilerPlugin.tsx +++ b/packages/editor-app/src/plugins/builtin/ProfilerPlugin.tsx @@ -5,14 +5,12 @@ import type { ServiceContainer } from '@esengine/ecs-framework'; import type { - IPluginLoader, + IPlugin, IEditorModuleLoader, PluginDescriptor, - PanelDescriptor, MenuItemDescriptor } from '@esengine/editor-core'; -import { MessageHub, SettingsRegistry, PanelPosition } from '@esengine/editor-core'; -import { ProfilerDockPanel } from '../../components/ProfilerDockPanel'; +import { MessageHub, SettingsRegistry } from '@esengine/editor-core'; import { ProfilerService } from '../../services/ProfilerService'; /** @@ -100,19 +98,6 @@ class ProfilerEditorModule implements IEditorModuleLoader { delete (window as any).__PROFILER_SERVICE__; } - getPanels(): PanelDescriptor[] { - return [ - { - id: 'profiler-monitor', - title: 'Performance Monitor', - position: PanelPosition.Center, - closable: false, - component: ProfilerDockPanel, - order: 200 - } - ]; - } - getMenuItems(): MenuItemDescriptor[] { return [ { @@ -122,14 +107,6 @@ class ProfilerEditorModule implements IEditorModuleLoader { execute: () => { this.messageHub?.publish('ui:openWindow', { windowId: 'profiler' }); } - }, - { - id: 'window.advancedProfiler', - label: 'Advanced Profiler', - parentId: 'window', - execute: () => { - this.messageHub?.publish('ui:openWindow', { windowId: 'advancedProfiler' }); - } } ]; } @@ -151,13 +128,12 @@ const descriptor: PluginDescriptor = { { name: 'ProfilerEditor', type: 'editor', - loadingPhase: 'postDefault', - panels: ['profiler-monitor'] + loadingPhase: 'postDefault' } ] }; -export const ProfilerPlugin: IPluginLoader = { +export const ProfilerPlugin: IPlugin = { descriptor, editorModule: new ProfilerEditorModule() }; diff --git a/packages/editor-app/src/plugins/builtin/ProjectSettingsPlugin.tsx b/packages/editor-app/src/plugins/builtin/ProjectSettingsPlugin.tsx index 67defe7d..424dc35c 100644 --- a/packages/editor-app/src/plugins/builtin/ProjectSettingsPlugin.tsx +++ b/packages/editor-app/src/plugins/builtin/ProjectSettingsPlugin.tsx @@ -8,7 +8,7 @@ import type { ServiceContainer } from '@esengine/ecs-framework'; import { createLogger, Core } from '@esengine/ecs-framework'; -import type { IPluginLoader, IEditorModuleLoader, PluginDescriptor } from '@esengine/editor-core'; +import type { IPlugin, IEditorModuleLoader, PluginDescriptor } from '@esengine/editor-core'; import { SettingsRegistry, ProjectService } from '@esengine/editor-core'; import EngineService from '../../services/EngineService'; @@ -167,7 +167,7 @@ const descriptor: PluginDescriptor = { ] }; -export const ProjectSettingsPlugin: IPluginLoader = { +export const ProjectSettingsPlugin: IPlugin = { descriptor, editorModule: new ProjectSettingsEditorModule() }; diff --git a/packages/editor-app/src/plugins/builtin/SceneInspectorPlugin.ts b/packages/editor-app/src/plugins/builtin/SceneInspectorPlugin.ts index d1ac645b..47fa3266 100644 --- a/packages/editor-app/src/plugins/builtin/SceneInspectorPlugin.ts +++ b/packages/editor-app/src/plugins/builtin/SceneInspectorPlugin.ts @@ -6,7 +6,7 @@ import { Core, Entity } from '@esengine/ecs-framework'; import type { ServiceContainer } from '@esengine/ecs-framework'; import type { - IPluginLoader, + IPlugin, IEditorModuleLoader, PluginDescriptor, PanelDescriptor, @@ -15,7 +15,9 @@ import type { EntityCreationTemplate } from '@esengine/editor-core'; import { PanelPosition, EntityStoreService, MessageHub } from '@esengine/editor-core'; -import { TransformComponent, SpriteComponent, SpriteAnimatorComponent, CameraComponent } from '@esengine/ecs-components'; +import { TransformComponent } from '@esengine/engine-core'; +import { SpriteComponent, SpriteAnimatorComponent } from '@esengine/sprite'; +import { CameraComponent } from '@esengine/camera'; /** * Scene Inspector 编辑器模块 @@ -186,14 +188,12 @@ const descriptor: PluginDescriptor = { { name: 'SceneInspectorEditor', type: 'editor', - loadingPhase: 'default', - panels: ['panel-scene-hierarchy', 'panel-entity-inspector'], - inspectors: ['EntityInspector'] + loadingPhase: 'default' } ] }; -export const SceneInspectorPlugin: IPluginLoader = { +export const SceneInspectorPlugin: IPlugin = { descriptor, editorModule: new SceneInspectorEditorModule() }; diff --git a/packages/editor-app/src/plugins/builtin/index.ts b/packages/editor-app/src/plugins/builtin/index.ts index 31c82848..08b5b57a 100644 --- a/packages/editor-app/src/plugins/builtin/index.ts +++ b/packages/editor-app/src/plugins/builtin/index.ts @@ -9,4 +9,5 @@ export { ProfilerPlugin } from './ProfilerPlugin'; export { EditorAppearancePlugin } from './EditorAppearancePlugin'; export { PluginConfigPlugin } from './PluginConfigPlugin'; export { ProjectSettingsPlugin } from './ProjectSettingsPlugin'; -export { BlueprintPlugin } from '@esengine/blueprint/editor'; +// TODO: Re-enable when blueprint-editor package is fixed +// export { BlueprintPlugin } from '@esengine/blueprint-editor'; diff --git a/packages/editor-app/src/services/EditorEngineSync.ts b/packages/editor-app/src/services/EditorEngineSync.ts index 394e9eb7..0735a0cd 100644 --- a/packages/editor-app/src/services/EditorEngineSync.ts +++ b/packages/editor-app/src/services/EditorEngineSync.ts @@ -8,7 +8,8 @@ import { Entity, Component } from '@esengine/ecs-framework'; import { MessageHub, EntityStoreService } from '@esengine/editor-core'; -import { TransformComponent, SpriteComponent, SpriteAnimatorComponent } from '@esengine/ecs-components'; +import { TransformComponent } from '@esengine/engine-core'; +import { SpriteComponent, SpriteAnimatorComponent } from '@esengine/sprite'; import { EngineService } from './EngineService'; export class EditorEngineSync { diff --git a/packages/editor-app/src/services/EngineService.ts b/packages/editor-app/src/services/EngineService.ts index e40d9915..72253a57 100644 --- a/packages/editor-app/src/services/EngineService.ts +++ b/packages/editor-app/src/services/EngineService.ts @@ -1,16 +1,17 @@ /** * Engine service for managing Rust engine lifecycle. * 管理Rust引擎生命周期的服务。 + * + * 使用统一的 GameRuntime 架构 + * Uses the unified GameRuntime architecture */ -import { EngineBridge, EngineRenderSystem, GizmoDataProviderFn, HasGizmoProviderFn, CameraConfig, CameraSystem } from '@esengine/ecs-engine-bindgen'; import { GizmoRegistry, EntityStoreService, MessageHub, SceneManagerService, ProjectService, PluginManager, IPluginManager, type SystemContext } from '@esengine/editor-core'; -import { Core, Scene, Entity, SceneSerializer } from '@esengine/ecs-framework'; -import { TransformComponent, SpriteComponent, SpriteAnimatorComponent, SpriteAnimatorSystem } from '@esengine/ecs-components'; -import { TilemapComponent, TilemapRenderingSystem } from '@esengine/tilemap'; -import { BehaviorTreeExecutionSystem } from '@esengine/behavior-tree'; -import { UIRenderDataProvider, invalidateUIRenderCaches, UIInputSystem } from '@esengine/ui'; -import { Physics2DSystem } from '@esengine/physics-rapier2d'; +import { Core, Scene, Entity, SceneSerializer, ProfilerSDK } from '@esengine/ecs-framework'; +import { CameraConfig } from '@esengine/ecs-engine-bindgen'; +import { TransformComponent } from '@esengine/engine-core'; +import { SpriteComponent, SpriteAnimatorComponent } from '@esengine/sprite'; +import { invalidateUIRenderCaches } from '@esengine/ui'; import * as esEngine from '@esengine/engine'; import { AssetManager, @@ -20,39 +21,41 @@ import { globalPathResolver, SceneResourceManager } from '@esengine/asset-system'; +import { + GameRuntime, + createGameRuntime, + EditorPlatformAdapter, + type GameRuntimeConfig +} from '@esengine/runtime-core'; import { convertFileSrc } from '@tauri-apps/api/core'; import { IdGenerator } from '../utils/idGenerator'; /** * Engine service singleton for editor integration. * 用于编辑器集成的引擎服务单例。 + * + * 内部使用 GameRuntime,对外保持原有 API 兼容 + * Internally uses GameRuntime, maintains original API compatibility externally */ export class EngineService { private static instance: EngineService | null = null; - private bridge: EngineBridge | null = null; - private scene: Scene | null = null; - private renderSystem: EngineRenderSystem | null = null; - private cameraSystem: CameraSystem | null = null; - private animatorSystem: SpriteAnimatorSystem | null = null; - private tilemapSystem: TilemapRenderingSystem | null = null; - private behaviorTreeSystem: BehaviorTreeExecutionSystem | null = null; - private physicsSystem: Physics2DSystem | null = null; - private uiRenderProvider: UIRenderDataProvider | null = null; - private uiInputSystem: UIInputSystem | null = null; - private initialized = false; - private modulesInitialized = false; - private running = false; - private animationFrameId: number | null = null; - private lastTime = 0; - private sceneSnapshot: string | null = null; - private assetManager: AssetManager | null = null; - private engineIntegration: EngineIntegration | null = null; - private sceneResourceManager: SceneResourceManager | null = null; - private assetPathResolver: AssetPathResolver | null = null; - private assetSystemInitialized = false; - private initializationError: Error | null = null; - private canvasId: string | null = null; + private _runtime: GameRuntime | null = null; + private _initialized = false; + private _modulesInitialized = false; + private _running = false; + private _canvasId: string | null = null; + + // 资产系统相关 + private _assetManager: AssetManager | null = null; + private _engineIntegration: EngineIntegration | null = null; + private _sceneResourceManager: SceneResourceManager | null = null; + private _assetPathResolver: AssetPathResolver | null = null; + private _assetSystemInitialized = false; + private _initializationError: Error | null = null; + + // 编辑器相机状态(用于恢复) + private _editorCameraState = { x: 0, y: 0, zoom: 1 }; private constructor() {} @@ -69,151 +72,110 @@ export class EngineService { /** * 等待引擎初始化完成 - * @param timeout 超时时间(毫秒),默认 10 秒 */ async waitForInitialization(timeout = 10000): Promise { - if (this.initialized) { + if (this._initialized) { return true; } const startTime = Date.now(); - while (!this.initialized && Date.now() - startTime < timeout) { + while (!this._initialized && Date.now() - startTime < timeout) { await new Promise(resolve => setTimeout(resolve, 100)); } - return this.initialized; + return this._initialized; } /** * Initialize the engine with canvas. * 使用canvas初始化引擎。 - * - * 注意:此方法只初始化引擎基础设施(Core、渲染系统等), - * 模块的初始化需要在项目打开后调用 initializeModuleSystems() */ async initialize(canvasId: string): Promise { - if (this.initialized) { + if (this._initialized) { return; } - this.canvasId = canvasId; + this._canvasId = canvasId; try { - // Create engine bridge | 创建引擎桥接 - this.bridge = new EngineBridge({ - canvasId - }); - - // Initialize WASM with pre-imported module | 使用预导入模块初始化WASM - await this.bridge.initializeWithModule(esEngine); - - // Set path resolver for Tauri asset URLs | 设置Tauri资产URL的路径解析器 - this.bridge.setPathResolver((path: string) => { - // If already a URL, return as-is + // 创建路径转换函数 + const pathTransformer = (path: string) => { if (path.startsWith('http://') || path.startsWith('https://') || path.startsWith('data:') || path.startsWith('asset://')) { return path; } - // Convert file path to Tauri asset URL return convertFileSrc(path); + }; + + // 创建编辑器平台适配器 + const platform = new EditorPlatformAdapter({ + wasmModule: esEngine, + pathTransformer, + gizmoDataProvider: (component, entity, isSelected) => + GizmoRegistry.getGizmoData(component, entity, isSelected), + hasGizmoProvider: (component) => + GizmoRegistry.hasProvider(component.constructor as any) }); - // Initialize Core if not already | 初始化Core(如果尚未初始化) - if (!Core.scene) { - Core.create({ debug: false }); - } + // 创建统一运行时 + // 编辑器模式下跳过内部插件加载,由 editor-core 的 PluginManager 管理 + this._runtime = createGameRuntime({ + platform, + canvasId, + autoStartRenderLoop: true, + uiCanvasSize: { width: 1920, height: 1080 }, + skipPluginLoading: true // 编辑器自己管理插件 + }); - // 使用现有 Core 场景或创建新的 - if (Core.scene) { - this.scene = Core.scene as Scene; - } else { - this.scene = new Scene({ name: 'EditorScene' }); - Core.setScene(this.scene); - } + await this._runtime.initialize(); - // Add camera system (基础系统,始终需要) - this.cameraSystem = new CameraSystem(this.bridge); - this.scene.addSystem(this.cameraSystem); + // 启用性能分析器(编辑器模式默认启用) + ProfilerSDK.setEnabled(true); - // Add render system to the scene (基础系统,始终需要) - this.renderSystem = new EngineRenderSystem(this.bridge, TransformComponent); - this.scene.addSystem(this.renderSystem); - - // Inject GizmoRegistry into render system - this.renderSystem.setGizmoRegistry( - ((component, entity, isSelected) => - GizmoRegistry.getGizmoData(component, entity, isSelected)) as GizmoDataProviderFn, - ((component) => - GizmoRegistry.hasProvider(component.constructor as any)) as HasGizmoProviderFn + // 设置 Gizmo 注册表 + this._runtime.setGizmoRegistry( + (component, entity, isSelected) => + GizmoRegistry.getGizmoData(component, entity, isSelected), + (component) => + GizmoRegistry.hasProvider(component.constructor as any) ); - // Set initial UI canvas size (will be updated from ProjectService when project opens) - // 设置初始 UI 画布尺寸(项目打开后会从 ProjectService 更新为项目配置的分辨率) - this.renderSystem.setUICanvasSize(1920, 1080); + // 初始化资产系统 + await this._initializeAssetSystem(); - // Initialize asset system | 初始化资产系统 - await this.initializeAssetSystem(); - - // Start the default world to enable system updates - // 启动默认world以启用系统更新 - const defaultWorld = Core.worldManager.getWorld('__default__'); - if (defaultWorld && !defaultWorld.isActive) { - defaultWorld.start(); - } - - this.initialized = true; - - // Sync viewport size immediately after initialization - // 初始化后立即同步视口尺寸 + // 同步视口尺寸 const canvas = document.getElementById(canvasId) as HTMLCanvasElement; if (canvas && canvas.parentElement) { - // Get container size in CSS pixels - // 获取容器尺寸(CSS像素) const rect = canvas.parentElement.getBoundingClientRect(); const dpr = window.devicePixelRatio || 1; - // Canvas internal size uses DPR for sharpness - // Canvas内部尺寸使用DPR以保持清晰 canvas.width = Math.floor(rect.width * dpr); canvas.height = Math.floor(rect.height * dpr); canvas.style.width = `${rect.width}px`; canvas.style.height = `${rect.height}px`; - // Camera uses actual canvas pixels for correct rendering - // 相机使用实际canvas像素以保证正确渲染 - this.bridge.resize(canvas.width, canvas.height); + this._runtime.resize(canvas.width, canvas.height); } - // Auto-start render loop for editor preview | 自动启动渲染循环用于编辑器预览 - this.startRenderLoop(); + this._initialized = true; } catch (error) { - console.error('Failed to initialize engine | 引擎初始化失败:', error); + console.error('Failed to initialize engine:', error); throw error; } } /** * 初始化模块系统 - * Initialize module systems for all enabled plugins - * - * 通过 PluginManager 初始化所有插件的运行时模块 - * Initialize all plugin runtime modules via PluginManager */ async initializeModuleSystems(): Promise { - if (!this.initialized) { + if (!this._initialized || !this._runtime) { console.error('Engine not initialized. Call initialize() first.'); return; } - if (!this.scene || !this.renderSystem || !this.bridge) { - console.error('Scene or render system not available.'); - return; - } - - // 如果之前已经初始化过模块,先清理 - if (this.modulesInitialized) { + if (this._modulesInitialized) { this.clearModuleSystems(); } @@ -224,201 +186,123 @@ export class EngineService { return; } - // 初始化所有插件的运行时模块(注册组件和服务) - // Initialize all plugin runtime modules (register components and services) + // 初始化所有插件的运行时模块 await pluginManager.initializeRuntime(Core.services); // 创建系统上下文 - // Create system context const context: SystemContext = { core: Core, - engineBridge: this.bridge, - renderSystem: this.renderSystem, - assetManager: this.assetManager, + engineBridge: this._runtime.bridge, + renderSystem: this._runtime.renderSystem, + assetManager: this._assetManager, isEditor: true }; // 让插件为场景创建系统 - // Let plugins create systems for scene - pluginManager.createSystemsForScene(this.scene, context); + pluginManager.createSystemsForScene(this._runtime.scene!, context); - // 保存插件创建的系统引用 - // Save system references created by plugins - this.animatorSystem = context.animatorSystem as SpriteAnimatorSystem | undefined ?? null; - this.tilemapSystem = context.tilemapSystem as TilemapRenderingSystem | undefined ?? null; - this.behaviorTreeSystem = context.behaviorTreeSystem as BehaviorTreeExecutionSystem | undefined ?? null; - this.physicsSystem = context.physicsSystem as Physics2DSystem | undefined ?? null; - this.uiRenderProvider = context.uiRenderProvider as UIRenderDataProvider | undefined ?? null; - this.uiInputSystem = context.uiInputSystem as UIInputSystem | undefined ?? null; + // 同步系统引用到 GameRuntime 的 systemContext(用于 start/stop 时启用/禁用系统) + this._runtime.updateSystemContext({ + animatorSystem: context.animatorSystem, + behaviorTreeSystem: context.behaviorTreeSystem, + physicsSystem: context.physicsSystem, + uiInputSystem: context.uiInputSystem, + uiRenderProvider: context.uiRenderProvider + }); - // 设置 UI 渲染数据提供者到 EngineRenderSystem - // Set UI render data provider to EngineRenderSystem - if (this.uiRenderProvider && this.renderSystem) { - this.renderSystem.setUIRenderDataProvider(this.uiRenderProvider); + // 设置 UI 渲染数据提供者 + if (context.uiRenderProvider && this._runtime.renderSystem) { + this._runtime.renderSystem.setUIRenderDataProvider(context.uiRenderProvider); } - // 在编辑器模式下,动画、行为树和物理系统默认禁用 - // In editor mode, animation, behavior tree and physics systems are disabled by default - if (this.animatorSystem) { - this.animatorSystem.enabled = false; + // 在编辑器模式下,禁用游戏逻辑系统 + if (context.animatorSystem) { + context.animatorSystem.enabled = false; } - if (this.behaviorTreeSystem) { - this.behaviorTreeSystem.enabled = false; + if (context.behaviorTreeSystem) { + context.behaviorTreeSystem.enabled = false; } - if (this.physicsSystem) { - this.physicsSystem.enabled = false; + if (context.physicsSystem) { + context.physicsSystem.enabled = false; } - this.modulesInitialized = true; + this._modulesInitialized = true; } /** * 清理模块系统 - * 用于项目关闭或切换时 - * Clear module systems, used when project closes or switches */ clearModuleSystems(): void { - // 通过 PluginManager 清理场景系统 - // Clear scene systems via PluginManager const pluginManager = Core.services.tryResolve(IPluginManager); if (pluginManager) { pluginManager.clearSceneSystems(); } - // Unbind UI input system before clearing - // 清理前解绑 UI 输入系统 - if (this.uiInputSystem) { - this.uiInputSystem.unbind(); + const ctx = this._runtime?.systemContext; + if (ctx?.uiInputSystem) { + ctx.uiInputSystem.unbind?.(); } - // 清空本地引用(系统的实际清理由场景管理) - // Clear local references (actual system cleanup is managed by scene) - this.animatorSystem = null; - this.tilemapSystem = null; - this.behaviorTreeSystem = null; - this.physicsSystem = null; - this.uiRenderProvider = null; - this.uiInputSystem = null; - this.modulesInitialized = false; + this._modulesInitialized = false; } /** * 检查模块系统是否已初始化 */ isModulesInitialized(): boolean { - return this.modulesInitialized; + return this._modulesInitialized; } - /** - * Start render loop (editor preview mode). - * 启动渲染循环(编辑器预览模式)。 - */ - private startRenderLoop(): void { - if (this.animationFrameId !== null) { - return; - } - this.lastTime = performance.now(); - this.renderLoop(); - } - - private frameCount = 0; - - /** - * Render loop for editor preview (always runs). - * 编辑器预览的渲染循环(始终运行)。 - */ - private renderLoop = (): void => { - const currentTime = performance.now(); - const deltaTime = (currentTime - this.lastTime) / 1000; - this.lastTime = currentTime; - - this.frameCount++; - - // Update via Core (handles deltaTime internally) | 通过Core更新 - Core.update(deltaTime); - - // Note: Rendering is handled by EngineRenderSystem.process() - // Texture loading is handled automatically via Rust engine's path-based loading - // 注意:渲染由 EngineRenderSystem.process() 处理 - // 纹理加载由Rust引擎的路径加载自动处理 - - this.animationFrameId = requestAnimationFrame(this.renderLoop); - }; - /** * Check if engine is initialized. - * 检查引擎是否已初始化。 */ isInitialized(): boolean { - return this.initialized; + return this._initialized; } /** * Check if engine is running. - * 检查引擎是否正在运行。 */ isRunning(): boolean { - return this.running; + return this._running; } /** - * Start the game loop. - * 启动游戏循环。 + * Start the game loop (preview mode). */ start(): void { - if (!this.initialized || this.running) { + if (!this._initialized || !this._runtime || this._running) { return; } - this.running = true; - this.lastTime = performance.now(); + this._running = true; + this._runtime.start(); - // Enable preview mode for UI rendering (screen space overlay) - // 启用预览模式用于 UI 渲染(屏幕空间叠加) - if (this.renderSystem) { - this.renderSystem.setPreviewMode(true); - } + // 启动自动播放动画 + this._startAutoPlayAnimations(); + } - // Bind UI input system to canvas for event handling - // 绑定 UI 输入系统到 canvas 以处理事件 - if (this.uiInputSystem && this.canvasId) { - const canvas = document.getElementById(this.canvasId) as HTMLCanvasElement; - if (canvas) { - this.uiInputSystem.bindToCanvas(canvas); - } - } + /** + * Stop the game loop. + */ + stop(): void { + if (!this._runtime) return; - // Enable animator system and start auto-play animations - // 启用动画系统并启动自动播放的动画 - if (this.animatorSystem) { - this.animatorSystem.enabled = true; - } - // Enable behavior tree system for preview - // 启用行为树系统用于预览 - if (this.behaviorTreeSystem) { - this.behaviorTreeSystem.enabled = true; - // 启动所有自动启动的行为树(因为在编辑器模式下 onAdded 不会处理) - // Start all auto-start behavior trees (since onAdded doesn't handle them in editor mode) - this.behaviorTreeSystem.startAllAutoStartTrees(); - } - // Enable physics system for preview - // 启用物理系统用于预览 - if (this.physicsSystem) { - this.physicsSystem.enabled = true; - } - this.startAutoPlayAnimations(); + this._running = false; + this._runtime.stop(); - this.gameLoop(); + // 停止所有动画 + this._stopAllAnimations(); } /** * Start all auto-play animations. - * 启动所有自动播放的动画。 */ - private startAutoPlayAnimations(): void { - if (!this.scene) return; + private _startAutoPlayAnimations(): void { + const scene = this._runtime?.scene; + if (!scene) return; - const entities = this.scene.entities.findEntitiesWithComponent(SpriteAnimatorComponent); + const entities = scene.entities.findEntitiesWithComponent(SpriteAnimatorComponent); for (const entity of entities) { const animator = entity.getComponent(SpriteAnimatorComponent); if (animator && animator.autoPlay && animator.defaultAnimation) { @@ -428,20 +312,18 @@ export class EngineService { } /** - * Stop all animations and reset to first frame. - * 停止所有动画并重置到第一帧。 + * Stop all animations. */ - private stopAllAnimations(): void { - if (!this.scene) return; + private _stopAllAnimations(): void { + const scene = this._runtime?.scene; + if (!scene) return; - const entities = this.scene.entities.findEntitiesWithComponent(SpriteAnimatorComponent); + const entities = scene.entities.findEntitiesWithComponent(SpriteAnimatorComponent); for (const entity of entities) { const animator = entity.getComponent(SpriteAnimatorComponent); if (animator) { animator.stop(); - // Reset sprite texture to first frame - // 重置精灵纹理到第一帧 const sprite = entity.getComponent(SpriteComponent); if (sprite && animator.clips && animator.clips.length > 0) { const firstClip = animator.clips[0]; @@ -457,74 +339,65 @@ export class EngineService { } /** - * Stop the game loop. - * 停止游戏循环。 + * Initialize asset system */ - stop(): void { - this.running = false; + private async _initializeAssetSystem(): Promise { + try { + this._assetManager = new AssetManager(); - // Disable preview mode for UI rendering (back to world space) - // 禁用预览模式用于 UI 渲染(返回世界空间) - if (this.renderSystem) { - this.renderSystem.setPreviewMode(false); - } + const pathTransformerFn = (path: string) => { + if (!path.startsWith('http://') && !path.startsWith('https://') && + !path.startsWith('data:') && !path.startsWith('asset://')) { + if (!path.startsWith('/') && !path.match(/^[a-zA-Z]:/)) { + const projectService = Core.services.tryResolve(ProjectService); + if (projectService && projectService.isProjectOpen()) { + const projectInfo = projectService.getCurrentProject(); + if (projectInfo) { + const projectPath = projectInfo.path; + const separator = projectPath.includes('\\') ? '\\' : '/'; + path = `${projectPath}${separator}${path.replace(/\//g, separator)}`; + } + } + } + return convertFileSrc(path); + } + return path; + }; - // Unbind UI input system from canvas - // 从 canvas 解绑 UI 输入系统 - if (this.uiInputSystem) { - this.uiInputSystem.unbind(); - } + this._assetPathResolver = new AssetPathResolver({ + platform: AssetPlatform.Editor, + pathTransformer: pathTransformerFn + }); - // Disable animator system and stop all animations - // 禁用动画系统并停止所有动画 - if (this.animatorSystem) { - this.animatorSystem.enabled = false; - } - // Disable behavior tree system - // 禁用行为树系统 - if (this.behaviorTreeSystem) { - this.behaviorTreeSystem.enabled = false; - } - // Disable and reset physics system - // 禁用并重置物理系统 - if (this.physicsSystem) { - this.physicsSystem.enabled = false; - // Reset physics world state to prepare for next preview - // 重置物理世界状态,为下次预览做准备 - this.physicsSystem.reset(); - } - this.stopAllAnimations(); + globalPathResolver.updateConfig({ + platform: AssetPlatform.Editor, + pathTransformer: pathTransformerFn + }); - // Note: Don't cancel animationFrameId here, as renderLoop should keep running - // for editor preview. The renderLoop will continue but gameLoop will stop - // because this.running is false. - // 注意:这里不要取消 animationFrameId,因为 renderLoop 应该继续运行 - // 用于编辑器预览。renderLoop 会继续运行,但 gameLoop 会停止 - // 因为 this.running 是 false。 + if (this._runtime?.bridge) { + this._engineIntegration = new EngineIntegration(this._assetManager, this._runtime.bridge); + + this._sceneResourceManager = new SceneResourceManager(); + this._sceneResourceManager.setResourceLoader(this._engineIntegration); + + const sceneManagerService = Core.services.tryResolve(SceneManagerService); + if (sceneManagerService) { + sceneManagerService.setSceneResourceManager(this._sceneResourceManager); + } + } + + this._assetSystemInitialized = true; + this._initializationError = null; + } catch (error) { + this._assetSystemInitialized = false; + this._initializationError = error instanceof Error ? error : new Error(String(error)); + console.error('Failed to initialize asset system:', error); + throw this._initializationError; + } } - /** - * Main game loop. - * 主游戏循环。 - */ - private gameLoop = (): void => { - if (!this.running) { - return; - } - - const currentTime = performance.now(); - const deltaTime = (currentTime - this.lastTime) / 1000; - this.lastTime = currentTime; - - // Update via Core | 通过Core更新 - Core.update(deltaTime); - - this.animationFrameId = requestAnimationFrame(this.gameLoop); - }; - /** * Create entity with sprite and transform. - * 创建带精灵和变换的实体。 */ createSpriteEntity(name: string, options?: { x?: number; @@ -533,13 +406,11 @@ export class EngineService { width?: number; height?: number; }): Entity | null { - if (!this.scene) { - return null; - } + const scene = this._runtime?.scene; + if (!scene) return null; - const entity = this.scene.createEntity(name); + const entity = scene.createEntity(name); - // Add transform | 添加变换组件 const transform = new TransformComponent(); if (options) { transform.position.x = options.x ?? 0; @@ -547,7 +418,6 @@ export class EngineService { } entity.addComponent(transform); - // Add sprite | 添加精灵组件 const sprite = new SpriteComponent(); if (options) { sprite.textureId = options.textureId ?? 0; @@ -559,409 +429,221 @@ export class EngineService { return entity; } - /** - * Initialize asset system - * 初始化资产系统 - */ - private async initializeAssetSystem(): Promise { - try { - // 创建资产管理器 / Create asset manager - this.assetManager = new AssetManager(); - - // 创建路径解析器 / Create path resolver - const pathTransformerFn = (path: string) => { - // 编辑器平台使用Tauri的convertFileSrc - // Use Tauri's convertFileSrc for editor platform - if (!path.startsWith('http://') && !path.startsWith('https://') && !path.startsWith('data:') && !path.startsWith('asset://')) { - // 如果是相对路径,需要先转换为绝对路径 - // If it's a relative path, convert to absolute path first - if (!path.startsWith('/') && !path.match(/^[a-zA-Z]:/)) { - const projectService = Core.services.tryResolve(ProjectService); - if (projectService && projectService.isProjectOpen()) { - const projectInfo = projectService.getCurrentProject(); - if (projectInfo) { - const projectPath = projectInfo.path; - // 规范化路径分隔符 / Normalize path separators - const separator = projectPath.includes('\\') ? '\\' : '/'; - path = `${projectPath}${separator}${path.replace(/\//g, separator)}`; - } - } - } - return convertFileSrc(path); - } - return path; - }; - - this.assetPathResolver = new AssetPathResolver({ - platform: AssetPlatform.Editor, - pathTransformer: pathTransformerFn - }); - - // 配置全局路径解析器,供组件使用 - // Configure global path resolver for components to use - globalPathResolver.updateConfig({ - platform: AssetPlatform.Editor, - pathTransformer: pathTransformerFn - }); - - // 创建引擎集成 / Create engine integration - if (this.bridge) { - this.engineIntegration = new EngineIntegration(this.assetManager, this.bridge); - - // 创建场景资源管理器 / Create scene resource manager - this.sceneResourceManager = new SceneResourceManager(); - this.sceneResourceManager.setResourceLoader(this.engineIntegration); - - // 将 SceneResourceManager 设置到 SceneManagerService - // Set SceneResourceManager to SceneManagerService - const sceneManagerService = Core.services.tryResolve(SceneManagerService); - if (sceneManagerService) { - sceneManagerService.setSceneResourceManager(this.sceneResourceManager); - } - } - - this.assetSystemInitialized = true; - this.initializationError = null; - } catch (error) { - this.assetSystemInitialized = false; - this.initializationError = error instanceof Error ? error : new Error(String(error)); - console.error('Failed to initialize asset system:', error); - - // Notify user of failure - const messageHub = Core.services.tryResolve(MessageHub); - if (messageHub) { - messageHub.publish('notification:error', { - title: 'Asset System Error', - message: 'Failed to initialize asset system. Some features may not work properly.' - }); - } - - throw this.initializationError; - } - } - /** * Load texture. - * 加载纹理。 */ loadTexture(id: number, url: string): void { - if (this.renderSystem) { - this.renderSystem.loadTexture(id, url); - } + this._runtime?.renderSystem?.loadTexture(id, url); } /** * Load texture through asset system - * 通过资产系统加载纹理 */ async loadTextureAsset(path: string): Promise { - // Check if asset system is properly initialized - if (!this.assetSystemInitialized || this.initializationError) { + if (!this._assetSystemInitialized || this._initializationError) { console.warn('Asset system not initialized, using fallback texture loading'); const textureId = IdGenerator.nextId('texture-fallback'); this.loadTexture(textureId, path); return textureId; } - if (!this.engineIntegration) { - // 回退到直接加载 / Fallback to direct loading + if (!this._engineIntegration) { const textureId = IdGenerator.nextId('texture'); this.loadTexture(textureId, path); return textureId; } try { - return await this.engineIntegration.loadTextureForComponent(path); + return await this._engineIntegration.loadTextureForComponent(path); } catch (error) { console.error('Failed to load texture asset:', error); - // Return a valid fallback ID instead of 0 const fallbackId = IdGenerator.nextId('texture-fallback'); - - // Notify about texture loading failure - const messageHub = Core.services.tryResolve(MessageHub); - if (messageHub) { - messageHub.publish('notification:warning', { - title: 'Texture Loading Failed', - message: `Could not load texture: ${path}` - }); - } - return fallbackId; } } /** * Get asset manager - * 获取资产管理器 */ getAssetManager(): AssetManager | null { - return this.assetManager; + return this._assetManager; } /** * Get engine integration - * 获取引擎集成 */ getEngineIntegration(): EngineIntegration | null { - return this.engineIntegration; + return this._engineIntegration; } /** * Get asset path resolver - * 获取资产路径解析器 */ getAssetPathResolver(): AssetPathResolver | null { - return this.assetPathResolver; + return this._assetPathResolver; } /** * Get engine statistics. - * 获取引擎统计信息。 */ getStats(): { fps: number; drawCalls: number; spriteCount: number } { - if (!this.renderSystem) { - return { fps: 0, drawCalls: 0, spriteCount: 0 }; - } - - const engineStats = this.renderSystem.getStats(); - return { - fps: engineStats?.fps ?? 0, - drawCalls: engineStats?.drawCalls ?? 0, - spriteCount: this.renderSystem.spriteCount - }; + return this._runtime?.getStats() ?? { fps: 0, drawCalls: 0, spriteCount: 0 }; } /** * Get the ECS scene. - * 获取ECS场景。 */ getScene(): Scene | null { - return this.scene; + return this._runtime?.scene ?? null; } /** * Enable animation preview in editor mode. - * 在编辑器模式下启用动画预览。 */ enableAnimationPreview(): void { - if (this.animatorSystem && !this.running) { - // Clear entity cache to force re-query when enabled - // 清除实体缓存以便启用时强制重新查询 - this.animatorSystem.clearEntityCache(); - this.animatorSystem.enabled = true; + const ctx = this._runtime?.systemContext; + if (ctx?.animatorSystem && !this._running) { + ctx.animatorSystem.clearEntityCache?.(); + ctx.animatorSystem.enabled = true; } } /** * Disable animation preview in editor mode. - * 在编辑器模式下禁用动画预览。 */ disableAnimationPreview(): void { - if (this.animatorSystem && !this.running) { - this.animatorSystem.enabled = false; + const ctx = this._runtime?.systemContext; + if (ctx?.animatorSystem && !this._running) { + ctx.animatorSystem.enabled = false; } } /** * Check if animation preview is enabled. - * 检查动画预览是否启用。 */ isAnimationPreviewEnabled(): boolean { - return this.animatorSystem?.enabled ?? false; + return this._runtime?.systemContext?.animatorSystem?.enabled ?? false; } /** * Get the engine bridge. - * 获取引擎桥接。 */ - getBridge(): EngineBridge | null { - return this.bridge; + getBridge() { + return this._runtime?.bridge ?? null; } /** * Resize the engine viewport. - * 调整引擎视口大小。 */ resize(width: number, height: number): void { - if (this.bridge) { - this.bridge.resize(width, height); - } + this._runtime?.resize(width, height); } /** * Set camera position, zoom, and rotation. - * 设置相机位置、缩放和旋转。 */ setCamera(config: CameraConfig): void { - if (this.bridge) { - this.bridge.setCamera(config); - } + this._runtime?.setCamera(config); } /** * Get camera state. - * 获取相机状态。 */ getCamera(): CameraConfig { - if (this.bridge) { - return this.bridge.getCamera(); - } - return { x: 0, y: 0, zoom: 1, rotation: 0 }; + return this._runtime?.getCamera() ?? { x: 0, y: 0, zoom: 1, rotation: 0 }; } /** * Set grid visibility. - * 设置网格可见性。 */ setShowGrid(show: boolean): void { - if (this.bridge) { - this.bridge.setShowGrid(show); - } + this._runtime?.setShowGrid(show); } /** * Set clear color (background color). - * 设置清除颜色(背景颜色)。 */ setClearColor(r: number, g: number, b: number, a: number = 1.0): void { - if (this.bridge) { - this.bridge.setClearColor(r, g, b, a); - } + this._runtime?.setClearColor(r, g, b, a); } /** * Set gizmo visibility. - * 设置Gizmo可见性。 */ setShowGizmos(show: boolean): void { - if (this.renderSystem) { - this.renderSystem.setShowGizmos(show); - } + this._runtime?.setShowGizmos(show); } /** * Get gizmo visibility. - * 获取Gizmo可见性。 */ getShowGizmos(): boolean { - return this.renderSystem?.getShowGizmos() ?? true; + return this._runtime?.renderSystem?.getShowGizmos() ?? true; } /** * Set UI canvas size for boundary display. - * 设置 UI 画布尺寸以显示边界。 */ setUICanvasSize(width: number, height: number): void { - if (this.renderSystem) { - this.renderSystem.setUICanvasSize(width, height); - } + this._runtime?.setUICanvasSize(width, height); } /** * Get UI canvas size. - * 获取 UI 画布尺寸。 */ getUICanvasSize(): { width: number; height: number } { - return this.renderSystem?.getUICanvasSize() ?? { width: 0, height: 0 }; + return this._runtime?.getUICanvasSize() ?? { width: 0, height: 0 }; } /** * Set UI canvas boundary visibility. - * 设置 UI 画布边界可见性。 */ setShowUICanvasBoundary(show: boolean): void { - if (this.renderSystem) { - this.renderSystem.setShowUICanvasBoundary(show); - } + this._runtime?.setShowUICanvasBoundary(show); } /** * Get UI canvas boundary visibility. - * 获取 UI 画布边界可见性。 */ getShowUICanvasBoundary(): boolean { - return this.renderSystem?.getShowUICanvasBoundary() ?? true; + return this._runtime?.getShowUICanvasBoundary() ?? true; } // ===== Scene Snapshot API ===== - // ===== 场景快照 API ===== /** * Save a snapshot of the current scene state. - * 保存当前场景状态的快照。 */ saveSceneSnapshot(): boolean { - if (!this.scene) { - console.warn('Cannot save snapshot: no scene available'); - return false; - } - - try { - // Use SceneSerializer from core library - this.sceneSnapshot = SceneSerializer.serialize(this.scene, { - format: 'json', - pretty: false, - includeMetadata: false - }) as string; - - return true; - } catch (error) { - console.error('Failed to save scene snapshot:', error); - return false; - } + return this._runtime?.saveSceneSnapshot() ?? false; } /** * Restore scene state from saved snapshot. - * 从保存的快照恢复场景状态。 */ async restoreSceneSnapshot(): Promise { - if (!this.scene || !this.sceneSnapshot) { - console.warn('Cannot restore snapshot: no scene or snapshot available'); - return false; - } + if (!this._runtime) return false; - try { - // Clear tilemap rendering cache before restoring - // 恢复前清除瓦片地图渲染缓存 - if (this.tilemapSystem) { - this.tilemapSystem.clearCache(); - } + const success = await this._runtime.restoreSceneSnapshot(); - // Clear UI render caches before restoring - // 恢复前清除 UI 渲染缓存 + if (success) { + // 清除 UI 渲染缓存 invalidateUIRenderCaches(); - // Use SceneSerializer from core library - SceneSerializer.deserialize(this.scene, this.sceneSnapshot, { - strategy: 'replace', - preserveIds: true - }); - - // 加载场景资源 / Load scene resources - if (this.sceneResourceManager) { - await this.sceneResourceManager.loadSceneResources(this.scene); - } else { - console.warn('[EngineService] SceneResourceManager not available, skipping resource loading'); + // 加载场景资源 + if (this._sceneResourceManager && this._runtime.scene) { + await this._sceneResourceManager.loadSceneResources(this._runtime.scene); } - // Sync EntityStore with restored scene entities + // 同步 EntityStore const entityStore = Core.services.tryResolve(EntityStoreService); const messageHub = Core.services.tryResolve(MessageHub); if (entityStore && messageHub) { - // Remember selected entity ID before clearing const selectedEntity = entityStore.getSelectedEntity(); const selectedId = selectedEntity?.id; - // Clear old entities from store - entityStore.clear(); + entityStore.syncFromScene(); - // Add restored entities to store - for (const entity of this.scene.entities.buffer) { - entityStore.addEntity(entity); - } - - // Re-select the same entity (now with new reference) if (selectedId !== undefined) { const newEntity = entityStore.getEntity(selectedId); if (newEntity) { @@ -969,181 +651,133 @@ export class EngineService { } } - // Notify UI to refresh messageHub.publish('scene:restored', {}); } - - this.sceneSnapshot = null; - return true; - } catch (error) { - console.error('Failed to restore scene snapshot:', error); - return false; } + + return success; } /** * Check if a snapshot exists. - * 检查是否存在快照。 */ hasSnapshot(): boolean { - return this.sceneSnapshot !== null; + return this._runtime?.hasSnapshot() ?? false; } /** * Set selected entity IDs for gizmo display. - * 设置选中的实体ID用于Gizmo显示。 */ setSelectedEntityIds(ids: number[]): void { - if (this.renderSystem) { - this.renderSystem.setSelectedEntityIds(ids); - } + this._runtime?.setSelectedEntityIds(ids); } /** * Set transform tool mode. - * 设置变换工具模式。 */ setTransformMode(mode: 'select' | 'move' | 'rotate' | 'scale'): void { - if (this.renderSystem) { - this.renderSystem.setTransformMode(mode); - } + this._runtime?.setTransformMode(mode); } /** * Get transform tool mode. - * 获取变换工具模式。 */ getTransformMode(): 'select' | 'move' | 'rotate' | 'scale' { - return this.renderSystem?.getTransformMode() ?? 'select'; + return this._runtime?.getTransformMode() ?? 'select'; } // ===== Multi-viewport API ===== - // ===== 多视口 API ===== /** * Register a new viewport. - * 注册新视口。 */ registerViewport(id: string, canvasId: string): void { - if (this.bridge) { - this.bridge.registerViewport(id, canvasId); - } + this._runtime?.registerViewport(id, canvasId); } /** * Unregister a viewport. - * 注销视口。 */ unregisterViewport(id: string): void { - if (this.bridge) { - this.bridge.unregisterViewport(id); - } + this._runtime?.unregisterViewport(id); } /** * Set the active viewport. - * 设置活动视口。 */ setActiveViewport(id: string): boolean { - if (this.bridge) { - return this.bridge.setActiveViewport(id); - } - return false; + return this._runtime?.setActiveViewport(id) ?? false; } /** * Set camera for a specific viewport. - * 为特定视口设置相机。 */ setViewportCamera(viewportId: string, config: CameraConfig): void { - if (this.bridge) { - this.bridge.setViewportCamera(viewportId, config); - } + this._runtime?.bridge?.setViewportCamera(viewportId, config); } /** * Get camera for a specific viewport. - * 获取特定视口的相机。 */ getViewportCamera(viewportId: string): CameraConfig | null { - if (this.bridge) { - return this.bridge.getViewportCamera(viewportId); - } - return null; + return this._runtime?.bridge?.getViewportCamera(viewportId) ?? null; } /** * Set viewport configuration. - * 设置视口配置。 */ setViewportConfig(viewportId: string, showGrid: boolean, showGizmos: boolean): void { - if (this.bridge) { - this.bridge.setViewportConfig(viewportId, showGrid, showGizmos); - } + this._runtime?.bridge?.setViewportConfig(viewportId, showGrid, showGizmos); } /** * Resize a specific viewport. - * 调整特定视口大小。 */ resizeViewport(viewportId: string, width: number, height: number): void { - if (this.bridge) { - this.bridge.resizeViewport(viewportId, width, height); - } + this._runtime?.bridge?.resizeViewport(viewportId, width, height); } /** * Render to a specific viewport. - * 渲染到特定视口。 */ renderToViewport(viewportId: string): void { - if (this.bridge) { - this.bridge.renderToViewport(viewportId); - } + this._runtime?.bridge?.renderToViewport(viewportId); } /** * Get all registered viewport IDs. - * 获取所有已注册的视口ID。 */ getViewportIds(): string[] { - if (this.bridge) { - return this.bridge.getViewportIds(); - } - return []; + return this._runtime?.bridge?.getViewportIds() ?? []; + } + + /** + * Get the underlying GameRuntime instance. + * 获取底层 GameRuntime 实例。 + */ + getRuntime(): GameRuntime | null { + return this._runtime; } /** * Dispose engine resources. - * 释放引擎资源。 */ dispose(): void { this.stop(); - // Stop render loop | 停止渲染循环 - if (this.animationFrameId !== null) { - cancelAnimationFrame(this.animationFrameId); - this.animationFrameId = null; + if (this._assetManager) { + this._assetManager.dispose(); + this._assetManager = null; } - // Dispose asset system | 释放资产系统 - if (this.assetManager) { - this.assetManager.dispose(); - this.assetManager = null; - } - this.engineIntegration = null; + this._engineIntegration = null; - // Scene doesn't have a destroy method, just clear reference - // 场景没有destroy方法,只需清除引用 - this.scene = null; - - if (this.bridge) { - this.bridge.dispose(); - this.bridge = null; + if (this._runtime) { + this._runtime.dispose(); + this._runtime = null; } - this.renderSystem = null; - this.initialized = false; + this._initialized = false; } } diff --git a/packages/editor-app/src/services/PluginSDKRegistry.ts b/packages/editor-app/src/services/PluginSDKRegistry.ts index 79c600be..f92393b3 100644 --- a/packages/editor-app/src/services/PluginSDKRegistry.ts +++ b/packages/editor-app/src/services/PluginSDKRegistry.ts @@ -18,7 +18,10 @@ import { EntityStoreService, MessageHub } from '@esengine/editor-core'; import * as editorRuntime from '@esengine/editor-runtime'; import * as ecsFramework from '@esengine/ecs-framework'; import * as behaviorTree from '@esengine/behavior-tree'; -import * as ecsComponents from '@esengine/ecs-components'; +import * as engineCore from '@esengine/engine-core'; +import * as sprite from '@esengine/sprite'; +import * as camera from '@esengine/camera'; +import * as audio from '@esengine/audio'; // 存储服务实例引用(在初始化时设置) let entityStoreInstance: EntityStoreService | null = null; @@ -29,7 +32,10 @@ const SDK_MODULES = { '@esengine/editor-runtime': editorRuntime, '@esengine/ecs-framework': ecsFramework, '@esengine/behavior-tree': behaviorTree, - '@esengine/ecs-components': ecsComponents, + '@esengine/engine-core': engineCore, + '@esengine/sprite': sprite, + '@esengine/camera': camera, + '@esengine/audio': audio, } as const; // 全局变量名称映射(用于插件构建配置) @@ -37,7 +43,10 @@ export const SDK_GLOBALS = { '@esengine/editor-runtime': '__ESENGINE__.editorRuntime', '@esengine/ecs-framework': '__ESENGINE__.ecsFramework', '@esengine/behavior-tree': '__ESENGINE__.behaviorTree', - '@esengine/ecs-components': '__ESENGINE__.ecsComponents', + '@esengine/engine-core': '__ESENGINE__.engineCore', + '@esengine/sprite': '__ESENGINE__.sprite', + '@esengine/camera': '__ESENGINE__.camera', + '@esengine/audio': '__ESENGINE__.audio', } as const; /** @@ -62,7 +71,10 @@ interface ESEngineGlobal { editorRuntime: typeof editorRuntime; ecsFramework: typeof ecsFramework; behaviorTree: typeof behaviorTree; - ecsComponents: typeof ecsComponents; + engineCore: typeof engineCore; + sprite: typeof sprite; + camera: typeof camera; + audio: typeof audio; require: (moduleName: string) => any; api: IPluginAPI; } @@ -117,7 +129,10 @@ export class PluginSDKRegistry { editorRuntime, ecsFramework, behaviorTree, - ecsComponents, + engineCore, + sprite, + camera, + audio, require: this.requireModule.bind(this), api: pluginAPI, }; diff --git a/packages/editor-app/src/services/ProfilerService.ts b/packages/editor-app/src/services/ProfilerService.ts index 0a4519fc..0d47d73f 100644 --- a/packages/editor-app/src/services/ProfilerService.ts +++ b/packages/editor-app/src/services/ProfilerService.ts @@ -70,7 +70,7 @@ type AdvancedProfilerDataListener = (data: AdvancedProfilerDataPayload) => void; export class ProfilerService { private ws: WebSocket | null = null; private isServerRunning = false; - private wsPort: string; + private wsPort: number; private listeners: Set = new Set(); private advancedListeners: Set = new Set(); private currentData: ProfilerData | null = null; @@ -82,7 +82,7 @@ export class ProfilerService { constructor() { const settings = SettingsService.getInstance(); - this.wsPort = settings.get('profiler.port', '8080'); + this.wsPort = settings.get('profiler.port', 8080); this.autoStart = settings.get('profiler.autoStart', true); this.startServerCheck(); @@ -97,8 +97,9 @@ export class ProfilerService { private listenToSettingsChanges(): void { window.addEventListener('settings:changed', ((event: CustomEvent) => { const newPort = event.detail['profiler.port']; - if (newPort && newPort !== this.wsPort) { - this.wsPort = newPort; + if (newPort !== undefined && Number(newPort) !== this.wsPort) { + console.log(`[ProfilerService] Port changed from ${this.wsPort} to ${newPort}`); + this.wsPort = Number(newPort); this.reconnectWithNewPort(); } }) as EventListener); @@ -247,8 +248,8 @@ export class ProfilerService { private async startServer(): Promise { try { - const port = parseInt(this.wsPort); - await invoke('start_profiler_server', { port }); + console.log(`[ProfilerService] Starting server on port ${this.wsPort}`); + await invoke('start_profiler_server', { port: this.wsPort }); this.isServerRunning = true; } catch (error) { // Ignore "already running" error - it's expected in some cases @@ -300,7 +301,7 @@ export class ProfilerService { try { const message = JSON.parse(event.data); if (message.type === 'debug_data' && message.data) { - this.handleDebugData(message.data); + this.handleDebugData(message.data, message.advancedProfiler); } else if (message.type === 'get_raw_entity_list_response' && message.data) { this.handleRawEntityListResponse(message.data); } else if (message.type === 'get_entity_details_response' && message.data) { @@ -338,7 +339,7 @@ export class ProfilerService { } } - private handleDebugData(debugData: any): void { + private handleDebugData(debugData: any, advancedProfiler?: any): void { const performance = debugData.performance; if (!performance) return; @@ -380,18 +381,25 @@ export class ProfilerService { this.notifyListeners(this.currentData); - // 通知高级监听器原始数据 - this.lastRawData = { - performance: debugData.performance, - systems: { - systemsInfo: systems.map(sys => ({ - name: sys.name, - executionTime: sys.executionTime, - entityCount: sys.entityCount, - averageTime: sys.averageTime - })) - } - }; + // 如果有高级性能数据,优先使用它 + if (advancedProfiler) { + this.lastRawData = { + advancedProfiler + }; + } else { + // 否则使用传统数据 + this.lastRawData = { + performance: debugData.performance, + systems: { + systemsInfo: systems.map(sys => ({ + name: sys.name, + executionTime: sys.executionTime, + entityCount: sys.entityCount, + averageTime: sys.averageTime + })) + } + }; + } this.notifyAdvancedListeners(this.lastRawData); // 请求完整的实体列表 diff --git a/packages/editor-app/src/styles/AdvancedProfiler.css b/packages/editor-app/src/styles/AdvancedProfiler.css index 6bdb6046..47d74eb8 100644 --- a/packages/editor-app/src/styles/AdvancedProfiler.css +++ b/packages/editor-app/src/styles/AdvancedProfiler.css @@ -586,9 +586,44 @@ .profiler-table-cell.name .expand-icon { color: #666; flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; + width: 14px; + height: 14px; +} + +.profiler-table-cell.name .expand-icon.clickable { cursor: pointer; } +.profiler-table-cell.name .expand-icon.clickable:hover { + color: #fff; + background: rgba(255, 255, 255, 0.1); + border-radius: 2px; +} + +.profiler-table-cell.name .expand-icon.placeholder { + visibility: hidden; +} + +/* 层级行的背景色变化 */ +.profiler-table-row.depth-1 { + background: rgba(255, 255, 255, 0.02); +} + +.profiler-table-row.depth-2 { + background: rgba(255, 255, 255, 0.04); +} + +.profiler-table-row.depth-3 { + background: rgba(255, 255, 255, 0.06); +} + +.profiler-table-row.depth-4 { + background: rgba(255, 255, 255, 0.08); +} + .profiler-table-cell.name .category-dot { width: 6px; height: 6px; diff --git a/packages/editor-app/src/styles/ConsolePanel.css b/packages/editor-app/src/styles/ConsolePanel.css deleted file mode 100644 index 463a111b..00000000 --- a/packages/editor-app/src/styles/ConsolePanel.css +++ /dev/null @@ -1,445 +0,0 @@ -.console-panel { - display: flex; - flex-direction: column; - height: 100%; - background: var(--color-bg-base); - position: relative; -} - -.console-toolbar { - display: flex; - align-items: center; - justify-content: space-between; - padding: 0 8px; - background: var(--color-bg-elevated); - border-bottom: 1px solid var(--color-border-default); - flex-shrink: 0; - gap: 8px; - height: 26px; -} - -.console-toolbar-left { - display: flex; - align-items: center; - gap: 8px; - flex: 1; -} - -.console-toolbar-right { - display: flex; - align-items: center; - gap: 4px; -} - -.console-btn { - display: inline-flex; - align-items: center; - justify-content: center; - padding: 6px; - background: transparent; - border: none; - border-radius: var(--radius-sm); - color: var(--color-text-secondary); - cursor: pointer; - transition: all var(--transition-fast); -} - -.console-btn:hover { - background: var(--color-bg-hover); - color: var(--color-text-primary); -} - -.console-search { - display: flex; - align-items: center; - gap: 6px; - flex: 1; - max-width: 300px; - background: var(--color-bg-inset); - border: 1px solid var(--color-border-default); - border-radius: var(--radius-sm); - padding: 4px 8px; - color: var(--color-text-tertiary); -} - -.console-search:focus-within { - border-color: var(--color-primary); - color: var(--color-text-primary); -} - -.console-search input { - flex: 1; - background: transparent; - border: none; - outline: none; - color: var(--color-text-primary); - font-size: var(--font-size-xs); - font-family: var(--font-family-mono); -} - -.console-search input::placeholder { - color: var(--color-text-tertiary); -} - -.console-filter-btn { - display: inline-flex; - align-items: center; - gap: 4px; - padding: 4px 8px; - background: transparent; - border: 1px solid transparent; - border-radius: var(--radius-sm); - cursor: pointer; - font-size: var(--font-size-xs); - font-weight: 500; - transition: all var(--transition-fast); - opacity: 0.5; -} - -.console-filter-btn:hover { - opacity: 1; - background: var(--color-bg-hover); -} - -.console-filter-btn.active { - opacity: 1; - border-color: currentColor; - background: rgba(255, 255, 255, 0.05); -} - -.console-filter-btn:nth-child(1) { - color: #10b981; -} - -.console-filter-btn:nth-child(1).active { - color: #34d399; - border-color: #10b981; -} - -.console-filter-btn:nth-child(2) { - color: #858585; -} - -.console-filter-btn:nth-child(2).active { - color: #a0a0a0; - border-color: #858585; -} - -.console-filter-btn:nth-child(3) { - color: #4a9eff; -} - -.console-filter-btn:nth-child(3).active { - color: #6eb3ff; - border-color: #4a9eff; -} - -.console-filter-btn:nth-child(4) { - color: #ffc107; -} - -.console-filter-btn:nth-child(4).active { - color: #ffd54f; - border-color: #ffc107; -} - -.console-filter-btn:nth-child(5) { - color: #f44336; -} - -.console-filter-btn:nth-child(5).active { - color: #ef5350; - border-color: #f44336; -} - -.console-filter-btn span { - font-size: var(--font-size-xs); - font-family: var(--font-family-mono); -} - -.console-content { - flex: 1; - overflow-y: auto; - overflow-x: hidden; - font-family: var(--font-family-mono); - font-size: var(--font-size-xs); - line-height: 1.4; -} - -.console-empty { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - height: 100%; - color: var(--color-text-tertiary); - gap: 8px; -} - -.console-empty p { - margin: 0; - font-size: var(--font-size-sm); -} - -.log-entry { - display: flex; - align-items: flex-start; - gap: 8px; - padding: 4px 12px; - border-bottom: 1px solid var(--color-border-subtle); - transition: background-color var(--transition-fast); -} - -.log-entry:hover { - background: rgba(255, 255, 255, 0.02); -} - -.log-entry-icon { - display: flex; - align-items: center; - padding-top: 2px; - flex-shrink: 0; -} - -.log-entry-time { - color: var(--color-text-tertiary); - font-size: var(--font-size-xs); - white-space: nowrap; - padding-top: 2px; - flex-shrink: 0; - font-variant-numeric: tabular-nums; -} - -.log-entry-source { - color: var(--color-text-secondary); - font-size: var(--font-size-xs); - white-space: nowrap; - padding-top: 2px; - flex-shrink: 0; - opacity: 0.7; -} - -.log-entry-source.source-remote { - color: #4a9eff; - opacity: 1; - font-weight: 600; -} - -.log-entry-client { - color: #10b981; - font-size: calc(var(--font-size-xs) - 2px); - white-space: nowrap; - padding: 1px 6px; - flex-shrink: 0; - background: rgba(16, 185, 129, 0.15); - border: 1px solid rgba(16, 185, 129, 0.4); - border-radius: var(--radius-sm); - font-weight: 600; - font-family: var(--font-family-mono); -} - -.log-entry-remote { - border-left: 2px solid #4a9eff; - background: rgba(74, 158, 255, 0.05); -} - -.log-entry-expander { - display: flex; - align-items: center; - padding-top: 2px; - cursor: pointer; - color: var(--color-text-secondary); - flex-shrink: 0; - transition: color var(--transition-fast); -} - -.log-entry-expander:hover { - color: var(--color-text-primary); -} - -.log-entry-expanded { - flex-direction: column; - align-items: flex-start; -} - -.log-entry-expanded .log-entry-message { - padding-left: 22px; - width: 100%; -} - -.log-entry-message { - flex: 1; - color: var(--color-text-primary); - word-break: break-word; - padding-top: 2px; -} - -.log-message-container { - display: flex; - align-items: flex-start; - gap: 8px; - width: 100%; -} - -.log-message-preview { - opacity: 0.9; - flex: 1; -} - -.log-open-json-btn { - display: inline-flex; - align-items: center; - justify-content: center; - padding: 4px; - background: var(--color-primary); - border: none; - border-radius: var(--radius-sm); - color: white; - cursor: pointer; - opacity: 0.7; - transition: all var(--transition-fast); - flex-shrink: 0; -} - -.log-open-json-btn:hover { - opacity: 1; - transform: scale(1.1); -} - -.log-expand-btn { - display: inline-flex; - align-items: center; - justify-content: center; - margin-left: 8px; - padding: 2px 6px; - background: var(--color-bg-elevated); - border: 1px solid var(--color-border-default); - border-radius: var(--radius-sm); - color: var(--color-primary); - cursor: pointer; - font-size: var(--font-size-xs); - font-family: var(--font-family-mono); - transition: all var(--transition-fast); -} - -.log-expand-btn:hover { - background: var(--color-bg-hover); - border-color: var(--color-primary); -} - -.log-message-json { - margin: 4px 0 0 0; - padding: 8px; - background: rgba(0, 0, 0, 0.3); - border-radius: var(--radius-sm); - border: 1px solid var(--color-border-default); - font-family: var(--font-family-mono); - font-size: var(--font-size-xs); - line-height: 1.5; - overflow: auto; - white-space: pre; - color: #a0e7a0; - max-height: 400px; -} - -.log-message-json::-webkit-scrollbar { - width: 8px; - height: 8px; -} - -.log-message-json::-webkit-scrollbar-track { - background: rgba(0, 0, 0, 0.2); - border-radius: 4px; -} - -.log-message-json::-webkit-scrollbar-thumb { - background: rgba(160, 231, 160, 0.3); - border-radius: 4px; -} - -.log-message-json::-webkit-scrollbar-thumb:hover { - background: rgba(160, 231, 160, 0.5); -} - -.log-entry-debug { - color: var(--color-text-tertiary); -} - -.log-entry-debug .log-entry-icon { - color: #858585; -} - -.log-entry-info .log-entry-icon { - color: #4a9eff; -} - -.log-entry-warn { - background: rgba(255, 193, 7, 0.05); -} - -.log-entry-warn .log-entry-icon { - color: #ffc107; -} - -.log-entry-error { - background: rgba(244, 67, 54, 0.05); -} - -.log-entry-error .log-entry-icon { - color: #f44336; -} - -.console-scroll-to-bottom { - position: absolute; - bottom: 12px; - left: 50%; - transform: translateX(-50%); - padding: 6px 12px; - background: var(--color-primary); - color: var(--color-text-inverse); - border: none; - border-radius: var(--radius-sm); - font-size: var(--font-size-xs); - font-weight: 500; - cursor: pointer; - box-shadow: var(--shadow-md); - transition: all var(--transition-fast); - z-index: var(--z-index-above); -} - -.console-scroll-to-bottom:hover { - background: var(--color-primary-hover); - transform: translateX(-50%) translateY(-2px); - box-shadow: var(--shadow-lg); -} - -.console-scroll-to-bottom:active { - transform: translateX(-50%) translateY(0); -} - -/* Scrollbar */ -.console-content::-webkit-scrollbar { - width: 10px; -} - -.console-content::-webkit-scrollbar-track { - background: var(--color-bg-elevated); -} - -.console-content::-webkit-scrollbar-thumb { - background: var(--color-border-default); - border-radius: 5px; -} - -.console-content::-webkit-scrollbar-thumb:hover { - background: var(--color-text-secondary); -} - -@media (prefers-reduced-motion: reduce) { - .console-btn, - .console-filter-btn, - .log-entry, - .console-scroll-to-bottom { - transition: none; - } -} diff --git a/packages/editor-app/src/styles/ContextMenu.css b/packages/editor-app/src/styles/ContextMenu.css index 2e933bc5..3ec496aa 100644 --- a/packages/editor-app/src/styles/ContextMenu.css +++ b/packages/editor-app/src/styles/ContextMenu.css @@ -5,24 +5,30 @@ border-radius: 4px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5); padding: 4px 0; - min-width: 180px; + min-width: 200px; z-index: var(--z-index-popover); font-size: 13px; } +.context-menu.submenu { + position: fixed; +} + .context-menu-item { display: flex; align-items: center; gap: 8px; padding: 6px 12px; + padding-right: 24px; color: #cccccc; cursor: pointer; transition: background-color 0.1s ease; user-select: none; + position: relative; } .context-menu-item:hover:not(.disabled) { - background-color: #383838; + background-color: #094771; color: #ffffff; } @@ -32,6 +38,10 @@ opacity: 0.5; } +.context-menu-item.has-submenu { + padding-right: 28px; +} + .context-menu-icon { display: flex; align-items: center; @@ -51,8 +61,41 @@ white-space: nowrap; } +.context-menu-shortcut { + color: #888888; + font-size: 11px; + margin-left: 24px; + white-space: nowrap; +} + +.context-menu-item:hover:not(.disabled) .context-menu-shortcut { + color: #aaaaaa; +} + +.context-menu-arrow { + position: absolute; + right: 8px; + top: 50%; + transform: translateY(-50%); + color: #888888; +} + +.context-menu-item:hover:not(.disabled) .context-menu-arrow { + color: #ffffff; +} + .context-menu-separator { height: 1px; background-color: #3e3e42; margin: 4px 0; } + +/* Section header in submenu */ +.context-menu-section-header { + padding: 4px 12px; + color: #888888; + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.5px; + cursor: default; +} diff --git a/packages/editor-app/src/styles/OutputLogPanel.css b/packages/editor-app/src/styles/OutputLogPanel.css index 2bc12939..ead429e1 100644 --- a/packages/editor-app/src/styles/OutputLogPanel.css +++ b/packages/editor-app/src/styles/OutputLogPanel.css @@ -318,15 +318,6 @@ background: #5a5a5a; } -/* Hide timestamp/source based on settings */ -.output-log-content.hide-timestamp .output-log-entry-time { - display: none; -} - -.output-log-content.hide-source .output-log-entry-source { - display: none; -} - /* Empty State */ .output-log-empty { display: flex; @@ -345,12 +336,7 @@ /* ==================== Log Entry ==================== */ .output-log-entry { - display: flex; - align-items: flex-start; - gap: 8px; - padding: 4px 12px; border-bottom: 1px solid #2a2a2a; - line-height: 1.4; transition: background 0.1s ease; } @@ -358,6 +344,19 @@ background: rgba(255, 255, 255, 0.02); } +.output-log-entry.expanded { + background: rgba(255, 255, 255, 0.03); +} + +.output-log-entry-main { + display: flex; + align-items: flex-start; + gap: 8px; + padding: 6px 12px; + line-height: 1.4; + cursor: pointer; +} + .output-log-entry-icon { display: flex; align-items: center; @@ -389,78 +388,69 @@ font-weight: 600; } -.output-log-entry-client { - color: #10b981; - font-size: 10px; - white-space: nowrap; - padding: 1px 6px; - flex-shrink: 0; - background: rgba(16, 185, 129, 0.15); - border: 1px solid rgba(16, 185, 129, 0.4); - border-radius: 3px; - font-weight: 600; -} - .output-log-entry-message { flex: 1; color: #e0e0e0; word-break: break-word; padding-top: 2px; + white-space: pre-wrap; } -.output-log-message-container { - display: flex; - align-items: flex-start; - gap: 8px; -} - -.output-log-message-text { - flex: 1; -} - -.output-log-message-preview { - opacity: 0.9; -} - -.output-log-expand-btn { - display: inline; - margin-left: 8px; - padding: 2px 6px; - background: #3c3c3c; - border: 1px solid #4a4a4a; - border-radius: 3px; - color: #3b82f6; - font-size: 10px; - cursor: pointer; - transition: all 0.1s ease; -} - -.output-log-expand-btn:hover { - background: #4a4a4a; - border-color: #3b82f6; -} - -.output-log-json-btn { +.output-log-entry-copy { display: flex; align-items: center; justify-content: center; - padding: 2px 6px; - background: #3b82f6; - border: none; - border-radius: 3px; - color: #fff; - font-size: 10px; - font-weight: 600; + width: 22px; + height: 22px; + padding: 0; + background: transparent; + border: 1px solid transparent; + border-radius: 4px; + color: #666; cursor: pointer; - opacity: 0.8; - transition: all 0.1s ease; + opacity: 0; + transition: all 0.15s ease; flex-shrink: 0; } -.output-log-json-btn:hover { +.output-log-entry:hover .output-log-entry-copy { opacity: 1; } +.output-log-entry-copy:hover { + background: #3c3c3c; + border-color: #5a5a5a; + color: #e0e0e0; +} + +/* Stack Trace (expanded) */ +.output-log-entry-stack { + padding: 8px 12px 12px 42px; + background: rgba(0, 0, 0, 0.2); + border-top: 1px solid #2a2a2a; +} + +.output-log-stack-header { + color: #888; + font-size: 11px; + margin-bottom: 6px; + font-weight: 600; +} + +.output-log-stack-line { + color: #888; + font-size: 11px; + line-height: 1.6; + white-space: pre; + padding-left: 12px; +} + +.output-log-stack-line:hover { + color: #4a9eff; + background: rgba(74, 158, 255, 0.1); + cursor: pointer; +} + /* Log Level Styles */ .output-log-entry-debug { color: #858585; @@ -470,10 +460,18 @@ color: #858585; } +.output-log-entry-debug .output-log-entry-message { + color: #858585; +} + .output-log-entry-info .output-log-entry-icon { color: #4a9eff; } +.output-log-entry-info .output-log-entry-message { + color: #e0e0e0; +} + .output-log-entry-warn { background: rgba(255, 193, 7, 0.05); } @@ -482,6 +480,10 @@ color: #ffc107; } +.output-log-entry-warn .output-log-entry-message { + color: #ffc107; +} + .output-log-entry-error { background: rgba(244, 67, 54, 0.08); } @@ -490,6 +492,10 @@ color: #f44336; } +.output-log-entry-error .output-log-entry-message { + color: #f44336; +} + .log-entry-remote { border-left: 2px solid #4a9eff; background: rgba(74, 158, 255, 0.03); diff --git a/packages/editor-app/src/styles/ProfilerWindow.css b/packages/editor-app/src/styles/ProfilerWindow.css index 98641c1f..a9535b7f 100644 --- a/packages/editor-app/src/styles/ProfilerWindow.css +++ b/packages/editor-app/src/styles/ProfilerWindow.css @@ -88,6 +88,30 @@ } } +.profiler-window-controls { + display: flex; + align-items: center; + gap: 4px; +} + +.profiler-window-btn { + padding: 6px; + background: transparent; + border: none; + border-radius: var(--radius-sm); + color: var(--color-text-secondary); + cursor: pointer; + transition: all var(--transition-fast); + display: flex; + align-items: center; + justify-content: center; +} + +.profiler-window-btn:hover { + background: var(--color-bg-hover); + color: var(--color-text-primary); +} + .profiler-window-close { padding: 6px; background: transparent; @@ -106,6 +130,24 @@ color: var(--color-text-primary); } +/* Fullscreen styles */ +.profiler-window-overlay.fullscreen { + background: rgba(0, 0, 0, 0.95); +} + +.profiler-window.fullscreen { + border: none; + border-radius: 0; + max-height: none; + height: 100vh; + width: 100vw; + max-width: none; +} + +.profiler-window.fullscreen .profiler-window-header { + border-radius: 0; +} + .profiler-window-toolbar { display: flex; align-items: center; diff --git a/packages/editor-app/src/styles/SceneHierarchy.css b/packages/editor-app/src/styles/SceneHierarchy.css index 9fcaa2bb..a5ceee5c 100644 --- a/packages/editor-app/src/styles/SceneHierarchy.css +++ b/packages/editor-app/src/styles/SceneHierarchy.css @@ -243,10 +243,20 @@ opacity: 0.5; } -.outliner-item.drop-target { +/* Drop Indicators */ +.outliner-item.drop-before { border-top: 2px solid #4a9eff; } +.outliner-item.drop-after { + border-bottom: 2px solid #4a9eff; +} + +.outliner-item.drop-inside { + background: rgba(74, 158, 255, 0.2); + box-shadow: inset 0 0 0 1px #4a9eff; +} + .outliner-item.disabled { opacity: 0.5; } @@ -291,6 +301,17 @@ flex-shrink: 0; } +.outliner-item-expand.clickable { + cursor: pointer; + border-radius: 3px; + transition: background 0.15s ease, color 0.15s ease; +} + +.outliner-item-expand.clickable:hover { + color: #fff; + background: rgba(255, 255, 255, 0.1); +} + .outliner-item-expand:hover { color: #ccc; } diff --git a/packages/editor-core/package.json b/packages/editor-core/package.json index 19bbd3f2..8e7a0a61 100644 --- a/packages/editor-core/package.json +++ b/packages/editor-core/package.json @@ -33,15 +33,15 @@ "build:watch": "tsc --watch", "rebuild": "npm run clean && npm run build", "build:npm": "npm run build && node build-rollup.cjs", - "test": "jest --config jest.config.cjs", - "test:watch": "jest --watch --config jest.config.cjs", - "test:coverage": "jest --coverage --config jest.config.cjs", "lint": "eslint \"src/**/*.{ts,tsx}\"", "lint:fix": "eslint \"src/**/*.{ts,tsx}\" --fix" }, "author": "yhh", "license": "MIT", "devDependencies": { + "@esengine/ecs-framework": "workspace:*", + "@esengine/asset-system": "workspace:*", + "@esengine/engine-core": "workspace:*", "@babel/core": "^7.28.3", "@babel/plugin-transform-nullish-coalescing-operator": "^7.27.1", "@babel/plugin-transform-optional-chaining": "^7.27.1", @@ -61,11 +61,7 @@ "rollup": "^4.42.0", "rollup-plugin-dts": "^6.2.1", "ts-jest": "^29.4.0", - "typescript": "^5.8.3" - }, - "peerDependencies": { - "@esengine/asset-system": "*", - "@esengine/ecs-framework": "^2.2.8", + "typescript": "^5.8.3", "react": "^18.2.0", "rxjs": "^7.8.0", "tsyringe": "^4.8.0" diff --git a/packages/editor-core/rollup.config.cjs b/packages/editor-core/rollup.config.cjs index 84ad1602..bc68fad0 100644 --- a/packages/editor-core/rollup.config.cjs +++ b/packages/editor-core/rollup.config.cjs @@ -15,7 +15,13 @@ const banner = `/** * @license ${pkg.license} */`; -const external = ['@esengine/ecs-framework']; +const external = [ + '@esengine/ecs-framework', + 'react', + 'react-dom', + 'react/jsx-runtime', + /^@types\// +]; const commonPlugins = [ resolve({ diff --git a/packages/editor-core/src/Plugin/IPluginLoader.ts b/packages/editor-core/src/Plugin/IPluginLoader.ts index 590d37b0..ea386662 100644 --- a/packages/editor-core/src/Plugin/IPluginLoader.ts +++ b/packages/editor-core/src/Plugin/IPluginLoader.ts @@ -1,71 +1,29 @@ /** - * 插件加载器接口 - * Plugin loader interfaces + * 编辑器模块接口 + * Editor module interfaces + * + * 定义编辑器专用的模块接口和 UI 描述符类型。 + * Define editor-specific module interfaces and UI descriptor types. */ -import type { IScene, ServiceContainer, ComponentRegistry } from '@esengine/ecs-framework'; -import type { PluginDescriptor } from './PluginDescriptor'; +import type { ServiceContainer } from '@esengine/ecs-framework'; -/** - * 系统创建上下文 - * System creation context - */ -export interface SystemContext { - /** 是否为编辑器模式 | Is editor mode */ - isEditor: boolean; +// 从 PluginDescriptor 重新导出(来源于 engine-core) +export type { + PluginCategory, + LoadingPhase, + ModuleType, + ModuleDescriptor, + PluginDependency, + PluginDescriptor, + SystemContext, + IRuntimeModule, + IPlugin +} from './PluginDescriptor'; - /** 引擎桥接(如有) | Engine bridge (if available) */ - engineBridge?: any; - - /** 渲染系统(如有) | Render system (if available) */ - renderSystem?: any; - - /** 其他已创建的系统引用 | Other created system references */ - [key: string]: any; -} - -/** - * 运行时模块加载器 - * Runtime module loader - */ -export interface IRuntimeModuleLoader { - /** - * 注册组件到 ComponentRegistry - * Register components to ComponentRegistry - */ - registerComponents(registry: typeof ComponentRegistry): void; - - /** - * 注册服务到 ServiceContainer - * Register services to ServiceContainer - */ - registerServices?(services: ServiceContainer): void; - - /** - * 为场景创建系统 - * Create systems for scene - */ - createSystems?(scene: IScene, context: SystemContext): void; - - /** - * 所有系统创建完成后调用 - * 用于处理跨插件的系统依赖关系 - * Called after all systems are created, used for cross-plugin system dependencies - */ - onSystemsCreated?(scene: IScene, context: SystemContext): void; - - /** - * 模块初始化完成回调 - * Module initialization complete callback - */ - onInitialize?(): Promise; - - /** - * 模块销毁回调 - * Module destroy callback - */ - onDestroy?(): void; -} +// ============================================================================ +// UI 描述符类型 | UI Descriptor Types +// ============================================================================ /** * 面板位置 @@ -146,8 +104,8 @@ export interface ToolbarItemDescriptor { } /** - * 组件检视器提供者(简化版) - * Component inspector provider (simplified) + * 组件检视器提供者 + * Component inspector provider */ export interface ComponentInspectorProviderDef { /** 组件类型名 | Component type name */ @@ -235,6 +193,33 @@ export interface ISerializer { deserialize(data: Uint8Array): T; } +/** + * 文件创建模板 + * File creation template + */ +export interface FileCreationTemplate { + /** 模板ID | Template ID */ + id: string; + /** 标签 | Label */ + label: string; + /** 扩展名 | Extension */ + extension: string; + /** 图标 | Icon */ + icon?: string; + /** 分类 | Category */ + category?: string; + /** + * 获取文件内容 | Get file content + * @param fileName 文件名(不含路径,含扩展名) + * @returns 文件内容字符串 + */ + getContent: (fileName: string) => string | Promise; +} + +// ============================================================================ +// 编辑器模块接口 | Editor Module Interface +// ============================================================================ + /** * 编辑器模块加载器 * Editor module loader @@ -327,43 +312,22 @@ export interface IEditorModuleLoader { setLocale?(locale: string): void; } -/** - * 统一插件加载器 - * Unified plugin loader - */ -export interface IPluginLoader { - /** 插件描述符 | Plugin descriptor */ - readonly descriptor: PluginDescriptor; - - /** 运行时模块(可选) | Runtime module (optional) */ - readonly runtimeModule?: IRuntimeModuleLoader; - - /** 编辑器模块(可选) | Editor module (optional) */ - readonly editorModule?: IEditorModuleLoader; -} +// ============================================================================ +// 类型别名(向后兼容)| Type Aliases (backward compatibility) +// ============================================================================ /** - * 文件创建模板 - * File creation template + * IPluginLoader 类型别名 * - * 插件通过 getContent 提供文件内容,编辑器负责写入文件。 - * 这样可以避免插件直接访问文件系统带来的权限问题。 + * @deprecated 使用 IPlugin 代替。IPluginLoader 只是 IPlugin 的别名。 + * @deprecated Use IPlugin instead. IPluginLoader is just an alias for IPlugin. */ -export interface FileCreationTemplate { - /** 模板ID | Template ID */ - id: string; - /** 标签 | Label */ - label: string; - /** 扩展名 | Extension */ - extension: string; - /** 图标 | Icon */ - icon?: string; - /** 分类 | Category */ - category?: string; - /** - * 获取文件内容 | Get file content - * @param fileName 文件名(不含路径,含扩展名) - * @returns 文件内容字符串 - */ - getContent: (fileName: string) => string | Promise; -} +export type { IPlugin as IPluginLoader } from './PluginDescriptor'; + +/** + * IRuntimeModuleLoader 类型别名 + * + * @deprecated 使用 IRuntimeModule 代替。 + * @deprecated Use IRuntimeModule instead. + */ +export type { IRuntimeModule as IRuntimeModuleLoader } from './PluginDescriptor'; diff --git a/packages/editor-core/src/Plugin/PluginDescriptor.ts b/packages/editor-core/src/Plugin/PluginDescriptor.ts index e5e5b90d..37a2308d 100644 --- a/packages/editor-core/src/Plugin/PluginDescriptor.ts +++ b/packages/editor-core/src/Plugin/PluginDescriptor.ts @@ -1,155 +1,23 @@ /** - * 插件系统类型定义 - * Plugin system type definitions + * 插件描述符类型 + * Plugin descriptor types + * + * 从 @esengine/engine-core 重新导出基础类型,并添加编辑器专用类型。 + * Re-export base types from @esengine/engine-core, and add editor-specific types. */ -/** - * 插件类别 - * Plugin category - */ -export type PluginCategory = - | 'core' // 核心功能 | Core functionality - | 'rendering' // 渲染相关 | Rendering - | 'ui' // UI 系统 | UI System - | 'ai' // AI/行为树 | AI/Behavior - | 'physics' // 物理引擎 | Physics - | 'audio' // 音频系统 | Audio - | 'networking' // 网络功能 | Networking - | 'tools' // 工具/编辑器扩展 | Tools/Editor extensions - | 'scripting' // 脚本/蓝图 | Scripting/Blueprint - | 'content'; // 内容/资源 | Content/Assets - -/** - * 加载阶段 - 控制插件模块的加载顺序 - * Loading phase - controls the loading order of plugin modules - */ -export type LoadingPhase = - | 'earliest' // 最早加载(核心模块) | Earliest (core modules) - | 'preDefault' // 默认之前 | Before default - | 'default' // 默认阶段 | Default phase - | 'postDefault' // 默认之后 | After default - | 'postEngine'; // 引擎初始化后 | After engine init - -/** - * 模块类型 - * Module type - */ -export type ModuleType = 'runtime' | 'editor'; - -/** - * 模块描述符 - 描述插件内的一个模块 - * Module descriptor - describes a module within a plugin - */ -export interface ModuleDescriptor { - /** 模块名称 | Module name */ - name: string; - - /** 模块类型 | Module type */ - type: ModuleType; - - /** 加载阶段 | Loading phase */ - loadingPhase?: LoadingPhase; - - /** 模块入口文件(相对路径) | Module entry file (relative path) */ - entry?: string; - - // ===== 运行时模块配置 | Runtime module config ===== - - /** 导出的组件类名列表 | Exported component class names */ - components?: string[]; - - /** 导出的系统类名列表 | Exported system class names */ - systems?: string[]; - - /** 导出的服务类名列表 | Exported service class names */ - services?: string[]; - - // ===== 编辑器模块配置 | Editor module config ===== - - /** 注册的面板ID列表 | Registered panel IDs */ - panels?: string[]; - - /** 注册的检视器类型列表 | Registered inspector types */ - inspectors?: string[]; - - /** 注册的 Gizmo 提供者列表 | Registered Gizmo providers */ - gizmoProviders?: string[]; - - /** 注册的编译器列表 | Registered compilers */ - compilers?: string[]; - - /** 注册的文件处理器扩展名 | Registered file handler extensions */ - fileHandlers?: string[]; -} - -/** - * 插件依赖 - * Plugin dependency - */ -export interface PluginDependency { - /** 依赖的插件ID | Dependent plugin ID */ - id: string; - - /** 版本要求(semver) | Version requirement (semver) */ - version?: string; - - /** 是否可选 | Optional */ - optional?: boolean; -} - -/** - * 插件描述符 - 对应 plugin.json 文件 - * Plugin descriptor - corresponds to plugin.json - */ -export interface PluginDescriptor { - /** 插件唯一标识符,如 "@esengine/tilemap" | Unique plugin ID */ - id: string; - - /** 显示名称 | Display name */ - name: string; - - /** 版本号 | Version */ - version: string; - - /** 描述 | Description */ - description?: string; - - /** 作者 | Author */ - author?: string; - - /** 许可证 | License */ - license?: string; - - /** 插件类别 | Plugin category */ - category: PluginCategory; - - /** 标签(用于搜索) | Tags (for search) */ - tags?: string[]; - - /** 图标(Lucide 图标名) | Icon (Lucide icon name) */ - icon?: string; - - /** 是否默认启用 | Enabled by default */ - enabledByDefault: boolean; - - /** 是否可以包含内容资产 | Can contain content assets */ - canContainContent: boolean; - - /** 是否为引擎内置插件 | Is engine built-in plugin */ - isEnginePlugin: boolean; - - /** 是否为核心插件(不可禁用) | Is core plugin (cannot be disabled) */ - isCore?: boolean; - - /** 模块列表 | Module list */ - modules: ModuleDescriptor[]; - - /** 依赖列表 | Dependency list */ - dependencies?: PluginDependency[]; - - /** 平台要求 | Platform requirements */ - platforms?: ('web' | 'desktop' | 'mobile')[]; -} +// 从 engine-core 重新导出所有插件相关类型 +export type { + PluginCategory, + LoadingPhase, + ModuleType, + ModuleDescriptor, + PluginDependency, + PluginDescriptor, + SystemContext, + IRuntimeModule, + IPlugin +} from '@esengine/engine-core'; /** * 插件状态 diff --git a/packages/editor-core/src/Plugin/PluginManager.ts b/packages/editor-core/src/Plugin/PluginManager.ts index b809498c..0b2e05cc 100644 --- a/packages/editor-core/src/Plugin/PluginManager.ts +++ b/packages/editor-core/src/Plugin/PluginManager.ts @@ -9,16 +9,18 @@ import type { PluginDescriptor, PluginState, PluginCategory, - LoadingPhase + LoadingPhase, + IPlugin } from './PluginDescriptor'; import type { - IPluginLoader, - SystemContext + SystemContext, + IEditorModuleLoader } from './IPluginLoader'; import { EntityCreationRegistry } from '../Services/EntityCreationRegistry'; import { ComponentActionRegistry } from '../Services/ComponentActionRegistry'; import { FileActionRegistry } from '../Services/FileActionRegistry'; import { UIRegistry } from '../Services/UIRegistry'; +import { MessageHub } from '../Services/MessageHub'; const logger = createLogger('PluginManager'); @@ -28,13 +30,65 @@ const logger = createLogger('PluginManager'); */ export const IPluginManager = Symbol.for('IPluginManager'); +/** + * 标准化后的插件描述符(所有字段都有值) + * Normalized plugin descriptor (all fields have values) + */ +export interface NormalizedPluginDescriptor { + id: string; + name: string; + version: string; + description: string; + category: PluginCategory; + tags: string[]; + icon?: string; + enabledByDefault: boolean; + canContainContent: boolean; + isEnginePlugin: boolean; + isCore: boolean; + modules: Array<{ name: string; type: 'runtime' | 'editor'; loadingPhase: LoadingPhase }>; + dependencies: Array<{ id: string; version?: string; optional?: boolean }>; + platforms: ('web' | 'desktop' | 'mobile')[]; +} + +/** + * 标准化后的插件(内部使用) + * Normalized plugin (internal use) + */ +export interface NormalizedPlugin { + descriptor: NormalizedPluginDescriptor; + runtimeModule?: IPlugin['runtimeModule']; + editorModule?: IEditorModuleLoader; +} + +/** + * 插件注册的资源(用于卸载时清理) + * Resources registered by plugin (for cleanup on unload) + */ +export interface PluginRegisteredResources { + /** 注册的面板ID | Registered panel IDs */ + panelIds: string[]; + /** 注册的菜单ID | Registered menu IDs */ + menuIds: string[]; + /** 注册的工具栏ID | Registered toolbar IDs */ + toolbarIds: string[]; + /** 注册的实体模板ID | Registered entity template IDs */ + entityTemplateIds: string[]; + /** 注册的组件操作 | Registered component actions */ + componentActions: Array<{ componentName: string; actionId: string }>; + /** 注册的文件处理器 | Registered file handlers */ + fileHandlers: any[]; + /** 注册的文件模板 | Registered file templates */ + fileTemplates: any[]; +} + /** * 已注册的插件信息 * Registered plugin info */ export interface RegisteredPlugin { - /** 插件加载器 | Plugin loader */ - loader: IPluginLoader; + /** 标准化后的插件 | Normalized plugin */ + plugin: NormalizedPlugin; /** 插件状态 | Plugin state */ state: PluginState; /** 错误信息 | Error info */ @@ -45,6 +99,8 @@ export interface RegisteredPlugin { loadedAt?: number; /** 激活时间 | Activation time */ activatedAt?: number; + /** 插件注册的资源 | Resources registered by plugin */ + registeredResources?: PluginRegisteredResources; } /** @@ -80,9 +136,29 @@ export class PluginManager implements IService { private plugins: Map = new Map(); private initialized = false; private editorInitialized = false; + private services: ServiceContainer | null = null; + private currentScene: IScene | null = null; + private currentContext: SystemContext | null = null; constructor() {} + /** + * 设置服务容器(用于动态启用插件) + * Set service container (for dynamic plugin enabling) + */ + setServiceContainer(services: ServiceContainer): void { + this.services = services; + } + + /** + * 设置当前场景和上下文(用于动态创建系统) + * Set current scene and context (for dynamic system creation) + */ + setSceneContext(scene: IScene, context: SystemContext): void { + this.currentScene = scene; + this.currentContext = context; + } + /** * 释放资源 * Dispose resources @@ -91,25 +167,60 @@ export class PluginManager implements IService { this.reset(); } + /** + * 标准化插件描述符,填充默认值 + * Normalize plugin descriptor, fill in defaults + */ + private normalizePlugin(input: IPlugin): NormalizedPlugin { + const d = input.descriptor; + return { + descriptor: { + id: d.id, + name: d.name, + version: d.version, + description: d.description ?? '', + category: d.category ?? 'tools', + tags: d.tags ?? [], + icon: d.icon, + enabledByDefault: d.enabledByDefault ?? false, + canContainContent: d.canContainContent ?? false, + isEnginePlugin: d.isEnginePlugin ?? true, + isCore: d.isCore ?? false, + modules: (d.modules ?? [{ name: 'Runtime', type: 'runtime' as const, loadingPhase: 'default' as const }]).map((m: { name: string; type: 'runtime' | 'editor'; loadingPhase?: LoadingPhase }) => ({ + name: m.name, + type: m.type, + loadingPhase: m.loadingPhase ?? 'default' as LoadingPhase + })), + dependencies: d.dependencies ?? [], + platforms: d.platforms ?? ['web', 'desktop'] + }, + runtimeModule: input.runtimeModule, + editorModule: input.editorModule as IEditorModuleLoader | undefined + }; + } + /** * 注册插件 * Register plugin + * + * 接受任何符合 IPlugin 接口的插件,内部会标准化所有字段。 + * Accepts any plugin conforming to IPlugin interface, normalizes all fields internally. */ - register(loader: IPluginLoader): void { - if (!loader) { - logger.error('Cannot register plugin: loader is null or undefined'); + register(plugin: IPlugin): void { + if (!plugin) { + logger.error('Cannot register plugin: plugin is null or undefined'); return; } - if (!loader.descriptor) { - logger.error('Cannot register plugin: descriptor is null or undefined', loader); + if (!plugin.descriptor) { + logger.error('Cannot register plugin: descriptor is null or undefined', plugin); return; } - const { id } = loader.descriptor; + const { id } = plugin.descriptor; if (!id) { - logger.error('Cannot register plugin: descriptor.id is null or undefined', loader.descriptor); + logger.error('Cannot register plugin: descriptor.id is null or undefined', plugin.descriptor); return; } @@ -118,36 +229,42 @@ export class PluginManager implements IService { return; } - const enabled = loader.descriptor.isCore || loader.descriptor.enabledByDefault; + const normalized = this.normalizePlugin(plugin); + const enabled = normalized.descriptor.isCore || normalized.descriptor.enabledByDefault; this.plugins.set(id, { - loader, + plugin: normalized, state: 'loaded', enabled, loadedAt: Date.now() }); - logger.info(`Plugin registered: ${id} (${loader.descriptor.name})`); + logger.info(`Plugin registered: ${id} (${normalized.descriptor.name})`); } /** - * 启用插件 - * Enable plugin + * 启用插件(动态加载编辑器模块和运行时系统) + * Enable plugin (dynamically load editor module and runtime systems) */ - enable(pluginId: string): boolean { + async enable(pluginId: string): Promise { const plugin = this.plugins.get(pluginId); if (!plugin) { logger.error(`Plugin ${pluginId} not found`); return false; } - if (plugin.loader.descriptor.isCore) { + if (plugin.plugin.descriptor.isCore) { logger.warn(`Core plugin ${pluginId} cannot be disabled/enabled`); return false; } + if (plugin.enabled) { + logger.warn(`Plugin ${pluginId} is already enabled`); + return true; + } + // 检查依赖 - const deps = plugin.loader.descriptor.dependencies || []; + const deps = plugin.plugin.descriptor.dependencies; for (const dep of deps) { if (dep.optional) continue; const depPlugin = this.plugins.get(dep.id); @@ -158,31 +275,57 @@ export class PluginManager implements IService { } plugin.enabled = true; - plugin.state = 'loaded'; - logger.info(`Plugin enabled: ${pluginId}`); - return true; + plugin.state = 'loading'; + + try { + // 动态加载编辑器模块 + if (this.services && this.editorInitialized) { + await this.activatePluginEditor(pluginId); + } + + // 动态加载运行时模块 + if (this.currentScene && this.currentContext && this.initialized) { + await this.activatePluginRuntime(pluginId); + } + + plugin.state = 'active'; + plugin.activatedAt = Date.now(); + logger.info(`Plugin enabled and activated: ${pluginId}`); + return true; + } catch (e) { + logger.error(`Failed to activate plugin ${pluginId}:`, e); + plugin.state = 'error'; + plugin.error = e as Error; + plugin.enabled = false; + return false; + } } /** - * 禁用插件 - * Disable plugin + * 禁用插件(动态卸载编辑器模块和运行时系统) + * Disable plugin (dynamically unload editor module and runtime systems) */ - disable(pluginId: string): boolean { + async disable(pluginId: string): Promise { const plugin = this.plugins.get(pluginId); if (!plugin) { logger.error(`Plugin ${pluginId} not found`); return false; } - if (plugin.loader.descriptor.isCore) { + if (plugin.plugin.descriptor.isCore) { logger.warn(`Core plugin ${pluginId} cannot be disabled`); return false; } + if (!plugin.enabled) { + logger.warn(`Plugin ${pluginId} is already disabled`); + return true; + } + // 检查是否有其他插件依赖此插件 for (const [id, p] of this.plugins) { if (!p.enabled || id === pluginId) continue; - const deps = p.loader.descriptor.dependencies || []; + const deps = p.plugin.descriptor.dependencies; const hasDep = deps.some(d => d.id === pluginId && !d.optional); if (hasDep) { logger.error(`Cannot disable ${pluginId}: plugin ${id} depends on it`); @@ -190,10 +333,318 @@ export class PluginManager implements IService { } } - plugin.enabled = false; - plugin.state = 'disabled'; - logger.info(`Plugin disabled: ${pluginId}`); - return true; + try { + // 卸载编辑器模块 + if (this.services) { + await this.deactivatePluginEditor(pluginId); + } + + // 卸载运行时模块(清理系统) + if (this.currentScene) { + this.deactivatePluginRuntime(pluginId); + } + + plugin.enabled = false; + plugin.state = 'disabled'; + plugin.registeredResources = undefined; + logger.info(`Plugin disabled: ${pluginId}`); + return true; + } catch (e) { + logger.error(`Failed to deactivate plugin ${pluginId}:`, e); + return false; + } + } + + /** + * 动态激活插件的编辑器模块 + * Dynamically activate plugin's editor module + */ + private async activatePluginEditor(pluginId: string): Promise { + const plugin = this.plugins.get(pluginId); + if (!plugin || !this.services) { + logger.warn(`activatePluginEditor: skipping ${pluginId} (plugin=${!!plugin}, services=${!!this.services})`); + return; + } + + const editorModule = plugin.plugin.editorModule; + if (!editorModule) { + logger.debug(`activatePluginEditor: ${pluginId} has no editorModule`); + return; + } + + logger.info(`activatePluginEditor: activating ${pluginId}`); + + // 初始化资源跟踪 + const resources: PluginRegisteredResources = { + panelIds: [], + menuIds: [], + toolbarIds: [], + entityTemplateIds: [], + componentActions: [], + fileHandlers: [], + fileTemplates: [] + }; + + // 获取注册表服务 + const entityCreationRegistry = this.services.tryResolve(EntityCreationRegistry); + const componentActionRegistry = this.services.tryResolve(ComponentActionRegistry); + const fileActionRegistry = this.services.tryResolve(FileActionRegistry); + const uiRegistry = this.services.tryResolve(UIRegistry); + + // 安装编辑器模块 + await editorModule.install(this.services); + logger.debug(`Editor module installed: ${pluginId}`); + + // 注册实体创建模板 + if (entityCreationRegistry && editorModule.getEntityCreationTemplates) { + const templates = editorModule.getEntityCreationTemplates(); + logger.info(`[${pluginId}] getEntityCreationTemplates returned ${templates?.length ?? 0} templates`); + if (templates && templates.length > 0) { + entityCreationRegistry.registerMany(templates); + resources.entityTemplateIds = templates.map(t => t.id); + logger.info(`Registered ${templates.length} entity templates from: ${pluginId}`, templates.map(t => t.id)); + } + } else { + logger.debug(`[${pluginId}] entityCreationRegistry=${!!entityCreationRegistry}, hasGetEntityCreationTemplates=${!!editorModule.getEntityCreationTemplates}`); + } + + // 注册组件操作 + if (componentActionRegistry && editorModule.getComponentActions) { + const actions = editorModule.getComponentActions(); + if (actions && actions.length > 0) { + for (const action of actions) { + componentActionRegistry.register(action); + resources.componentActions.push({ + componentName: action.componentName, + actionId: action.id + }); + } + logger.debug(`Registered ${actions.length} component actions from: ${pluginId}`); + } + } + + // 注册文件操作处理器 + if (fileActionRegistry && editorModule.getFileActionHandlers) { + const handlers = editorModule.getFileActionHandlers(); + if (handlers && handlers.length > 0) { + for (const handler of handlers) { + fileActionRegistry.registerActionHandler(handler); + resources.fileHandlers.push(handler); + } + logger.debug(`Registered ${handlers.length} file action handlers from: ${pluginId}`); + } + } + + // 注册文件创建模板 + if (fileActionRegistry && editorModule.getFileCreationTemplates) { + const templates = editorModule.getFileCreationTemplates(); + if (templates && templates.length > 0) { + for (const template of templates) { + fileActionRegistry.registerCreationTemplate(template); + resources.fileTemplates.push(template); + } + logger.debug(`Registered ${templates.length} file creation templates from: ${pluginId}`); + } + } + + // 注册面板 + if (uiRegistry && editorModule.getPanels) { + const panels = editorModule.getPanels(); + if (panels && panels.length > 0) { + uiRegistry.registerPanels(panels); + resources.panelIds = panels.map(p => p.id); + logger.debug(`Registered ${panels.length} panels from: ${pluginId}`); + } + } + + // 注册菜单 + if (uiRegistry && editorModule.getMenuItems) { + const menuItems = editorModule.getMenuItems(); + if (menuItems && menuItems.length > 0) { + for (const item of menuItems) { + // 转换 MenuItemDescriptor 到 MenuItem(execute -> onClick) + const menuItem = { + ...item, + onClick: item.execute + }; + uiRegistry.registerMenu(menuItem as any); + resources.menuIds.push(item.id); + } + logger.debug(`Registered ${menuItems.length} menu items from: ${pluginId}`); + } + } + + // 注册工具栏 + if (uiRegistry && editorModule.getToolbarItems) { + const toolbarItems = editorModule.getToolbarItems(); + if (toolbarItems && toolbarItems.length > 0) { + for (const item of toolbarItems) { + uiRegistry.registerToolbarItem(item as any); + resources.toolbarIds.push(item.id); + } + logger.debug(`Registered ${toolbarItems.length} toolbar items from: ${pluginId}`); + } + } + + // 保存注册的资源 + plugin.registeredResources = resources; + + // 调用 onEditorReady + if (editorModule.onEditorReady) { + await editorModule.onEditorReady(); + } + + // 发布插件安装事件,通知 UI 刷新 + const messageHub = this.services.tryResolve(MessageHub); + if (messageHub) { + messageHub.publish('plugin:installed', { pluginId }); + } + } + + /** + * 动态卸载插件的编辑器模块 + * Dynamically deactivate plugin's editor module + */ + private async deactivatePluginEditor(pluginId: string): Promise { + const plugin = this.plugins.get(pluginId); + if (!plugin || !this.services) return; + + const editorModule = plugin.plugin.editorModule; + const resources = plugin.registeredResources; + + // 获取注册表服务 + const entityCreationRegistry = this.services.tryResolve(EntityCreationRegistry); + const componentActionRegistry = this.services.tryResolve(ComponentActionRegistry); + const fileActionRegistry = this.services.tryResolve(FileActionRegistry); + const uiRegistry = this.services.tryResolve(UIRegistry); + + if (resources) { + // 注销面板 + if (uiRegistry) { + for (const panelId of resources.panelIds) { + uiRegistry.unregisterPanel(panelId); + } + } + + // 注销菜单 + if (uiRegistry) { + for (const menuId of resources.menuIds) { + uiRegistry.unregisterMenu(menuId); + } + } + + // 注销工具栏 + if (uiRegistry) { + for (const toolbarId of resources.toolbarIds) { + uiRegistry.unregisterToolbarItem(toolbarId); + } + } + + // 注销实体模板 + if (entityCreationRegistry) { + for (const templateId of resources.entityTemplateIds) { + entityCreationRegistry.unregister(templateId); + } + } + + // 注销组件操作 + if (componentActionRegistry) { + for (const action of resources.componentActions) { + componentActionRegistry.unregister(action.componentName, action.actionId); + } + } + + // 注销文件处理器 + if (fileActionRegistry) { + for (const handler of resources.fileHandlers) { + fileActionRegistry.unregisterActionHandler(handler); + } + } + + // 注销文件模板 + if (fileActionRegistry) { + for (const template of resources.fileTemplates) { + fileActionRegistry.unregisterCreationTemplate(template); + } + } + + logger.debug(`Unregistered resources for: ${pluginId}`); + } + + // 调用 uninstall + if (editorModule?.uninstall) { + await editorModule.uninstall(); + logger.debug(`Editor module uninstalled: ${pluginId}`); + } + + // 发布插件卸载事件,通知 UI 刷新 + const messageHub = this.services.tryResolve(MessageHub); + if (messageHub) { + messageHub.publish('plugin:uninstalled', { pluginId }); + } + } + + /** + * 动态激活插件的运行时模块 + * Dynamically activate plugin's runtime module + */ + private async activatePluginRuntime(pluginId: string): Promise { + const plugin = this.plugins.get(pluginId); + if (!plugin || !this.currentScene || !this.currentContext || !this.services) return; + + const runtimeModule = plugin.plugin.runtimeModule; + if (!runtimeModule) return; + + // 注册组件 + if (runtimeModule.registerComponents) { + runtimeModule.registerComponents(ComponentRegistry); + logger.debug(`Components registered for: ${pluginId}`); + } + + // 注册服务 + if (runtimeModule.registerServices) { + runtimeModule.registerServices(this.services); + logger.debug(`Services registered for: ${pluginId}`); + } + + // 创建系统 + if (runtimeModule.createSystems) { + runtimeModule.createSystems(this.currentScene, this.currentContext); + logger.debug(`Systems created for: ${pluginId}`); + } + + // 调用系统创建后回调 + if (runtimeModule.onSystemsCreated) { + runtimeModule.onSystemsCreated(this.currentScene, this.currentContext); + logger.debug(`Systems wired for: ${pluginId}`); + } + + // 调用初始化 + if (runtimeModule.onInitialize) { + await runtimeModule.onInitialize(); + logger.debug(`Runtime initialized for: ${pluginId}`); + } + } + + /** + * 动态卸载插件的运行时模块 + * Dynamically deactivate plugin's runtime module + */ + private deactivatePluginRuntime(pluginId: string): void { + const plugin = this.plugins.get(pluginId); + if (!plugin) return; + + const runtimeModule = plugin.plugin.runtimeModule; + if (!runtimeModule) return; + + // 调用销毁回调 + if (runtimeModule.onDestroy) { + runtimeModule.onDestroy(); + logger.debug(`Runtime destroyed for: ${pluginId}`); + } + + // 注意:组件和服务无法动态注销,这是设计限制 + // 系统的移除需要场景支持,暂时只调用 onDestroy } /** @@ -235,7 +686,7 @@ export class PluginManager implements IService { */ getPluginsByCategory(category: PluginCategory): RegisteredPlugin[] { return this.getAllPlugins().filter( - p => p.loader.descriptor.category === category + p => p.plugin.descriptor.category === category ); } @@ -257,6 +708,9 @@ export class PluginManager implements IService { return; } + // 保存服务容器引用 + this.services = services; + logger.info('Initializing runtime modules...'); const sortedPlugins = this.sortByLoadingPhase('runtime'); @@ -266,8 +720,8 @@ export class PluginManager implements IService { const plugin = this.plugins.get(pluginId); if (!plugin?.enabled) continue; - const runtimeModule = plugin.loader.runtimeModule; - if (runtimeModule) { + const runtimeModule = plugin.plugin.runtimeModule; + if (runtimeModule?.registerComponents) { try { runtimeModule.registerComponents(ComponentRegistry); logger.debug(`Components registered for: ${pluginId}`); @@ -284,7 +738,7 @@ export class PluginManager implements IService { const plugin = this.plugins.get(pluginId); if (!plugin?.enabled || plugin.state === 'error') continue; - const runtimeModule = plugin.loader.runtimeModule; + const runtimeModule = plugin.plugin.runtimeModule; if (runtimeModule?.registerServices) { try { runtimeModule.registerServices(services); @@ -302,7 +756,7 @@ export class PluginManager implements IService { const plugin = this.plugins.get(pluginId); if (!plugin?.enabled || plugin.state === 'error') continue; - const runtimeModule = plugin.loader.runtimeModule; + const runtimeModule = plugin.plugin.runtimeModule; if (runtimeModule?.onInitialize) { try { await runtimeModule.onInitialize(); @@ -330,6 +784,10 @@ export class PluginManager implements IService { * Create systems for scene */ createSystemsForScene(scene: IScene, context: SystemContext): void { + // 保存场景和上下文引用(用于动态启用插件) + this.currentScene = scene; + this.currentContext = context; + logger.info('Creating systems for scene...'); console.log('[PluginManager] createSystemsForScene called, context.assetManager:', context.assetManager ? 'exists' : 'null'); @@ -340,10 +798,10 @@ export class PluginManager implements IService { // Phase 1: Create all systems for (const pluginId of sortedPlugins) { const plugin = this.plugins.get(pluginId); - console.log(`[PluginManager] Plugin ${pluginId}: enabled=${plugin?.enabled}, state=${plugin?.state}, hasRuntimeModule=${!!plugin?.loader.runtimeModule}`); + console.log(`[PluginManager] Plugin ${pluginId}: enabled=${plugin?.enabled}, state=${plugin?.state}, hasRuntimeModule=${!!plugin?.plugin.runtimeModule}`); if (!plugin?.enabled || plugin.state === 'error') continue; - const runtimeModule = plugin.loader.runtimeModule; + const runtimeModule = plugin.plugin.runtimeModule; if (runtimeModule?.createSystems) { try { console.log(`[PluginManager] Calling createSystems for: ${pluginId}`); @@ -361,7 +819,7 @@ export class PluginManager implements IService { const plugin = this.plugins.get(pluginId); if (!plugin?.enabled || plugin.state === 'error') continue; - const runtimeModule = plugin.loader.runtimeModule; + const runtimeModule = plugin.plugin.runtimeModule; if (runtimeModule?.onSystemsCreated) { try { runtimeModule.onSystemsCreated(scene, context); @@ -385,82 +843,26 @@ export class PluginManager implements IService { return; } + // 保存服务容器引用 + this.services = services; + logger.info('Initializing editor modules...'); const sortedPlugins = this.sortByLoadingPhase('editor'); - - // 获取注册表服务 | Get registry services - const entityCreationRegistry = services.tryResolve(EntityCreationRegistry); - const componentActionRegistry = services.tryResolve(ComponentActionRegistry); - const fileActionRegistry = services.tryResolve(FileActionRegistry); - const uiRegistry = services.tryResolve(UIRegistry); + logger.info(`Sorted plugins for editor initialization: ${sortedPlugins.join(', ')}`); for (const pluginId of sortedPlugins) { const plugin = this.plugins.get(pluginId); + logger.debug(`Processing plugin ${pluginId}: enabled=${plugin?.enabled}, hasEditorModule=${!!plugin?.plugin.editorModule}`); if (!plugin?.enabled) continue; - const editorModule = plugin.loader.editorModule; - if (editorModule) { - try { - // 安装编辑器模块 | Install editor module - await editorModule.install(services); - logger.debug(`Editor module installed: ${pluginId}`); - - // 注册实体创建模板 | Register entity creation templates - if (entityCreationRegistry && editorModule.getEntityCreationTemplates) { - const templates = editorModule.getEntityCreationTemplates(); - if (templates && templates.length > 0) { - entityCreationRegistry.registerMany(templates); - logger.debug(`Registered ${templates.length} entity templates from: ${pluginId}`); - } - } - - // 注册组件操作 | Register component actions - if (componentActionRegistry && editorModule.getComponentActions) { - const actions = editorModule.getComponentActions(); - if (actions && actions.length > 0) { - for (const action of actions) { - componentActionRegistry.register(action); - } - logger.debug(`Registered ${actions.length} component actions from: ${pluginId}`); - } - } - - // 注册文件操作处理器 | Register file action handlers - if (fileActionRegistry && editorModule.getFileActionHandlers) { - const handlers = editorModule.getFileActionHandlers(); - if (handlers && handlers.length > 0) { - for (const handler of handlers) { - fileActionRegistry.registerActionHandler(handler); - } - logger.debug(`Registered ${handlers.length} file action handlers from: ${pluginId}`); - } - } - - // 注册文件创建模板 | Register file creation templates - if (fileActionRegistry && editorModule.getFileCreationTemplates) { - const templates = editorModule.getFileCreationTemplates(); - if (templates && templates.length > 0) { - for (const template of templates) { - fileActionRegistry.registerCreationTemplate(template); - } - logger.debug(`Registered ${templates.length} file creation templates from: ${pluginId}`); - } - } - - // 注册面板 | Register panels - if (uiRegistry && editorModule.getPanels) { - const panels = editorModule.getPanels(); - if (panels && panels.length > 0) { - uiRegistry.registerPanels(panels); - logger.debug(`Registered ${panels.length} panels from: ${pluginId}`); - } - } - } catch (e) { - logger.error(`Failed to install editor module for ${pluginId}:`, e); - plugin.state = 'error'; - plugin.error = e as Error; - } + try { + // 使用统一的激活方法,自动跟踪注册的资源 + await this.activatePluginEditor(pluginId); + } catch (e) { + logger.error(`Failed to install editor module for ${pluginId}:`, e); + plugin.state = 'error'; + plugin.error = e as Error; } } @@ -479,77 +881,14 @@ export class PluginManager implements IService { return; } - const editorModule = plugin.loader.editorModule; - if (!editorModule) { - return; + // 确保服务容器已设置 + if (!this.services) { + this.services = services; } - // 获取注册表服务 | Get registry services - const entityCreationRegistry = services.tryResolve(EntityCreationRegistry); - const componentActionRegistry = services.tryResolve(ComponentActionRegistry); - const fileActionRegistry = services.tryResolve(FileActionRegistry); - const uiRegistry = services.tryResolve(UIRegistry); - try { - // 安装编辑器模块 | Install editor module - await editorModule.install(services); - logger.debug(`Editor module installed: ${pluginId}`); - - // 注册实体创建模板 | Register entity creation templates - if (entityCreationRegistry && editorModule.getEntityCreationTemplates) { - const templates = editorModule.getEntityCreationTemplates(); - if (templates && templates.length > 0) { - entityCreationRegistry.registerMany(templates); - logger.debug(`Registered ${templates.length} entity templates from: ${pluginId}`); - } - } - - // 注册组件操作 | Register component actions - if (componentActionRegistry && editorModule.getComponentActions) { - const actions = editorModule.getComponentActions(); - if (actions && actions.length > 0) { - for (const action of actions) { - componentActionRegistry.register(action); - } - logger.debug(`Registered ${actions.length} component actions from: ${pluginId}`); - } - } - - // 注册文件操作处理器 | Register file action handlers - if (fileActionRegistry && editorModule.getFileActionHandlers) { - const handlers = editorModule.getFileActionHandlers(); - if (handlers && handlers.length > 0) { - for (const handler of handlers) { - fileActionRegistry.registerActionHandler(handler); - } - logger.debug(`Registered ${handlers.length} file action handlers from: ${pluginId}`); - } - } - - // 注册文件创建模板 | Register file creation templates - if (fileActionRegistry && editorModule.getFileCreationTemplates) { - const templates = editorModule.getFileCreationTemplates(); - if (templates && templates.length > 0) { - for (const template of templates) { - fileActionRegistry.registerCreationTemplate(template); - } - logger.debug(`Registered ${templates.length} file creation templates from: ${pluginId}`); - } - } - - // 注册面板 | Register panels - if (uiRegistry && editorModule.getPanels) { - const panels = editorModule.getPanels(); - if (panels && panels.length > 0) { - uiRegistry.registerPanels(panels); - logger.debug(`Registered ${panels.length} panels from: ${pluginId}`); - } - } - - // 调用 onEditorReady(如果编辑器已就绪) - if (this.editorInitialized && editorModule.onEditorReady) { - await editorModule.onEditorReady(); - } + // 使用统一的激活方法 + await this.activatePluginEditor(pluginId); } catch (e) { logger.error(`Failed to install editor module for ${pluginId}:`, e); plugin.state = 'error'; @@ -564,7 +903,7 @@ export class PluginManager implements IService { async notifyEditorReady(): Promise { for (const [pluginId, plugin] of this.plugins) { if (!plugin.enabled) continue; - const editorModule = plugin.loader.editorModule; + const editorModule = plugin.plugin.editorModule; if (editorModule?.onEditorReady) { try { await editorModule.onEditorReady(); @@ -582,7 +921,7 @@ export class PluginManager implements IService { async notifyProjectOpen(projectPath: string): Promise { for (const [pluginId, plugin] of this.plugins) { if (!plugin.enabled) continue; - const editorModule = plugin.loader.editorModule; + const editorModule = plugin.plugin.editorModule; if (editorModule?.onProjectOpen) { try { await editorModule.onProjectOpen(projectPath); @@ -600,7 +939,7 @@ export class PluginManager implements IService { async notifyProjectClose(): Promise { for (const [pluginId, plugin] of this.plugins) { if (!plugin.enabled) continue; - const editorModule = plugin.loader.editorModule; + const editorModule = plugin.plugin.editorModule; if (editorModule?.onProjectClose) { try { await editorModule.onProjectClose(); @@ -618,7 +957,7 @@ export class PluginManager implements IService { notifySceneLoaded(scenePath: string): void { for (const [pluginId, plugin] of this.plugins) { if (!plugin.enabled) continue; - const editorModule = plugin.loader.editorModule; + const editorModule = plugin.plugin.editorModule; if (editorModule?.onSceneLoaded) { try { editorModule.onSceneLoaded(scenePath); @@ -636,7 +975,7 @@ export class PluginManager implements IService { notifySceneSaving(scenePath: string): boolean { for (const [pluginId, plugin] of this.plugins) { if (!plugin.enabled) continue; - const editorModule = plugin.loader.editorModule; + const editorModule = plugin.plugin.editorModule; if (editorModule?.onSceneSaving) { try { const result = editorModule.onSceneSaving(scenePath); @@ -658,7 +997,7 @@ export class PluginManager implements IService { setLocale(locale: string): void { for (const [pluginId, plugin] of this.plugins) { if (!plugin.enabled) continue; - const editorModule = plugin.loader.editorModule; + const editorModule = plugin.plugin.editorModule; if (editorModule?.setLocale) { try { editorModule.setLocale(locale); @@ -676,7 +1015,7 @@ export class PluginManager implements IService { exportConfig(): PluginConfig { const enabledPlugins: string[] = []; for (const [id, plugin] of this.plugins) { - if (plugin.enabled && !plugin.loader.descriptor.isCore) { + if (plugin.enabled && !plugin.plugin.descriptor.isCore) { enabledPlugins.push(id); } } @@ -684,22 +1023,48 @@ export class PluginManager implements IService { } /** - * 加载配置 - * Load configuration + * 加载配置并激活插件 + * Load configuration and activate plugins + * + * 此方法会: + * 1. 根据配置启用/禁用插件 + * 2. 激活新启用插件的编辑器模块 + * 3. 卸载新禁用插件的编辑器模块 */ - loadConfig(config: PluginConfig): void { + async loadConfig(config: PluginConfig): Promise { const { enabledPlugins } = config; + logger.info(`loadConfig called with: ${enabledPlugins.join(', ')}`); + + // 收集状态变化的插件 + const toEnable: string[] = []; + const toDisable: string[] = []; for (const [id, plugin] of this.plugins) { - if (plugin.loader.descriptor.isCore) { - plugin.enabled = true; - } else { - plugin.enabled = enabledPlugins.includes(id); - plugin.state = plugin.enabled ? 'loaded' : 'disabled'; + if (plugin.plugin.descriptor.isCore) { + continue; // 核心插件始终启用 + } + + const shouldBeEnabled = enabledPlugins.includes(id); + const wasEnabled = plugin.enabled; + + if (shouldBeEnabled && !wasEnabled) { + toEnable.push(id); + } else if (!shouldBeEnabled && wasEnabled) { + toDisable.push(id); } } - logger.info(`Loaded config: ${enabledPlugins.length} plugins enabled`); + // 禁用不再需要的插件 + for (const pluginId of toDisable) { + await this.disable(pluginId); + } + + // 启用新插件 + for (const pluginId of toEnable) { + await this.enable(pluginId); + } + + logger.info(`Config loaded and applied: ${toEnable.length} enabled, ${toDisable.length} disabled`); } /** @@ -718,12 +1083,12 @@ export class PluginManager implements IService { const pluginB = this.plugins.get(b); const moduleA = moduleType === 'runtime' - ? pluginA?.loader.descriptor.modules.find(m => m.type === 'runtime') - : pluginA?.loader.descriptor.modules.find(m => m.type === 'editor'); + ? pluginA?.plugin.descriptor.modules.find(m => m.type === 'runtime') + : pluginA?.plugin.descriptor.modules.find(m => m.type === 'editor'); const moduleB = moduleType === 'runtime' - ? pluginB?.loader.descriptor.modules.find(m => m.type === 'runtime') - : pluginB?.loader.descriptor.modules.find(m => m.type === 'editor'); + ? pluginB?.plugin.descriptor.modules.find(m => m.type === 'runtime') + : pluginB?.plugin.descriptor.modules.find(m => m.type === 'editor'); const phaseA = moduleA?.loadingPhase || 'default'; const phaseB = moduleB?.loadingPhase || 'default'; @@ -748,7 +1113,7 @@ export class PluginManager implements IService { const plugin = this.plugins.get(id); if (plugin) { - const deps = plugin.loader.descriptor.dependencies || []; + const deps = plugin.plugin.descriptor.dependencies || []; for (const dep of deps) { if (pluginIds.includes(dep.id)) { visit(dep.id); @@ -773,7 +1138,7 @@ export class PluginManager implements IService { clearSceneSystems(): void { for (const [pluginId, plugin] of this.plugins) { if (!plugin.enabled) continue; - const runtimeModule = plugin.loader.runtimeModule; + const runtimeModule = plugin.plugin.runtimeModule; if (runtimeModule?.onDestroy) { try { runtimeModule.onDestroy(); diff --git a/packages/editor-core/src/Services/AssetRegistryService.ts b/packages/editor-core/src/Services/AssetRegistryService.ts new file mode 100644 index 00000000..a9509ad8 --- /dev/null +++ b/packages/editor-core/src/Services/AssetRegistryService.ts @@ -0,0 +1,650 @@ +/** + * Asset Registry Service + * 资产注册表服务 + * + * 负责扫描项目资产目录,为每个资产生成唯一GUID, + * 并维护 GUID ↔ 路径 的映射关系。 + * + * Responsible for scanning project asset directories, + * generating unique GUIDs for each asset, and maintaining + * GUID ↔ path mappings. + */ + +import { Core } from '@esengine/ecs-framework'; +import { MessageHub } from './MessageHub'; + +// Simple logger for AssetRegistry +const logger = { + info: (msg: string, ...args: unknown[]) => console.log(`[AssetRegistry] ${msg}`, ...args), + warn: (msg: string, ...args: unknown[]) => console.warn(`[AssetRegistry] ${msg}`, ...args), + error: (msg: string, ...args: unknown[]) => console.error(`[AssetRegistry] ${msg}`, ...args), + debug: (msg: string, ...args: unknown[]) => console.debug(`[AssetRegistry] ${msg}`, ...args), +}; + +/** + * Asset GUID type (simplified, no dependency on asset-system) + */ +export type AssetGUID = string; + +/** + * Asset type for registry (using different name to avoid conflict) + */ +export type AssetRegistryType = string; + +/** + * Asset metadata (simplified) + */ +export interface IAssetRegistryMetadata { + guid: AssetGUID; + path: string; + type: AssetRegistryType; + name: string; + size: number; + hash: string; + lastModified: number; +} + +/** + * Asset catalog entry for export + */ +export interface IAssetRegistryCatalogEntry { + guid: AssetGUID; + path: string; + type: AssetRegistryType; + size: number; + hash: string; +} + +/** + * Asset file info from filesystem scan + */ +export interface AssetFileInfo { + /** Absolute path to the file */ + absolutePath: string; + /** Path relative to project root */ + relativePath: string; + /** File name without extension */ + name: string; + /** File extension (e.g., '.png', '.btree') */ + extension: string; + /** File size in bytes */ + size: number; + /** Last modified timestamp */ + lastModified: number; +} + +/** + * Asset registry manifest stored in project + * 存储在项目中的资产注册表清单 + */ +export interface AssetManifest { + version: string; + createdAt: number; + updatedAt: number; + assets: Record; +} + +/** + * Single asset entry in manifest + */ +export interface AssetManifestEntry { + guid: AssetGUID; + relativePath: string; + type: AssetRegistryType; + hash?: string; +} + +/** + * Extension to asset type mapping + */ +const EXTENSION_TYPE_MAP: Record = { + // Textures + '.png': 'texture', + '.jpg': 'texture', + '.jpeg': 'texture', + '.webp': 'texture', + '.gif': 'texture', + // Audio + '.mp3': 'audio', + '.ogg': 'audio', + '.wav': 'audio', + // Data + '.json': 'json', + '.txt': 'text', + // Custom types + '.btree': 'btree', + '.ecs': 'scene', + '.prefab': 'prefab', + '.tmx': 'tilemap', + '.tsx': 'tileset', +}; + +/** + * File system interface for asset scanning + */ +interface IFileSystem { + readDir(path: string): Promise; + readFile(path: string): Promise; + writeFile(path: string, content: string): Promise; + exists(path: string): Promise; + stat(path: string): Promise<{ size: number; mtime: number; isDirectory: boolean }>; + isDirectory(path: string): Promise; +} + +/** + * Simple in-memory asset database + */ +class SimpleAssetDatabase { + private readonly _metadata = new Map(); + private readonly _pathToGuid = new Map(); + private readonly _typeToGuids = new Map>(); + + addAsset(metadata: IAssetRegistryMetadata): void { + const { guid, path, type } = metadata; + this._metadata.set(guid, metadata); + this._pathToGuid.set(path, guid); + + if (!this._typeToGuids.has(type)) { + this._typeToGuids.set(type, new Set()); + } + this._typeToGuids.get(type)!.add(guid); + } + + removeAsset(guid: AssetGUID): void { + const metadata = this._metadata.get(guid); + if (!metadata) return; + + this._metadata.delete(guid); + this._pathToGuid.delete(metadata.path); + + const typeSet = this._typeToGuids.get(metadata.type); + if (typeSet) { + typeSet.delete(guid); + } + } + + getMetadata(guid: AssetGUID): IAssetRegistryMetadata | undefined { + return this._metadata.get(guid); + } + + getMetadataByPath(path: string): IAssetRegistryMetadata | undefined { + const guid = this._pathToGuid.get(path); + return guid ? this._metadata.get(guid) : undefined; + } + + findAssetsByType(type: AssetRegistryType): AssetGUID[] { + const guids = this._typeToGuids.get(type); + return guids ? Array.from(guids) : []; + } + + exportToCatalog(): IAssetRegistryCatalogEntry[] { + const entries: IAssetRegistryCatalogEntry[] = []; + this._metadata.forEach((metadata) => { + entries.push({ + guid: metadata.guid, + path: metadata.path, + type: metadata.type, + size: metadata.size, + hash: metadata.hash + }); + }); + return entries; + } + + getStatistics(): { totalAssets: number } { + return { totalAssets: this._metadata.size }; + } + + clear(): void { + this._metadata.clear(); + this._pathToGuid.clear(); + this._typeToGuids.clear(); + } +} + +/** + * Asset Registry Service + */ +export class AssetRegistryService { + private _database: SimpleAssetDatabase; + private _projectPath: string | null = null; + private _manifest: AssetManifest | null = null; + private _fileSystem: IFileSystem | null = null; + private _messageHub: MessageHub | null = null; + private _initialized = false; + + /** Manifest file name */ + static readonly MANIFEST_FILE = 'asset-manifest.json'; + /** Current manifest version */ + static readonly MANIFEST_VERSION = '1.0.0'; + + constructor() { + this._database = new SimpleAssetDatabase(); + } + + /** + * Initialize the service + */ + async initialize(): Promise { + if (this._initialized) return; + + // 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; + + // Subscribe to project events + if (this._messageHub) { + this._messageHub.subscribe('project:opened', this._onProjectOpened.bind(this)); + this._messageHub.subscribe('project:closed', this._onProjectClosed.bind(this)); + } + + this._initialized = true; + logger.info('AssetRegistryService initialized'); + } + + /** + * Handle project opened event + */ + private async _onProjectOpened(data: { path: string }): Promise { + await this.loadProject(data.path); + } + + /** + * Handle project closed event + */ + private _onProjectClosed(): void { + this.unloadProject(); + } + + /** + * Load project and scan assets + */ + async loadProject(projectPath: string): Promise { + if (!this._fileSystem) { + logger.warn('FileSystem service not available, skipping asset registry'); + return; + } + + this._projectPath = projectPath; + this._database.clear(); + + // Try to load existing manifest + await this._loadManifest(); + + // Scan assets directory + await this._scanAssetsDirectory(); + + // Save updated manifest + await this._saveManifest(); + + logger.info(`Project assets loaded: ${this._database.getStatistics().totalAssets} assets`); + + // Publish event + this._messageHub?.publish('assets:registry:loaded', { + projectPath, + assetCount: this._database.getStatistics().totalAssets + }); + } + + /** + * Unload current project + */ + unloadProject(): void { + this._projectPath = null; + this._manifest = null; + this._database.clear(); + logger.info('Project assets unloaded'); + } + + /** + * Load manifest from project + */ + private async _loadManifest(): Promise { + if (!this._fileSystem || !this._projectPath) return; + + const manifestPath = this._getManifestPath(); + + try { + const exists = await this._fileSystem.exists(manifestPath); + if (exists) { + const content = await this._fileSystem.readFile(manifestPath); + this._manifest = JSON.parse(content); + logger.debug('Loaded existing asset manifest'); + } else { + this._manifest = this._createEmptyManifest(); + logger.debug('Created new asset manifest'); + } + } catch (error) { + logger.warn('Failed to load manifest, creating new one:', error); + this._manifest = this._createEmptyManifest(); + } + } + + /** + * Save manifest to project + */ + private async _saveManifest(): Promise { + if (!this._fileSystem || !this._projectPath || !this._manifest) return; + + const manifestPath = this._getManifestPath(); + this._manifest.updatedAt = Date.now(); + + try { + const content = JSON.stringify(this._manifest, null, 2); + await this._fileSystem.writeFile(manifestPath, content); + logger.debug('Saved asset manifest'); + } catch (error) { + logger.error('Failed to save manifest:', error); + } + } + + /** + * Get manifest file path + */ + private _getManifestPath(): string { + const sep = this._projectPath!.includes('\\') ? '\\' : '/'; + return `${this._projectPath}${sep}${AssetRegistryService.MANIFEST_FILE}`; + } + + /** + * Create empty manifest + */ + private _createEmptyManifest(): AssetManifest { + return { + version: AssetRegistryService.MANIFEST_VERSION, + createdAt: Date.now(), + updatedAt: Date.now(), + assets: {} + }; + } + + /** + * Scan assets directory and register all assets + */ + private async _scanAssetsDirectory(): Promise { + if (!this._fileSystem || !this._projectPath) return; + + const sep = this._projectPath.includes('\\') ? '\\' : '/'; + const assetsPath = `${this._projectPath}${sep}assets`; + + 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 { + if (!this._fileSystem) return; + + try { + const entries = await this._fileSystem.readDir(absolutePath); + const sep = absolutePath.includes('\\') ? '\\' : '/'; + + for (const entry of entries) { + const entryAbsPath = `${absolutePath}${sep}${entry}`; + const entryRelPath = `${relativePath}/${entry}`; + + try { + const isDir = await this._fileSystem.isDirectory(entryAbsPath); + + if (isDir) { + // Recursively scan subdirectory + await this._scanDirectory(entryAbsPath, entryRelPath); + } else { + // Register file as asset + await this._registerAssetFile(entryAbsPath, entryRelPath); + } + } catch (error) { + logger.warn(`Failed to process entry ${entry}:`, error); + } + } + } catch (error) { + logger.warn(`Failed to read directory ${absolutePath}:`, error); + } + } + + /** + * Register a single asset file + */ + private async _registerAssetFile(absolutePath: string, relativePath: string): Promise { + if (!this._fileSystem || !this._manifest) return; + + // Get file extension + const lastDot = relativePath.lastIndexOf('.'); + if (lastDot === -1) return; // Skip files without extension + + const extension = relativePath.substring(lastDot).toLowerCase(); + const assetType = EXTENSION_TYPE_MAP[extension]; + + // Skip unknown file types + if (!assetType) return; + + // Get file info + let stat: { size: number; mtime: number }; + try { + stat = await this._fileSystem.stat(absolutePath); + } catch { + return; + } + + // Check if already in manifest + let guid: AssetGUID; + const existingEntry = this._manifest.assets[relativePath]; + + if (existingEntry) { + guid = existingEntry.guid; + } else { + // Generate new GUID + guid = this._generateGUID(); + this._manifest.assets[relativePath] = { + guid, + relativePath, + type: assetType + }; + } + + // Get file name + const lastSlash = relativePath.lastIndexOf('/'); + const fileName = lastSlash >= 0 ? relativePath.substring(lastSlash + 1) : relativePath; + const name = fileName.substring(0, fileName.lastIndexOf('.')); + + // Create metadata + const metadata: IAssetRegistryMetadata = { + guid, + path: relativePath, + type: assetType, + name, + size: stat.size, + hash: '', // Could compute hash if needed + lastModified: stat.mtime + }; + + // Register in database + this._database.addAsset(metadata); + } + + /** + * Generate a unique GUID + */ + private _generateGUID(): AssetGUID { + // Simple UUID v4 generation + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { + const r = (Math.random() * 16) | 0; + const v = c === 'x' ? r : (r & 0x3) | 0x8; + return v.toString(16); + }); + } + + // ==================== Public API ==================== + + /** + * Get asset metadata by GUID + */ + getAsset(guid: AssetGUID): IAssetRegistryMetadata | undefined { + return this._database.getMetadata(guid); + } + + /** + * Get asset metadata by relative path + */ + getAssetByPath(relativePath: string): IAssetRegistryMetadata | undefined { + return this._database.getMetadataByPath(relativePath); + } + + /** + * Get GUID for a relative path + */ + getGuidByPath(relativePath: string): AssetGUID | undefined { + const metadata = this._database.getMetadataByPath(relativePath); + return metadata?.guid; + } + + /** + * Get relative path for a GUID + */ + getPathByGuid(guid: AssetGUID): string | undefined { + const metadata = this._database.getMetadata(guid); + return metadata?.path; + } + + /** + * Convert absolute path to relative path + */ + absoluteToRelative(absolutePath: string): string | null { + if (!this._projectPath) return null; + + const normalizedAbs = absolutePath.replace(/\\/g, '/'); + const normalizedProject = this._projectPath.replace(/\\/g, '/'); + + if (normalizedAbs.startsWith(normalizedProject)) { + return normalizedAbs.substring(normalizedProject.length + 1); + } + + return null; + } + + /** + * Convert relative path to absolute path + */ + relativeToAbsolute(relativePath: string): string | null { + if (!this._projectPath) return null; + + const sep = this._projectPath.includes('\\') ? '\\' : '/'; + return `${this._projectPath}${sep}${relativePath.replace(/\//g, sep)}`; + } + + /** + * Find assets by type + */ + findAssetsByType(type: AssetRegistryType): IAssetRegistryMetadata[] { + const guids = this._database.findAssetsByType(type); + return guids + .map(guid => this._database.getMetadata(guid)) + .filter((m): m is IAssetRegistryMetadata => m !== undefined); + } + + /** + * Get all registered assets + */ + getAllAssets(): IAssetRegistryMetadata[] { + const entries = this._database.exportToCatalog(); + return entries.map(entry => this._database.getMetadata(entry.guid)) + .filter((m): m is IAssetRegistryMetadata => m !== undefined); + } + + /** + * Export catalog for runtime use + * 导出运行时使用的资产目录 + */ + exportCatalog(): IAssetRegistryCatalogEntry[] { + return this._database.exportToCatalog(); + } + + /** + * Export catalog as JSON string + */ + exportCatalogJSON(): string { + const entries = this._database.exportToCatalog(); + const catalog = { + version: '1.0.0', + createdAt: Date.now(), + entries: Object.fromEntries(entries.map(e => [e.guid, e])) + }; + return JSON.stringify(catalog, null, 2); + } + + /** + * Register a new asset (e.g., when a file is created) + */ + async registerAsset(absolutePath: string): Promise { + const relativePath = this.absoluteToRelative(absolutePath); + if (!relativePath) return null; + + await this._registerAssetFile(absolutePath, relativePath); + await this._saveManifest(); + + const metadata = this._database.getMetadataByPath(relativePath); + return metadata?.guid ?? null; + } + + /** + * Unregister an asset (e.g., when a file is deleted) + */ + async unregisterAsset(absolutePath: string): Promise { + const relativePath = this.absoluteToRelative(absolutePath); + if (!relativePath || !this._manifest) return; + + const metadata = this._database.getMetadataByPath(relativePath); + if (metadata) { + this._database.removeAsset(metadata.guid); + delete this._manifest.assets[relativePath]; + await this._saveManifest(); + } + } + + /** + * Refresh a single asset (e.g., when file is modified) + */ + async refreshAsset(absolutePath: string): Promise { + const relativePath = this.absoluteToRelative(absolutePath); + if (!relativePath) return; + + // Re-register the asset + await this._registerAssetFile(absolutePath, relativePath); + await this._saveManifest(); + } + + /** + * Get database statistics + */ + getStatistics() { + return this._database.getStatistics(); + } + + /** + * Check if service is ready + */ + get isReady(): boolean { + return this._initialized && this._projectPath !== null; + } + + /** + * Get current project path + */ + get projectPath(): string | null { + return this._projectPath; + } +} diff --git a/packages/editor-core/src/Services/EntityStoreService.ts b/packages/editor-core/src/Services/EntityStoreService.ts index 1ac7898a..2e0dc2ea 100644 --- a/packages/editor-core/src/Services/EntityStoreService.ts +++ b/packages/editor-core/src/Services/EntityStoreService.ts @@ -1,4 +1,4 @@ -import { Injectable, IService, Entity, Core } from '@esengine/ecs-framework'; +import { Injectable, IService, Entity, Core, HierarchyComponent } from '@esengine/ecs-framework'; import { MessageHub } from './MessageHub'; export interface EntityTreeNode { @@ -68,6 +68,10 @@ export class EntityStoreService implements IService { .filter((e): e is Entity => e !== undefined); } + public getRootEntityIds(): number[] { + return [...this.rootEntityIds]; + } + public getEntity(id: number): Entity | undefined { return this.entities.get(id); } @@ -88,7 +92,9 @@ export class EntityStoreService implements IService { scene.entities.forEach((entity) => { this.entities.set(entity.id, entity); - if (!entity.parent) { + const hierarchy = entity.getComponent(HierarchyComponent); + const bHasNoParent = hierarchy?.parentId === null || hierarchy?.parentId === undefined; + if (bHasNoParent) { this.rootEntityIds.push(entity.id); } }); diff --git a/packages/editor-core/src/Services/FileActionRegistry.ts b/packages/editor-core/src/Services/FileActionRegistry.ts index 73ff697f..9eede3ec 100644 --- a/packages/editor-core/src/Services/FileActionRegistry.ts +++ b/packages/editor-core/src/Services/FileActionRegistry.ts @@ -19,6 +19,12 @@ export interface AssetCreationMapping { canCreate?: boolean; } +/** + * FileActionRegistry 服务标识符 + * FileActionRegistry service identifier + */ +export const IFileActionRegistry = Symbol.for('IFileActionRegistry'); + /** * 文件操作注册表服务 * diff --git a/packages/editor-core/src/Services/LogService.ts b/packages/editor-core/src/Services/LogService.ts index 56c54368..ddd64041 100644 --- a/packages/editor-core/src/Services/LogService.ts +++ b/packages/editor-core/src/Services/LogService.ts @@ -8,6 +8,7 @@ export interface LogEntry { source: string; message: string; args: unknown[]; + stack?: string; // 调用堆栈 clientId?: string; // 远程客户端ID } @@ -59,12 +60,12 @@ export class LogService implements IService { }; console.warn = (...args: unknown[]) => { - this.addLog(LogLevel.Warn, 'console', this.formatMessage(args), args); + this.addLog(LogLevel.Warn, 'console', this.formatMessage(args), args, true); this.originalConsole.warn(...args); }; console.error = (...args: unknown[]) => { - this.addLog(LogLevel.Error, 'console', this.formatMessage(args), args); + this.addLog(LogLevel.Error, 'console', this.formatMessage(args), args, true); this.originalConsole.error(...args); }; @@ -93,7 +94,10 @@ export class LogService implements IService { private formatMessage(args: unknown[]): string { return args.map((arg) => { if (typeof arg === 'string') return arg; - if (arg instanceof Error) return arg.message; + if (arg instanceof Error) { + // 包含错误消息和堆栈 + return arg.stack || arg.message; + } try { return JSON.stringify(arg); } catch { @@ -102,17 +106,30 @@ export class LogService implements IService { }).join(' '); } + /** + * 捕获当前调用堆栈 + */ + private captureStack(): string { + const stack = new Error().stack; + if (!stack) return ''; + + // 移除前几行(Error、captureStack、addLog、console.xxx) + const lines = stack.split('\n'); + return lines.slice(4).join('\n'); + } + /** * 添加日志 */ - private addLog(level: LogLevel, source: string, message: string, args: unknown[]): void { + private addLog(level: LogLevel, source: string, message: string, args: unknown[], includeStack = false): void { const entry: LogEntry = { id: this.nextId++, timestamp: new Date(), level, source, message, - args + args, + stack: includeStack ? this.captureStack() : undefined }; this.logs.push(entry); diff --git a/packages/editor-core/src/Services/ProjectService.ts b/packages/editor-core/src/Services/ProjectService.ts index 57212724..e2b124ff 100644 --- a/packages/editor-core/src/Services/ProjectService.ts +++ b/packages/editor-core/src/Services/ProjectService.ts @@ -26,6 +26,15 @@ export interface UIDesignResolution { height: number; } +/** + * 插件配置 + * Plugin Configuration + */ +export interface PluginSettings { + /** 启用的插件 ID 列表 / Enabled plugin IDs */ + enabledPlugins: string[]; +} + export interface ProjectConfig { projectType?: ProjectType; componentsPath?: string; @@ -35,6 +44,8 @@ export interface ProjectConfig { defaultScene?: string; /** UI 设计分辨率 / UI design resolution */ uiDesignResolution?: UIDesignResolution; + /** 插件配置 / Plugin settings */ + plugins?: PluginSettings; } @Injectable() @@ -200,16 +211,21 @@ export class ProjectService implements IService { private async loadConfig(configPath: string): Promise { try { const content = await this.fileAPI.readFileContent(configPath); + logger.debug('Raw config content:', content); const config = JSON.parse(content) as ProjectConfig; - return { + logger.debug('Parsed config plugins:', config.plugins); + const result = { projectType: config.projectType || 'esengine', componentsPath: config.componentsPath || '', componentPattern: config.componentPattern || '**/*.ts', buildOutput: config.buildOutput || 'temp/editor-components', scenesPath: config.scenesPath || 'scenes', defaultScene: config.defaultScene || 'main.ecs', - uiDesignResolution: config.uiDesignResolution + uiDesignResolution: config.uiDesignResolution, + plugins: config.plugins }; + logger.debug('Loaded config result:', result); + return result; } catch (error) { logger.warn('Failed to load config, using defaults', error); return { @@ -280,6 +296,60 @@ export class ProjectService implements IService { await this.updateConfig({ uiDesignResolution: resolution }); } + /** + * 获取启用的插件列表 + * Get enabled plugins list + */ + public getEnabledPlugins(): string[] { + return this.projectConfig?.plugins?.enabledPlugins || []; + } + + /** + * 获取插件配置 + * Get plugin settings + */ + public getPluginSettings(): PluginSettings | null { + logger.debug('getPluginSettings called, projectConfig:', this.projectConfig); + logger.debug('getPluginSettings plugins:', this.projectConfig?.plugins); + return this.projectConfig?.plugins || null; + } + + /** + * 设置启用的插件列表 + * Set enabled plugins list + * + * @param enabledPlugins - Array of enabled plugin IDs + */ + public async setEnabledPlugins(enabledPlugins: string[]): Promise { + await this.updateConfig({ + plugins: { + enabledPlugins + } + }); + await this.messageHub.publish('project:pluginsChanged', { enabledPlugins }); + logger.info('Plugin settings saved', { count: enabledPlugins.length }); + } + + /** + * 启用插件 + * Enable a plugin + */ + public async enablePlugin(pluginId: string): Promise { + const current = this.getEnabledPlugins(); + if (!current.includes(pluginId)) { + await this.setEnabledPlugins([...current, pluginId]); + } + } + + /** + * 禁用插件 + * Disable a plugin + */ + public async disablePlugin(pluginId: string): Promise { + const current = this.getEnabledPlugins(); + await this.setEnabledPlugins(current.filter(id => id !== pluginId)); + } + public dispose(): void { this.currentProject = null; this.projectConfig = null; diff --git a/packages/editor-core/src/Services/PropertyMetadata.ts b/packages/editor-core/src/Services/PropertyMetadata.ts index 17e3ac07..c9ff87af 100644 --- a/packages/editor-core/src/Services/PropertyMetadata.ts +++ b/packages/editor-core/src/Services/PropertyMetadata.ts @@ -4,6 +4,14 @@ import { createLogger } from '@esengine/ecs-framework'; const logger = createLogger('PropertyMetadata'); +/** + * 不需要在 Inspector 中显示的内部组件类型 + * 这些组件不使用 @Property 装饰器,因为它们的属性不应该被手动编辑 + */ +const INTERNAL_COMPONENTS = new Set([ + 'HierarchyComponent' +]); + export type { PropertyOptions, PropertyAction, PropertyControl, AssetType, EnumOption }; export type PropertyMetadata = PropertyOptions; export type PropertyType = 'number' | 'integer' | 'string' | 'boolean' | 'color' | 'vector2' | 'vector3' | 'enum' | 'asset' | 'animationClips'; @@ -53,7 +61,10 @@ export class PropertyMetadataService implements IService { } // 没有元数据时返回空对象 - logger.warn(`No property metadata found for component: ${component.constructor.name}`); + // 内部组件(如 HierarchyComponent)不需要警告 + if (!INTERNAL_COMPONENTS.has(component.constructor.name)) { + logger.warn(`No property metadata found for component: ${component.constructor.name}`); + } return {}; } diff --git a/packages/editor-core/src/Services/SettingsRegistry.ts b/packages/editor-core/src/Services/SettingsRegistry.ts index ccdd063b..9a5b1fef 100644 --- a/packages/editor-core/src/Services/SettingsRegistry.ts +++ b/packages/editor-core/src/Services/SettingsRegistry.ts @@ -55,6 +55,7 @@ export class SettingsRegistry implements IService { if (this.categories.has(category.id)) { console.warn(`[SettingsRegistry] Category ${category.id} already registered, overwriting`); } + console.log(`[SettingsRegistry] Registering category: ${category.id} (${category.title}), sections: ${category.sections.map(s => s.id).join(', ')}`); this.categories.set(category.id, category); } diff --git a/packages/editor-core/src/index.ts b/packages/editor-core/src/index.ts index f431c4d8..4765d2e0 100644 --- a/packages/editor-core/src/index.ts +++ b/packages/editor-core/src/index.ts @@ -39,6 +39,7 @@ export * from './Services/IFieldEditor'; export * from './Services/FieldEditorRegistry'; export * from './Services/ComponentInspectorRegistry'; export * from './Services/ComponentActionRegistry'; +export * from './Services/AssetRegistryService'; export * from './Gizmos'; diff --git a/packages/editor-runtime/package.json b/packages/editor-runtime/package.json index b9f70fd0..99f671ba 100644 --- a/packages/editor-runtime/package.json +++ b/packages/editor-runtime/package.json @@ -26,18 +26,16 @@ "tsyringe": "^4.10.0", "reflect-metadata": "^0.2.2" }, - "peerDependencies": { + "devDependencies": { "@esengine/ecs-framework": "workspace:*", "@esengine/editor-core": "workspace:*", "@esengine/ui": "workspace:*", + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", "react": "^18.3.1", "react-dom": "^18.3.1", "zustand": "^5.0.8", - "lucide-react": "^0.545.0" - }, - "devDependencies": { - "@types/react": "^18.3.12", - "@types/react-dom": "^18.3.1", + "lucide-react": "^0.545.0", "rimraf": "^5.0.0", "typescript": "^5.8.3", "vite": "^6.0.7", diff --git a/packages/editor-runtime/src/index.ts b/packages/editor-runtime/src/index.ts index 1473af6e..5b0a49ee 100644 --- a/packages/editor-runtime/src/index.ts +++ b/packages/editor-runtime/src/index.ts @@ -135,6 +135,7 @@ export { IMessageHub, ICompilerRegistry, IInspectorRegistry, + IFileActionRegistry, } from '@esengine/editor-core'; // Type-only exports from editor-core diff --git a/packages/engine-core/package.json b/packages/engine-core/package.json new file mode 100644 index 00000000..ea1a87e3 --- /dev/null +++ b/packages/engine-core/package.json @@ -0,0 +1,44 @@ +{ + "name": "@esengine/engine-core", + "version": "1.0.0", + "description": "Engine core components - Transform, etc.", + "esengine": { + "plugin": true, + "pluginExport": "EnginePlugin", + "category": "core", + "isEnginePlugin": true + }, + "main": "dist/index.js", + "module": "dist/index.js", + "types": "dist/index.d.ts", + "type": "module", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "files": [ + "dist" + ], + "scripts": { + "build": "tsup", + "build:watch": "tsup --watch", + "type-check": "tsc --noEmit", + "clean": "rimraf dist" + }, + "devDependencies": { + "@esengine/ecs-framework": "workspace:*", + "@esengine/build-config": "workspace:*", + "rimraf": "^5.0.5", + "tsup": "^8.0.0", + "typescript": "^5.3.3" + }, + "keywords": [ + "ecs", + "engine", + "transform" + ], + "author": "yhh", + "license": "MIT" +} diff --git a/packages/engine-core/src/EnginePlugin.ts b/packages/engine-core/src/EnginePlugin.ts new file mode 100644 index 00000000..72aee990 --- /dev/null +++ b/packages/engine-core/src/EnginePlugin.ts @@ -0,0 +1,227 @@ +/** + * 插件系统核心类型定义 + * Plugin system core type definitions + * + * 这是插件类型的唯一定义源,editor-core 重新导出并扩展这些类型。 + * This is the single source of truth for plugin types. editor-core re-exports and extends them. + */ + +import type { ComponentRegistry as ComponentRegistryType, IScene, ServiceContainer } from '@esengine/ecs-framework'; +import { TransformComponent } from './TransformComponent'; + +// ============================================================================ +// 基础类型 | Basic Types +// ============================================================================ + +/** + * 插件类别 + * Plugin category + */ +export type PluginCategory = + | 'core' // 核心功能 | Core functionality + | 'rendering' // 渲染相关 | Rendering + | 'ui' // UI 系统 | UI System + | 'ai' // AI/行为树 | AI/Behavior + | 'physics' // 物理引擎 | Physics + | 'audio' // 音频系统 | Audio + | 'networking' // 网络功能 | Networking + | 'tools' // 工具/编辑器扩展 | Tools/Editor extensions + | 'scripting' // 脚本/蓝图 | Scripting/Blueprint + | 'tilemap' // 瓦片地图 | Tilemap + | 'content'; // 内容/资源 | Content/Assets + +/** + * 加载阶段 - 控制插件模块的加载顺序 + * Loading phase - controls the loading order of plugin modules + */ +export type LoadingPhase = + | 'earliest' // 最早加载(核心模块) | Earliest (core modules) + | 'preDefault' // 默认之前 | Before default + | 'default' // 默认阶段 | Default phase + | 'postDefault' // 默认之后 | After default + | 'postEngine'; // 引擎初始化后 | After engine init + +/** + * 模块类型 + * Module type + */ +export type ModuleType = 'runtime' | 'editor'; + +/** + * 模块描述符 + * Module descriptor + */ +export interface ModuleDescriptor { + /** 模块名称 | Module name */ + name: string; + /** 模块类型 | Module type */ + type: ModuleType; + /** 加载阶段 | Loading phase */ + loadingPhase?: LoadingPhase; +} + +/** + * 插件依赖 + * Plugin dependency + */ +export interface PluginDependency { + /** 依赖的插件ID | Dependent plugin ID */ + id: string; + /** 版本要求 | Version requirement */ + version?: string; + /** 是否可选 | Optional */ + optional?: boolean; +} + +// ============================================================================ +// 插件描述符 | Plugin Descriptor +// ============================================================================ + +/** + * 插件描述符 + * Plugin descriptor + * + * 所有字段都是可选的,PluginManager 会填充默认值。 + * All fields are optional, PluginManager will fill in defaults. + */ +export interface PluginDescriptor { + /** 插件唯一标识符 | Unique plugin ID */ + id: string; + /** 显示名称 | Display name */ + name: string; + /** 版本号 | Version */ + version: string; + /** 描述 | Description */ + description?: string; + /** 插件类别 | Plugin category */ + category?: PluginCategory; + /** 标签(用于搜索) | Tags (for search) */ + tags?: string[]; + /** 图标(Lucide 图标名) | Icon (Lucide icon name) */ + icon?: string; + /** 是否默认启用 | Enabled by default */ + enabledByDefault?: boolean; + /** 是否可以包含内容资产 | Can contain content assets */ + canContainContent?: boolean; + /** 是否为引擎内置插件 | Is engine built-in plugin */ + isEnginePlugin?: boolean; + /** 是否为核心插件(不可禁用) | Is core plugin (cannot be disabled) */ + isCore?: boolean; + /** 模块列表 | Module list */ + modules?: ModuleDescriptor[]; + /** 依赖列表 | Dependency list */ + dependencies?: PluginDependency[]; + /** 平台要求 | Platform requirements */ + platforms?: ('web' | 'desktop' | 'mobile')[]; +} + +// ============================================================================ +// 系统上下文 | System Context +// ============================================================================ + +/** + * 系统创建上下文 + * System creation context + */ +export interface SystemContext { + /** 是否为编辑器模式 | Is editor mode */ + isEditor: boolean; + /** 引擎桥接(如有) | Engine bridge (if available) */ + engineBridge?: any; + /** 渲染系统(如有) | Render system (if available) */ + renderSystem?: any; + /** 其他已创建的系统引用 | Other created system references */ + [key: string]: any; +} + +// ============================================================================ +// 运行时模块 | Runtime Module +// ============================================================================ + +/** + * 运行时模块接口 + * Runtime module interface + */ +export interface IRuntimeModule { + /** + * 注册组件到 ComponentRegistry + * Register components to ComponentRegistry + */ + registerComponents?(registry: typeof ComponentRegistryType): void; + + /** + * 注册服务到 ServiceContainer + * Register services to ServiceContainer + */ + registerServices?(services: ServiceContainer): void; + + /** + * 为场景创建系统 + * Create systems for scene + */ + createSystems?(scene: IScene, context: SystemContext): void; + + /** + * 所有系统创建完成后调用 + * Called after all systems are created + */ + onSystemsCreated?(scene: IScene, context: SystemContext): void; + + /** + * 模块初始化完成回调 + * Module initialization complete callback + */ + onInitialize?(): Promise; + + /** + * 模块销毁回调 + * Module destroy callback + */ + onDestroy?(): void; +} + +// ============================================================================ +// 插件接口 | Plugin Interface +// ============================================================================ + +/** + * 插件接口 + * Plugin interface + * + * 这是所有插件包导出的统一类型。 + * This is the unified type that all plugin packages export. + */ +export interface IPlugin { + /** 插件描述符 | Plugin descriptor */ + readonly descriptor: PluginDescriptor; + /** 运行时模块(可选) | Runtime module (optional) */ + readonly runtimeModule?: IRuntimeModule; + /** 编辑器模块(可选,类型为 any 以避免循环依赖) | Editor module (optional, typed as any to avoid circular deps) */ + readonly editorModule?: any; +} + +// ============================================================================ +// Engine Core 插件 | Engine Core Plugin +// ============================================================================ + +class EngineRuntimeModule implements IRuntimeModule { + registerComponents(registry: typeof ComponentRegistryType): void { + registry.register(TransformComponent); + } +} + +const descriptor: PluginDescriptor = { + id: '@esengine/engine-core', + name: 'Engine Core', + version: '1.0.0', + description: 'Transform 等核心组件', + category: 'core', + enabledByDefault: true, + isEnginePlugin: true, + isCore: true +}; + +export const EnginePlugin: IPlugin = { + descriptor, + runtimeModule: new EngineRuntimeModule() +}; diff --git a/packages/engine-core/src/HierarchyComponent.ts b/packages/engine-core/src/HierarchyComponent.ts new file mode 100644 index 00000000..0b37f509 --- /dev/null +++ b/packages/engine-core/src/HierarchyComponent.ts @@ -0,0 +1,2 @@ +// Re-export from ecs-framework +export { HierarchyComponent } from '@esengine/ecs-framework'; diff --git a/packages/engine-core/src/HierarchySystem.ts b/packages/engine-core/src/HierarchySystem.ts new file mode 100644 index 00000000..e16f983a --- /dev/null +++ b/packages/engine-core/src/HierarchySystem.ts @@ -0,0 +1,2 @@ +// Re-export from ecs-framework +export { HierarchySystem } from '@esengine/ecs-framework'; diff --git a/packages/engine-core/src/TransformComponent.ts b/packages/engine-core/src/TransformComponent.ts new file mode 100644 index 00000000..bdc8e7a4 --- /dev/null +++ b/packages/engine-core/src/TransformComponent.ts @@ -0,0 +1,110 @@ +import { Component, ECSComponent, Serializable, Serialize, Property } from '@esengine/ecs-framework'; + +export interface Vector3 { + x: number; + y: number; + z: number; +} + +/** + * 3x3 矩阵(用于 2D 变换:旋转 + 缩放) + * 实际存储为 [a, b, c, d, tx, ty] 形式的仿射变换 + */ +export interface Matrix2D { + a: number; // scaleX * cos(rotation) + b: number; // scaleX * sin(rotation) + c: number; // scaleY * -sin(rotation) + d: number; // scaleY * cos(rotation) + tx: number; // translateX + ty: number; // translateY +} + +@ECSComponent('Transform') +@Serializable({ version: 1, typeId: 'Transform' }) +export class TransformComponent extends Component { + @Serialize() + @Property({ type: 'vector3', label: 'Position' }) + position: Vector3 = { x: 0, y: 0, z: 0 }; + + /** 欧拉角,单位:度 */ + @Serialize() + @Property({ type: 'vector3', label: 'Rotation' }) + rotation: Vector3 = { x: 0, y: 0, z: 0 }; + + @Serialize() + @Property({ type: 'vector3', label: 'Scale' }) + scale: Vector3 = { x: 1, y: 1, z: 1 }; + + // ===== 世界变换(由 TransformSystem 计算)===== + + /** 世界位置(只读,由 TransformSystem 计算) */ + worldPosition: Vector3 = { x: 0, y: 0, z: 0 }; + + /** 世界旋转(只读,由 TransformSystem 计算) */ + worldRotation: Vector3 = { x: 0, y: 0, z: 0 }; + + /** 世界缩放(只读,由 TransformSystem 计算) */ + worldScale: Vector3 = { x: 1, y: 1, z: 1 }; + + /** 本地到世界的 2D 变换矩阵(只读,由 TransformSystem 计算) */ + localToWorldMatrix: Matrix2D = { a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0 }; + + /** 变换是否需要更新 */ + bDirty: boolean = true; + + constructor(x: number = 0, y: number = 0, z: number = 0) { + super(); + this.position = { x, y, z }; + // 初始化世界变换为本地变换值(在 TransformSystem 更新前使用) + this.worldPosition = { x, y, z }; + } + + setPosition(x: number, y: number, z: number = 0): this { + this.position = { x, y, z }; + this.bDirty = true; + return this; + } + + setRotation(x: number, y: number, z: number): this { + this.rotation = { x, y, z }; + this.bDirty = true; + return this; + } + + setScale(x: number, y: number, z: number = 1): this { + this.scale = { x, y, z }; + this.bDirty = true; + return this; + } + + /** + * 将本地坐标转换为世界坐标 + */ + localToWorld(localX: number, localY: number): { x: number; y: number } { + const m = this.localToWorldMatrix; + return { + x: m.a * localX + m.c * localY + m.tx, + y: m.b * localX + m.d * localY + m.ty + }; + } + + /** + * 将世界坐标转换为本地坐标 + */ + worldToLocal(worldX: number, worldY: number): { x: number; y: number } { + const m = this.localToWorldMatrix; + const det = m.a * m.d - m.b * m.c; + if (Math.abs(det) < 1e-10) { + return { x: 0, y: 0 }; + } + + const invDet = 1 / det; + const dx = worldX - m.tx; + const dy = worldY - m.ty; + + return { + x: (m.d * dx - m.c * dy) * invDet, + y: (-m.b * dx + m.a * dy) * invDet + }; + } +} diff --git a/packages/engine-core/src/TransformSystem.ts b/packages/engine-core/src/TransformSystem.ts new file mode 100644 index 00000000..fde4f06e --- /dev/null +++ b/packages/engine-core/src/TransformSystem.ts @@ -0,0 +1,145 @@ +import { EntitySystem, Matcher, Entity, ECSSystem, HierarchyComponent } from '@esengine/ecs-framework'; +import { TransformComponent, Matrix2D } from './TransformComponent'; + +const DEG_TO_RAD = Math.PI / 180; + +/** + * 变换系统 + * Transform System - Calculates world transforms based on hierarchy + * + * 根据实体层级关系计算世界变换矩阵。 + * 子实体的世界变换 = 父实体世界变换 * 子实体本地变换 + */ +@ECSSystem('Transform', { updateOrder: -100 }) +export class TransformSystem extends EntitySystem { + constructor() { + super(Matcher.empty().all(TransformComponent)); + } + + protected override process(entities: readonly Entity[]): void { + // 获取所有根实体(没有父级或父级没有 TransformComponent) + const rootEntities = entities.filter(e => { + const hierarchy = e.getComponent(HierarchyComponent); + if (!hierarchy || hierarchy.parentId === null) { + return true; + } + const parent = this.scene?.findEntityById(hierarchy.parentId); + return !parent || !parent.hasComponent(TransformComponent); + }); + + // 从根实体开始递归计算世界变换 + for (const entity of rootEntities) { + this.updateWorldTransform(entity, null); + } + } + + /** + * 递归更新实体及其子实体的世界变换 + */ + private updateWorldTransform(entity: Entity, parentMatrix: Matrix2D | null): void { + const transform = entity.getComponent(TransformComponent); + if (!transform) return; + + // 计算本地变换矩阵 + const localMatrix = this.calculateLocalMatrix(transform); + + // 计算世界变换矩阵 + if (parentMatrix) { + // 世界矩阵 = 父矩阵 * 本地矩阵 + transform.localToWorldMatrix = this.multiplyMatrices(parentMatrix, localMatrix); + } else { + // 没有父级,本地矩阵就是世界矩阵 + transform.localToWorldMatrix = localMatrix; + } + + // 从世界矩阵提取世界位置、旋转、缩放 + this.decomposeMatrix(transform); + + transform.bDirty = false; + + // 递归处理子实体 + const hierarchy = entity.getComponent(HierarchyComponent); + if (hierarchy && hierarchy.childIds.length > 0) { + for (const childId of hierarchy.childIds) { + const child = this.scene?.findEntityById(childId); + if (child) { + this.updateWorldTransform(child, transform.localToWorldMatrix); + } + } + } + } + + /** + * 计算本地变换矩阵 + */ + private calculateLocalMatrix(transform: TransformComponent): Matrix2D { + const { position, rotation, scale } = transform; + + // 只使用 z 轴旋转(2D) + const rad = rotation.z * DEG_TO_RAD; + const cos = Math.cos(rad); + const sin = Math.sin(rad); + + // 构建仿射变换矩阵: Scale -> Rotate -> Translate + // [a c tx] [sx 0 0] [cos -sin 0] [1 0 tx] + // [b d ty] = [0 sy 0] * [sin cos 0] * [0 1 ty] + // [0 0 1] [0 0 1] [0 0 1] [0 0 1] + + return { + a: scale.x * cos, + b: scale.x * sin, + c: scale.y * -sin, + d: scale.y * cos, + tx: position.x, + ty: position.y + }; + } + + /** + * 矩阵乘法: result = a * b + */ + private multiplyMatrices(a: Matrix2D, b: Matrix2D): Matrix2D { + return { + a: a.a * b.a + a.c * b.b, + b: a.b * b.a + a.d * b.b, + c: a.a * b.c + a.c * b.d, + d: a.b * b.c + a.d * b.d, + tx: a.a * b.tx + a.c * b.ty + a.tx, + ty: a.b * b.tx + a.d * b.ty + a.ty + }; + } + + /** + * 从世界矩阵分解出位置、旋转、缩放 + */ + private decomposeMatrix(transform: TransformComponent): void { + const m = transform.localToWorldMatrix; + + // 位置直接从矩阵获取 + transform.worldPosition.x = m.tx; + transform.worldPosition.y = m.ty; + transform.worldPosition.z = transform.position.z; + + // 计算缩放 + const scaleX = Math.sqrt(m.a * m.a + m.b * m.b); + const scaleY = Math.sqrt(m.c * m.c + m.d * m.d); + + // 检测负缩放(通过行列式符号) + const det = m.a * m.d - m.b * m.c; + const sign = det < 0 ? -1 : 1; + + transform.worldScale.x = scaleX; + transform.worldScale.y = scaleY * sign; + transform.worldScale.z = transform.scale.z; + + // 计算旋转(从归一化的矩阵) + if (scaleX > 1e-10) { + const rotation = Math.atan2(m.b / scaleX, m.a / scaleX); + transform.worldRotation.z = rotation / DEG_TO_RAD; + } else { + transform.worldRotation.z = 0; + } + transform.worldRotation.x = transform.rotation.x; + transform.worldRotation.y = transform.rotation.y; + } +} diff --git a/packages/engine-core/src/index.ts b/packages/engine-core/src/index.ts new file mode 100644 index 00000000..2ca10a7a --- /dev/null +++ b/packages/engine-core/src/index.ts @@ -0,0 +1,17 @@ +export { TransformComponent, type Vector3, type Matrix2D } from './TransformComponent'; +export { TransformSystem } from './TransformSystem'; +export { HierarchyComponent } from './HierarchyComponent'; +export { HierarchySystem } from './HierarchySystem'; +export { + EnginePlugin, + // 类型导出 + type PluginCategory, + type LoadingPhase, + type ModuleType, + type ModuleDescriptor, + type PluginDependency, + type PluginDescriptor, + type SystemContext, + type IRuntimeModule, + type IPlugin +} from './EnginePlugin'; diff --git a/packages/engine-core/tsconfig.build.json b/packages/engine-core/tsconfig.build.json new file mode 100644 index 00000000..f39a0594 --- /dev/null +++ b/packages/engine-core/tsconfig.build.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "composite": false, + "declaration": true, + "declarationMap": true, + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "**/*.test.ts"] +} diff --git a/packages/engine-core/tsconfig.json b/packages/engine-core/tsconfig.json new file mode 100644 index 00000000..02f5f187 --- /dev/null +++ b/packages/engine-core/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"], + "references": [ + { "path": "../core" } + ] +} diff --git a/packages/engine-core/tsup.config.ts b/packages/engine-core/tsup.config.ts new file mode 100644 index 00000000..f704a430 --- /dev/null +++ b/packages/engine-core/tsup.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'tsup'; +import { runtimeOnlyPreset } from '../build-config/src/presets/plugin-tsup'; + +export default defineConfig({ + ...runtimeOnlyPreset(), + tsconfig: 'tsconfig.build.json' +}); diff --git a/packages/node-editor/package.json b/packages/node-editor/package.json index 8c3afebc..e3812889 100644 --- a/packages/node-editor/package.json +++ b/packages/node-editor/package.json @@ -35,19 +35,9 @@ ], "author": "yhh", "license": "MIT", - "peerDependencies": { - "react": "^18.3.1", - "zustand": "^4.5.2" - }, - "peerDependenciesMeta": { - "react": { - "optional": false - }, - "zustand": { - "optional": false - } - }, "devDependencies": { + "react": "^18.3.1", + "zustand": "^5.0.8", "@types/node": "^20.19.17", "@types/react": "^18.3.12", "@vitejs/plugin-react": "^4.7.0", diff --git a/packages/physics-rapier2d-editor/package.json b/packages/physics-rapier2d-editor/package.json new file mode 100644 index 00000000..2bb3b3a1 --- /dev/null +++ b/packages/physics-rapier2d-editor/package.json @@ -0,0 +1,47 @@ +{ + "name": "@esengine/physics-rapier2d-editor", + "version": "1.0.0", + "description": "Editor support for @esengine/physics-rapier2d - inspectors and gizmos", + "main": "dist/index.js", + "module": "dist/index.js", + "types": "dist/index.d.ts", + "type": "module", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "files": [ + "dist" + ], + "scripts": { + "build": "tsup", + "build:watch": "tsup --watch", + "type-check": "tsc --noEmit", + "clean": "rimraf dist" + }, + "dependencies": { + "@esengine/physics-rapier2d": "workspace:*" + }, + "devDependencies": { + "@esengine/ecs-framework": "workspace:*", + "@esengine/engine-core": "workspace:*", + "@esengine/editor-core": "workspace:*", + "@esengine/build-config": "workspace:*", + "lucide-react": "^0.545.0", + "react": "^18.3.1", + "@types/react": "^18.3.12", + "rimraf": "^5.0.5", + "tsup": "^8.0.0", + "typescript": "^5.3.3" + }, + "keywords": [ + "ecs", + "physics", + "rapier2d", + "editor" + ], + "author": "", + "license": "MIT" +} diff --git a/packages/physics-rapier2d/src/editor/index.ts b/packages/physics-rapier2d-editor/src/Physics2DEditorModule.ts similarity index 94% rename from packages/physics-rapier2d/src/editor/index.ts rename to packages/physics-rapier2d-editor/src/Physics2DEditorModule.ts index 89c90564..9d49c398 100644 --- a/packages/physics-rapier2d/src/editor/index.ts +++ b/packages/physics-rapier2d-editor/src/Physics2DEditorModule.ts @@ -1,6 +1,6 @@ /** - * Physics 2D Editor Module Entry - * 2D 物理编辑器模块入口 + * Physics 2D Editor Module + * 2D 物理编辑器模块 */ import type { ServiceContainer, Entity } from '@esengine/ecs-framework'; @@ -16,17 +16,21 @@ import { ComponentRegistry, SettingsRegistry } from '@esengine/editor-core'; -import { DEFAULT_PHYSICS_CONFIG } from '../types/Physics2DTypes'; -import { TransformComponent } from '@esengine/ecs-components'; +import { TransformComponent } from '@esengine/engine-core'; -// Local imports -import { Rigidbody2DComponent } from '../components/Rigidbody2DComponent'; -import { BoxCollider2DComponent } from '../components/BoxCollider2DComponent'; -import { CircleCollider2DComponent } from '../components/CircleCollider2DComponent'; -import { CapsuleCollider2DComponent } from '../components/CapsuleCollider2DComponent'; -import { PolygonCollider2DComponent } from '../components/PolygonCollider2DComponent'; +// Import from @esengine/physics-rapier2d package +import { + DEFAULT_PHYSICS_CONFIG, + Rigidbody2DComponent, + BoxCollider2DComponent, + CircleCollider2DComponent, + CapsuleCollider2DComponent, + PolygonCollider2DComponent +} from '@esengine/physics-rapier2d'; +import { Physics2DSystem } from '@esengine/physics-rapier2d/runtime'; + +// Local editor imports import { registerPhysics2DGizmos } from './gizmos/Physics2DGizmo'; -import { Physics2DSystem } from '../systems/Physics2DSystem'; import { CollisionMatrixEditor } from './components/CollisionMatrixEditor'; /** @@ -418,7 +422,3 @@ export class Physics2DEditorModule implements IEditorModuleLoader { } export const physics2DEditorModule = new Physics2DEditorModule(); - -// Plugin exports -export { Physics2DPlugin } from './Physics2DPlugin'; -export default physics2DEditorModule; diff --git a/packages/physics-rapier2d-editor/src/Physics2DPlugin.ts b/packages/physics-rapier2d-editor/src/Physics2DPlugin.ts new file mode 100644 index 00000000..2f73ef07 --- /dev/null +++ b/packages/physics-rapier2d-editor/src/Physics2DPlugin.ts @@ -0,0 +1,37 @@ +/** + * Physics 2D Plugin (Complete) + * 完整的 2D 物理插件(运行时 + 编辑器) + */ + +import type { IPlugin, PluginDescriptor } from '@esengine/editor-core'; +import { PhysicsRuntimeModule } from '@esengine/physics-rapier2d/runtime'; +import { physics2DEditorModule } from './Physics2DEditorModule'; + +/** + * Physics 2D 插件描述符 + * Physics 2D Plugin Descriptor + */ +const descriptor: PluginDescriptor = { + id: '@esengine/physics-rapier2d', + name: 'Physics 2D', + version: '1.0.0', + description: 'Deterministic 2D physics with Rapier2D', + category: 'physics', + enabledByDefault: true, + isEnginePlugin: true, + canContainContent: false, + modules: [ + { name: 'Runtime', type: 'runtime', loadingPhase: 'default' }, + { name: 'Editor', type: 'editor', loadingPhase: 'postDefault' } + ] +}; + +/** + * 完整的 Physics 2D 插件(运行时 + 编辑器) + * Complete Physics 2D Plugin (runtime + editor) + */ +export const Physics2DPlugin: IPlugin = { + descriptor, + runtimeModule: new PhysicsRuntimeModule(), + editorModule: physics2DEditorModule +}; diff --git a/packages/physics-rapier2d/src/editor/components/CollisionLayerSelector.tsx b/packages/physics-rapier2d-editor/src/components/CollisionLayerSelector.tsx similarity index 99% rename from packages/physics-rapier2d/src/editor/components/CollisionLayerSelector.tsx rename to packages/physics-rapier2d-editor/src/components/CollisionLayerSelector.tsx index 4e8ebf38..c7a91837 100644 --- a/packages/physics-rapier2d/src/editor/components/CollisionLayerSelector.tsx +++ b/packages/physics-rapier2d-editor/src/components/CollisionLayerSelector.tsx @@ -4,7 +4,7 @@ */ import React, { useState, useEffect, useRef } from 'react'; -import { CollisionLayerConfig } from '../../services/CollisionLayerConfig'; +import { CollisionLayerConfig } from '@esengine/physics-rapier2d'; interface CollisionLayerSelectorProps { value: number; diff --git a/packages/physics-rapier2d/src/editor/components/CollisionMatrixEditor.tsx b/packages/physics-rapier2d-editor/src/components/CollisionMatrixEditor.tsx similarity index 99% rename from packages/physics-rapier2d/src/editor/components/CollisionMatrixEditor.tsx rename to packages/physics-rapier2d-editor/src/components/CollisionMatrixEditor.tsx index 86a4ee70..295796fe 100644 --- a/packages/physics-rapier2d/src/editor/components/CollisionMatrixEditor.tsx +++ b/packages/physics-rapier2d-editor/src/components/CollisionMatrixEditor.tsx @@ -7,7 +7,7 @@ */ import React, { useState, useEffect, useCallback, useRef } from 'react'; -import { CollisionLayerConfig } from '../../services/CollisionLayerConfig'; +import { CollisionLayerConfig } from '@esengine/physics-rapier2d'; interface CollisionMatrixEditorProps { onClose?: () => void; diff --git a/packages/physics-rapier2d/src/editor/gizmos/Physics2DGizmo.ts b/packages/physics-rapier2d-editor/src/gizmos/Physics2DGizmo.ts similarity index 95% rename from packages/physics-rapier2d/src/editor/gizmos/Physics2DGizmo.ts rename to packages/physics-rapier2d-editor/src/gizmos/Physics2DGizmo.ts index d56b0a76..370d00fc 100644 --- a/packages/physics-rapier2d/src/editor/gizmos/Physics2DGizmo.ts +++ b/packages/physics-rapier2d-editor/src/gizmos/Physics2DGizmo.ts @@ -16,14 +16,16 @@ import type { GizmoColor } from '@esengine/editor-core'; import { GizmoRegistry } from '@esengine/editor-core'; -import { TransformComponent } from '@esengine/ecs-components'; +import { TransformComponent } from '@esengine/engine-core'; -import { BoxCollider2DComponent } from '../../components/BoxCollider2DComponent'; -import { CircleCollider2DComponent } from '../../components/CircleCollider2DComponent'; -import { CapsuleCollider2DComponent } from '../../components/CapsuleCollider2DComponent'; -import { PolygonCollider2DComponent } from '../../components/PolygonCollider2DComponent'; -import { Rigidbody2DComponent } from '../../components/Rigidbody2DComponent'; -import { RigidbodyType2D } from '../../types/Physics2DTypes'; +import { + BoxCollider2DComponent, + CircleCollider2DComponent, + CapsuleCollider2DComponent, + PolygonCollider2DComponent, + Rigidbody2DComponent, + RigidbodyType2D +} from '@esengine/physics-rapier2d'; /** * 物理 Gizmo 颜色配置 diff --git a/packages/physics-rapier2d-editor/src/index.ts b/packages/physics-rapier2d-editor/src/index.ts new file mode 100644 index 00000000..1a6ae785 --- /dev/null +++ b/packages/physics-rapier2d-editor/src/index.ts @@ -0,0 +1,11 @@ +/** + * Physics 2D Editor Module Entry + * 2D 物理编辑器模块入口 + */ + +// Re-export editor module +export { Physics2DEditorModule, physics2DEditorModule } from './Physics2DEditorModule'; +export { physics2DEditorModule as default } from './Physics2DEditorModule'; + +// Re-export plugin +export { Physics2DPlugin } from './Physics2DPlugin'; diff --git a/packages/physics-rapier2d/src/editor/inspectors/BoxCollider2DInspector.tsx b/packages/physics-rapier2d-editor/src/inspectors/BoxCollider2DInspector.tsx similarity index 97% rename from packages/physics-rapier2d/src/editor/inspectors/BoxCollider2DInspector.tsx rename to packages/physics-rapier2d-editor/src/inspectors/BoxCollider2DInspector.tsx index be335f4a..0e27eadc 100644 --- a/packages/physics-rapier2d/src/editor/inspectors/BoxCollider2DInspector.tsx +++ b/packages/physics-rapier2d-editor/src/inspectors/BoxCollider2DInspector.tsx @@ -6,8 +6,7 @@ import React from 'react'; import { Component } from '@esengine/ecs-framework'; import type { IComponentInspector, ComponentInspectorContext } from '@esengine/editor-core'; -import { BoxCollider2DComponent } from '../../components/BoxCollider2DComponent'; -import { CollisionLayer2D } from '../../types/Physics2DTypes'; +import { BoxCollider2DComponent, CollisionLayer2D } from '@esengine/physics-rapier2d'; export class BoxCollider2DInspectorProvider implements IComponentInspector { readonly id = 'boxcollider2d-inspector'; diff --git a/packages/physics-rapier2d/src/editor/inspectors/CircleCollider2DInspector.tsx b/packages/physics-rapier2d-editor/src/inspectors/CircleCollider2DInspector.tsx similarity index 97% rename from packages/physics-rapier2d/src/editor/inspectors/CircleCollider2DInspector.tsx rename to packages/physics-rapier2d-editor/src/inspectors/CircleCollider2DInspector.tsx index d16d6542..14d72488 100644 --- a/packages/physics-rapier2d/src/editor/inspectors/CircleCollider2DInspector.tsx +++ b/packages/physics-rapier2d-editor/src/inspectors/CircleCollider2DInspector.tsx @@ -6,8 +6,7 @@ import React from 'react'; import { Component } from '@esengine/ecs-framework'; import type { IComponentInspector, ComponentInspectorContext } from '@esengine/editor-core'; -import { CircleCollider2DComponent } from '../../components/CircleCollider2DComponent'; -import { CollisionLayer2D } from '../../types/Physics2DTypes'; +import { CircleCollider2DComponent, CollisionLayer2D } from '@esengine/physics-rapier2d'; export class CircleCollider2DInspectorProvider implements IComponentInspector { readonly id = 'circlecollider2d-inspector'; diff --git a/packages/physics-rapier2d/src/editor/inspectors/Rigidbody2DInspector.tsx b/packages/physics-rapier2d-editor/src/inspectors/Rigidbody2DInspector.tsx similarity index 98% rename from packages/physics-rapier2d/src/editor/inspectors/Rigidbody2DInspector.tsx rename to packages/physics-rapier2d-editor/src/inspectors/Rigidbody2DInspector.tsx index 77a7c77a..b38c4969 100644 --- a/packages/physics-rapier2d/src/editor/inspectors/Rigidbody2DInspector.tsx +++ b/packages/physics-rapier2d-editor/src/inspectors/Rigidbody2DInspector.tsx @@ -6,8 +6,7 @@ import React from 'react'; import { Component } from '@esengine/ecs-framework'; import type { IComponentInspector, ComponentInspectorContext } from '@esengine/editor-core'; -import { Rigidbody2DComponent } from '../../components/Rigidbody2DComponent'; -import { RigidbodyType2D, CollisionDetectionMode2D } from '../../types/Physics2DTypes'; +import { Rigidbody2DComponent, RigidbodyType2D, CollisionDetectionMode2D } from '@esengine/physics-rapier2d'; export class Rigidbody2DInspectorProvider implements IComponentInspector { readonly id = 'rigidbody2d-inspector'; diff --git a/packages/physics-rapier2d-editor/tsconfig.build.json b/packages/physics-rapier2d-editor/tsconfig.build.json new file mode 100644 index 00000000..ba0684d9 --- /dev/null +++ b/packages/physics-rapier2d-editor/tsconfig.build.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ES2020", + "moduleResolution": "bundler", + "lib": ["ES2020", "DOM"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "declaration": true, + "declarationMap": true, + "jsx": "react-jsx", + "resolveJsonModule": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/physics-rapier2d-editor/tsconfig.json b/packages/physics-rapier2d-editor/tsconfig.json new file mode 100644 index 00000000..d659ba24 --- /dev/null +++ b/packages/physics-rapier2d-editor/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src", + "declaration": true, + "jsx": "react-jsx", + "moduleResolution": "bundler" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/physics-rapier2d-editor/tsup.config.ts b/packages/physics-rapier2d-editor/tsup.config.ts new file mode 100644 index 00000000..b4f49f5d --- /dev/null +++ b/packages/physics-rapier2d-editor/tsup.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'tsup'; +import { editorOnlyPreset } from '../build-config/src/presets/plugin-tsup'; + +export default defineConfig({ + ...editorOnlyPreset(), + tsconfig: 'tsconfig.build.json' +}); diff --git a/packages/physics-rapier2d/package.json b/packages/physics-rapier2d/package.json index a64ca634..cb5686ad 100644 --- a/packages/physics-rapier2d/package.json +++ b/packages/physics-rapier2d/package.json @@ -2,6 +2,11 @@ "name": "@esengine/physics-rapier2d", "version": "1.0.0", "description": "Deterministic 2D physics engine based on Rapier2D with enhanced-determinism", + "esengine": { + "plugin": true, + "pluginExport": "PhysicsPlugin", + "category": "physics" + }, "main": "dist/index.js", "module": "dist/index.js", "types": "dist/index.d.ts", @@ -14,21 +19,15 @@ "./runtime": { "types": "./dist/runtime.d.ts", "import": "./dist/runtime.js" - }, - "./editor": { - "types": "./dist/editor/index.d.ts", - "import": "./dist/editor/index.js" - }, - "./plugin.json": "./plugin.json" + } }, "files": [ - "dist", - "plugin.json" + "dist" ], "scripts": { "clean": "rimraf dist tsconfig.tsbuildinfo", - "build": "vite build", - "build:watch": "vite build --watch", + "build": "tsup", + "build:watch": "tsup --watch", "type-check": "tsc --noEmit" }, "keywords": [ @@ -41,30 +40,17 @@ ], "author": "yhh", "license": "MIT", - "peerDependencies": { - "@esengine/ecs-framework": ">=2.0.0", - "@esengine/ecs-components": "workspace:*", - "@esengine/editor-core": "workspace:*", - "react": "^18.3.1" - }, - "peerDependenciesMeta": { - "@esengine/editor-core": { - "optional": true - }, - "react": { - "optional": true - } - }, "dependencies": { "@dimforge/rapier2d-compat": "^0.14.0" }, "devDependencies": { - "@types/react": "^18.3.12", - "@vitejs/plugin-react": "^4.7.0", + "@esengine/ecs-framework": "workspace:*", + "@esengine/engine-core": "workspace:*", + "@esengine/editor-core": "workspace:*", + "@esengine/build-config": "workspace:*", "rimraf": "^5.0.0", - "typescript": "^5.8.3", - "vite": "^6.0.7", - "vite-plugin-dts": "^4.5.0" + "tsup": "^8.0.0", + "typescript": "^5.8.3" }, "publishConfig": { "access": "public", diff --git a/packages/physics-rapier2d/src/Physics2DComponentsModule.ts b/packages/physics-rapier2d/src/Physics2DComponentsModule.ts index dd2ac7e8..5650c9b6 100644 --- a/packages/physics-rapier2d/src/Physics2DComponentsModule.ts +++ b/packages/physics-rapier2d/src/Physics2DComponentsModule.ts @@ -7,7 +7,7 @@ */ import { ComponentRegistry } from '@esengine/ecs-framework'; -import type { IRuntimeModuleLoader } from '@esengine/ecs-components'; +import type { IRuntimeModule } from '@esengine/engine-core'; // Components (no WASM dependency) import { Rigidbody2DComponent } from './components/Rigidbody2DComponent'; @@ -23,7 +23,7 @@ import { PolygonCollider2DComponent } from './components/PolygonCollider2DCompon * 仅实现组件注册,不包含系统创建和 WASM 初始化 * 用于编辑器场景序列化 */ -export class Physics2DComponentsModule implements IRuntimeModuleLoader { +export class Physics2DComponentsModule implements IRuntimeModule { /** * 注册组件到 ComponentRegistry */ diff --git a/packages/physics-rapier2d/src/PhysicsEditorPlugin.ts b/packages/physics-rapier2d/src/PhysicsEditorPlugin.ts new file mode 100644 index 00000000..f9e4afab --- /dev/null +++ b/packages/physics-rapier2d/src/PhysicsEditorPlugin.ts @@ -0,0 +1,40 @@ +/** + * Physics Editor Plugin + * + * 编辑器版本的物理插件,不包含 WASM 依赖。 + * Editor version of physics plugin, without WASM dependencies. + * + * 用于编辑器中注册插件描述符,但不创建运行时模块。 + * 运行时使用 PhysicsPlugin from '@esengine/physics-rapier2d/runtime' + */ + +import type { IPlugin, PluginDescriptor } from '@esengine/engine-core'; + +const descriptor: PluginDescriptor = { + id: '@esengine/physics-rapier2d', + name: 'Physics 2D', + version: '1.0.0', + description: 'Deterministic 2D physics with Rapier2D', + category: 'physics', + enabledByDefault: false, + canContainContent: false, + isEnginePlugin: true, + modules: [ + { + name: 'PhysicsRuntime', + type: 'runtime', + loadingPhase: 'default' + } + ] +}; + +/** + * 编辑器物理插件(无运行时模块) + * Editor physics plugin (no runtime module) + * + * 编辑器使用此版本注册插件,运行时使用带 WASM 的完整版本。 + */ +export const Physics2DPlugin: IPlugin = { + descriptor + // No runtime module - editor doesn't need physics simulation +}; diff --git a/packages/physics-rapier2d/src/PhysicsRuntimeModule.ts b/packages/physics-rapier2d/src/PhysicsRuntimeModule.ts index fc5a4cbf..18fabfc1 100644 --- a/packages/physics-rapier2d/src/PhysicsRuntimeModule.ts +++ b/packages/physics-rapier2d/src/PhysicsRuntimeModule.ts @@ -1,54 +1,31 @@ -/** - * Physics 2D Runtime Module - * 2D 物理运行时模块 - * - * 提供确定性 2D 物理模拟功能 - */ - import type { IScene, ServiceContainer } from '@esengine/ecs-framework'; import { ComponentRegistry } from '@esengine/ecs-framework'; -import type { IRuntimeModuleLoader, SystemContext } from '@esengine/ecs-components'; +import type { IRuntimeModule, IPlugin, PluginDescriptor, SystemContext } from '@esengine/engine-core'; import * as RAPIER from '@dimforge/rapier2d-compat'; -// Components import { Rigidbody2DComponent } from './components/Rigidbody2DComponent'; import { BoxCollider2DComponent } from './components/BoxCollider2DComponent'; import { CircleCollider2DComponent } from './components/CircleCollider2DComponent'; import { CapsuleCollider2DComponent } from './components/CapsuleCollider2DComponent'; import { PolygonCollider2DComponent } from './components/PolygonCollider2DComponent'; - -// Systems import { Physics2DSystem } from './systems/Physics2DSystem'; - -// Services import { Physics2DService } from './services/Physics2DService'; -/** - * Physics 2D Runtime Module - * 2D 物理运行时模块 - * - * 实现 IRuntimeModuleLoader 接口,提供: - * - 物理组件注册 - * - 物理系统创建 - * - Rapier2D 初始化 - */ -export class PhysicsRuntimeModule implements IRuntimeModuleLoader { +export interface PhysicsSystemContext extends SystemContext { + physicsSystem?: Physics2DSystem; + physics2DWorld?: any; + physicsConfig?: any; +} + +class PhysicsRuntimeModule implements IRuntimeModule { private _rapierModule: typeof RAPIER | null = null; private _physicsSystem: Physics2DSystem | null = null; - /** - * 初始化模块 - * 异步初始化 Rapier2D WASM 模块 - */ async onInitialize(): Promise { - // 初始化 Rapier2D WASM await RAPIER.init(); this._rapierModule = RAPIER; } - /** - * 注册组件 - */ registerComponents(registry: typeof ComponentRegistry): void { registry.register(Rigidbody2DComponent); registry.register(BoxCollider2DComponent); @@ -57,61 +34,56 @@ export class PhysicsRuntimeModule implements IRuntimeModuleLoader { registry.register(PolygonCollider2DComponent); } - /** - * 注册服务 - */ - registerServices?(services: ServiceContainer): void { - // 注册物理服务 + registerServices(services: ServiceContainer): void { services.registerSingleton(Physics2DService); } - /** - * 创建系统 - */ createSystems(scene: IScene, context: SystemContext): void { - // 创建物理系统 + const physicsContext = context as PhysicsSystemContext; + const physicsSystem = new Physics2DSystem({ - physics: context.physicsConfig, - updateOrder: -1000 // 在其他系统之前运行 + physics: physicsContext.physicsConfig, + updateOrder: -1000 }); scene.addSystem(physicsSystem); this._physicsSystem = physicsSystem; - // 如果 Rapier 已加载,初始化物理系统 if (this._rapierModule) { physicsSystem.initializeWithRapier(this._rapierModule); } - // 导出到上下文供其他系统使用 - context.physicsSystem = physicsSystem; - context.physics2DWorld = physicsSystem.world; + physicsContext.physicsSystem = physicsSystem; + physicsContext.physics2DWorld = physicsSystem.world; } - /** - * 销毁模块 - */ onDestroy(): void { this._physicsSystem = null; this._rapierModule = null; } - /** - * 获取 Rapier 模块 - */ getRapierModule(): typeof RAPIER | null { return this._rapierModule; } - /** - * 获取物理系统 - */ getPhysicsSystem(): Physics2DSystem | null { return this._physicsSystem; } } -/** - * 默认导出模块实例 - */ -export default new PhysicsRuntimeModule(); +const descriptor: PluginDescriptor = { + id: '@esengine/physics-rapier2d', + name: 'Physics 2D', + version: '1.0.0', + description: 'Deterministic 2D physics with Rapier2D', + category: 'physics', + enabledByDefault: true, + isEnginePlugin: true +}; + +export const PhysicsPlugin: IPlugin = { + descriptor, + runtimeModule: new PhysicsRuntimeModule() +}; + +export { PhysicsRuntimeModule }; diff --git a/packages/physics-rapier2d/src/components/Collider2DBase.ts b/packages/physics-rapier2d/src/components/Collider2DBase.ts index 076ddf0a..b45ff803 100644 --- a/packages/physics-rapier2d/src/components/Collider2DBase.ts +++ b/packages/physics-rapier2d/src/components/Collider2DBase.ts @@ -4,7 +4,8 @@ */ import { Component, Property, Serialize } from '@esengine/ecs-framework'; -import { Vector2, CollisionLayer2D } from '../types/Physics2DTypes'; +import { CollisionLayer2D } from '../types/Physics2DTypes'; +import type { Vector2 } from '../types/Physics2DTypes'; /** * 2D 碰撞体基类 diff --git a/packages/physics-rapier2d/src/components/Rigidbody2DComponent.ts b/packages/physics-rapier2d/src/components/Rigidbody2DComponent.ts index 17e259e6..f4a019fe 100644 --- a/packages/physics-rapier2d/src/components/Rigidbody2DComponent.ts +++ b/packages/physics-rapier2d/src/components/Rigidbody2DComponent.ts @@ -4,7 +4,8 @@ */ import { Component, Property, Serialize, Serializable, ECSComponent } from '@esengine/ecs-framework'; -import { RigidbodyType2D, CollisionDetectionMode2D, Vector2 } from '../types/Physics2DTypes'; +import { RigidbodyType2D, CollisionDetectionMode2D } from '../types/Physics2DTypes'; +import type { Vector2 } from '../types/Physics2DTypes'; /** * 刚体约束配置 diff --git a/packages/physics-rapier2d/src/editor/Physics2DPlugin.ts b/packages/physics-rapier2d/src/editor/Physics2DPlugin.ts deleted file mode 100644 index d3e2af53..00000000 --- a/packages/physics-rapier2d/src/editor/Physics2DPlugin.ts +++ /dev/null @@ -1,65 +0,0 @@ -/** - * Physics 2D Unified Plugin - * 2D 物理统一插件 - * - * 编辑器专用插件入口 - * 使用完整运行时模块以支持编辑器预览 - */ - -import type { IPluginLoader, PluginDescriptor } from '@esengine/editor-core'; -import { Physics2DEditorModule } from './index'; -import { PhysicsRuntimeModule } from '../PhysicsRuntimeModule'; -import { CollisionLayerConfig } from '../services/CollisionLayerConfig'; - -// 暴露 CollisionLayerConfig 到全局,供 CollisionLayerField 访问 -(window as any).__PHYSICS_RAPIER2D__ = { - CollisionLayerConfig -}; - -/** - * 插件描述符 - */ -const descriptor: PluginDescriptor = { - id: '@esengine/physics-rapier2d', - name: 'Rapier 2D Physics', - version: '1.0.0', - description: '基于 Rapier2D 的确定性 2D 物理引擎', - category: 'physics', - enabledByDefault: true, - canContainContent: false, - isEnginePlugin: true, - modules: [ - { - name: 'PhysicsRuntime', - type: 'runtime', - loadingPhase: 'default', - entry: './src/runtime.ts' - }, - { - name: 'PhysicsEditor', - type: 'editor', - loadingPhase: 'default', - entry: './src/editor/index.ts' - } - ], - dependencies: [ - { id: '@esengine/ecs-framework', version: '^2.0.0' }, - { id: '@esengine/ecs-components', version: '^1.0.0' } - ], - icon: 'Atom' -}; - -/** - * Physics 2D Plugin Loader - * 2D 物理插件加载器 - * - * - runtimeModule: 完整运行时模块(含 WASM 物理系统),支持编辑器预览和游戏运行 - * - editorModule: 编辑器功能模块(检视器、Gizmo、实体模板等) - */ -export const Physics2DPlugin: IPluginLoader = { - descriptor, - editorModule: new Physics2DEditorModule(), - runtimeModule: new PhysicsRuntimeModule(), -}; - -export default Physics2DPlugin; diff --git a/packages/physics-rapier2d/src/index.ts b/packages/physics-rapier2d/src/index.ts index 49760ef0..52d4bb4d 100644 --- a/packages/physics-rapier2d/src/index.ts +++ b/packages/physics-rapier2d/src/index.ts @@ -22,5 +22,5 @@ export * from './services'; // Systems (type only for editor usage) export type { Physics2DSystem } from './systems/Physics2DSystem'; -// Plugin (for editor, no WASM dependency) -export { Physics2DPlugin } from './editor/Physics2DPlugin'; +// Editor plugin (no WASM dependency) +export { Physics2DPlugin } from './PhysicsEditorPlugin'; diff --git a/packages/physics-rapier2d/src/runtime.ts b/packages/physics-rapier2d/src/runtime.ts index 9973d19b..1c226001 100644 --- a/packages/physics-rapier2d/src/runtime.ts +++ b/packages/physics-rapier2d/src/runtime.ts @@ -52,6 +52,5 @@ export { Physics2DSystem, type Physics2DSystemConfig } from './systems/Physics2D // Services export { Physics2DService } from './services/Physics2DService'; -// Runtime Module -export { PhysicsRuntimeModule } from './PhysicsRuntimeModule'; -export { default as physicsRuntimeModule } from './PhysicsRuntimeModule'; +// Runtime module and plugin +export { PhysicsRuntimeModule, PhysicsPlugin, type PhysicsSystemContext } from './PhysicsRuntimeModule'; diff --git a/packages/physics-rapier2d/src/runtime/index.ts b/packages/physics-rapier2d/src/runtime/index.ts index f5896dce..057c0f95 100644 --- a/packages/physics-rapier2d/src/runtime/index.ts +++ b/packages/physics-rapier2d/src/runtime/index.ts @@ -6,8 +6,8 @@ * Contains WASM dependencies, for actual runtime environment */ -// Re-export runtime module with WASM -export { PhysicsRuntimeModule, default as physicsRuntimeModule } from '../PhysicsRuntimeModule'; +// Re-export runtime module and plugin with WASM +export { PhysicsRuntimeModule, PhysicsPlugin, type PhysicsSystemContext } from '../PhysicsRuntimeModule'; // Re-export world and system (they have WASM type dependencies) export { Physics2DWorld } from '../world/Physics2DWorld'; diff --git a/packages/physics-rapier2d/src/systems/Physics2DSystem.ts b/packages/physics-rapier2d/src/systems/Physics2DSystem.ts index 7cce60a9..151c12cb 100644 --- a/packages/physics-rapier2d/src/systems/Physics2DSystem.ts +++ b/packages/physics-rapier2d/src/systems/Physics2DSystem.ts @@ -6,7 +6,7 @@ */ import { EntitySystem, Matcher, type Entity } from '@esengine/ecs-framework'; -import { TransformComponent } from '@esengine/ecs-components'; +import { TransformComponent } from '@esengine/engine-core'; import { Physics2DWorld } from '../world/Physics2DWorld'; import { Rigidbody2DComponent } from '../components/Rigidbody2DComponent'; import { Collider2DBase } from '../components/Collider2DBase'; @@ -66,9 +66,6 @@ export class Physics2DSystem extends EntitySystem { // 待处理的新实体队列 private _pendingEntities: Entity[] = []; - // Transform 组件类型(用于检查) - private _transformType = TransformComponent; - constructor(config?: Physics2DSystemConfig) { // 匹配所有拥有 Rigidbody2DComponent 的实体 super(Matcher.empty().all(Rigidbody2DComponent)); @@ -290,10 +287,13 @@ export class Physics2DSystem extends EntitySystem { */ private _createPhysicsBody(entity: Entity): void { const rigidbody = entity.getComponent(Rigidbody2DComponent); - const transform = entity.getComponent(this._transformType); + const transform = entity.getComponent(TransformComponent); if (!rigidbody || !transform) { - this.logger.warn(`Entity ${entity.name} missing required components for physics`); + const missing: string[] = []; + if (!rigidbody) missing.push('Rigidbody2DComponent'); + if (!transform) missing.push('TransformComponent'); + this.logger.warn(`Entity ${entity.name} missing required components: ${missing.join(', ')}`); return; } @@ -361,7 +361,7 @@ export class Physics2DSystem extends EntitySystem { private _syncTransformsToPhysics(entities: readonly Entity[]): void { for (const entity of entities) { const rigidbody = entity.getComponent(Rigidbody2DComponent); - const transform = entity.getComponent(this._transformType); + const transform = entity.getComponent(TransformComponent); const mapping = this._entityBodies.get(entity.id); if (!rigidbody || !transform || !mapping) continue; @@ -465,7 +465,7 @@ export class Physics2DSystem extends EntitySystem { private _syncPhysicsToTransforms(entities: readonly Entity[]): void { for (const entity of entities) { const rigidbody = entity.getComponent(Rigidbody2DComponent); - const transform = entity.getComponent(this._transformType); + const transform = entity.getComponent(TransformComponent); const mapping = this._entityBodies.get(entity.id); if (!rigidbody || !transform || !mapping) continue; diff --git a/packages/physics-rapier2d/tsconfig.build.json b/packages/physics-rapier2d/tsconfig.build.json new file mode 100644 index 00000000..ba0684d9 --- /dev/null +++ b/packages/physics-rapier2d/tsconfig.build.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ES2020", + "moduleResolution": "bundler", + "lib": ["ES2020", "DOM"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "declaration": true, + "declarationMap": true, + "jsx": "react-jsx", + "resolveJsonModule": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/physics-rapier2d/tsconfig.json b/packages/physics-rapier2d/tsconfig.json index 2d7897fe..a449a378 100644 --- a/packages/physics-rapier2d/tsconfig.json +++ b/packages/physics-rapier2d/tsconfig.json @@ -25,6 +25,6 @@ "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"], "references": [ { "path": "../core" }, - { "path": "../components" } + { "path": "../engine-core" } ] } diff --git a/packages/physics-rapier2d/tsup.config.ts b/packages/physics-rapier2d/tsup.config.ts new file mode 100644 index 00000000..aca96ccf --- /dev/null +++ b/packages/physics-rapier2d/tsup.config.ts @@ -0,0 +1,23 @@ +import { defineConfig } from 'tsup'; +import { STANDARD_EXTERNALS } from '../build-config/src/types'; + +// Physics-rapier2d keeps runtime entry for WASM loading +export default defineConfig({ + entry: { + index: 'src/index.ts', + runtime: 'src/runtime.ts' + }, + format: ['esm'], + dts: true, + sourcemap: true, + clean: true, + tsconfig: 'tsconfig.build.json', + // 使用标准外部依赖列表,确保所有 @esengine/* 包都被外部化 + // 这避免了类被重复打包导致 instanceof 检查失败的问题 + external: [ + ...STANDARD_EXTERNALS, + ], + esbuildOptions(options) { + options.jsx = 'automatic'; + } +}); diff --git a/packages/physics-rapier2d/vite.config.ts b/packages/physics-rapier2d/vite.config.ts deleted file mode 100644 index 9a0f8c08..00000000 --- a/packages/physics-rapier2d/vite.config.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { defineConfig } from 'vite'; -import { resolve } from 'path'; -import dts from 'vite-plugin-dts'; -import react from '@vitejs/plugin-react'; - -export default defineConfig({ - plugins: [ - react(), - dts({ - include: ['src'], - outDir: 'dist', - rollupTypes: false, - tsconfigPath: './tsconfig.json' - }) - ], - esbuild: { - jsx: 'automatic', - }, - build: { - lib: { - entry: { - index: resolve(__dirname, 'src/index.ts'), - runtime: resolve(__dirname, 'src/runtime.ts'), - 'editor/index': resolve(__dirname, 'src/editor/index.ts') - }, - formats: ['es'], - fileName: (format, entryName) => `${entryName}.js` - }, - rollupOptions: { - external: [ - '@esengine/ecs-framework', - '@esengine/ecs-components', - '@esengine/editor-core', - 'react', - 'react/jsx-runtime', - /^@esengine\// - ], - output: { - exports: 'named', - preserveModules: false - } - }, - target: 'es2020', - minify: false, - sourcemap: true - } -}); diff --git a/packages/platform-common/tsconfig.json b/packages/platform-common/tsconfig.json index ddda78d4..4416d783 100644 --- a/packages/platform-common/tsconfig.json +++ b/packages/platform-common/tsconfig.json @@ -17,7 +17,10 @@ "forceConsistentCasingInFileNames": true, "resolveJsonModule": true, "isolatedModules": true, - "noEmit": false + "noEmit": false, + "composite": true, + "incremental": true, + "tsBuildInfoFile": "./dist/.tsbuildinfo" }, "include": ["src/**/*"], "exclude": ["node_modules", "dist"] diff --git a/packages/platform-web/package.json b/packages/platform-web/package.json index b3134068..f799ecd7 100644 --- a/packages/platform-web/package.json +++ b/packages/platform-web/package.json @@ -37,18 +37,20 @@ ], "author": "yhh", "license": "MIT", - "peerDependencies": { - "@esengine/asset-system": "workspace:*", - "@esengine/behavior-tree": "workspace:*", - "@esengine/ecs-components": "workspace:*", - "@esengine/ecs-engine-bindgen": "workspace:*", - "@esengine/ecs-framework": "workspace:*", - "@esengine/physics-rapier2d": "workspace:*", - "@esengine/platform-common": "workspace:*", - "@esengine/tilemap": "workspace:*", - "@esengine/ui": "workspace:*" - }, "devDependencies": { + "@esengine/ecs-framework": "workspace:*", + "@esengine/asset-system": "workspace:*", + "@esengine/platform-common": "workspace:*", + "@esengine/audio": "workspace:*", + "@esengine/behavior-tree": "workspace:*", + "@esengine/camera": "workspace:*", + "@esengine/ecs-engine-bindgen": "workspace:*", + "@esengine/engine-core": "workspace:*", + "@esengine/physics-rapier2d": "workspace:*", + "@esengine/runtime-core": "workspace:*", + "@esengine/sprite": "workspace:*", + "@esengine/tilemap": "workspace:*", + "@esengine/ui": "workspace:*", "@rollup/plugin-alias": "^6.0.0", "@rollup/plugin-commonjs": "^28.0.3", "@rollup/plugin-node-resolve": "^16.0.1", diff --git a/packages/platform-web/rollup.runtime.config.js b/packages/platform-web/rollup.runtime.config.js index b5b3ab39..0188dfdd 100644 --- a/packages/platform-web/rollup.runtime.config.js +++ b/packages/platform-web/rollup.runtime.config.js @@ -17,7 +17,13 @@ export default { external: [ 'react', 'react-dom', - '@esengine/editor-core' + '@esengine/editor-core', + // Editor packages should never be imported in runtime + '@esengine/ui-editor', + '@esengine/tilemap-editor', + '@esengine/behavior-tree-editor', + '@esengine/blueprint-editor', + '@esengine/physics-rapier2d-editor' ], plugins: [ // Replace process.env.NODE_ENV for browser @@ -28,10 +34,7 @@ export default { resolve({ browser: true, preferBuiltins: false, - // Only resolve main/module fields, not source - mainFields: ['module', 'main'], - // Support package.json exports field for subpath imports - exportConditions: ['import', 'module', 'default'] + exportConditions: ['import', 'default'] }), commonjs(), typescript({ diff --git a/packages/platform-web/src/RuntimeSystems.ts b/packages/platform-web/src/RuntimeSystems.ts deleted file mode 100644 index 8ebf1452..00000000 --- a/packages/platform-web/src/RuntimeSystems.ts +++ /dev/null @@ -1,410 +0,0 @@ -/** - * Runtime Systems Configuration - * 运行时系统配置 - */ - -import { Core, ComponentRegistry, ServiceContainer } from '@esengine/ecs-framework'; -import type { IScene } from '@esengine/ecs-framework'; -import { EngineBridge, EngineRenderSystem, CameraSystem } from '@esengine/ecs-engine-bindgen'; -import { TransformComponent, SpriteAnimatorSystem, CoreRuntimeModule } from '@esengine/ecs-components'; -import type { SystemContext, IPluginLoader, IRuntimeModuleLoader, PluginDescriptor } from '@esengine/ecs-components'; -// Import runtime modules -// 注意:这些模块需要从各自包的 runtime 入口点导入,以避免编辑器依赖(React 等) -import { UIRuntimeModule, UIRenderDataProvider, UIInputSystem } from '@esengine/ui/runtime'; -import { TilemapRuntimeModule, TilemapRenderingSystem } from '@esengine/tilemap/runtime'; -import { BehaviorTreeRuntimeModule, BehaviorTreeExecutionSystem } from '@esengine/behavior-tree/runtime'; -import { PhysicsRuntimeModule, Physics2DSystem } from '@esengine/physics-rapier2d/runtime'; - -/** - * 运行时系统集合 - */ -export interface RuntimeSystems { - cameraSystem: CameraSystem; - animatorSystem?: SpriteAnimatorSystem; - tilemapSystem?: TilemapRenderingSystem; - behaviorTreeSystem?: BehaviorTreeExecutionSystem; - physicsSystem?: Physics2DSystem; - renderSystem: EngineRenderSystem; - uiRenderProvider?: UIRenderDataProvider; -} - -/** - * 运行时配置 - */ -export interface RuntimeModuleConfig { - /** 启用的插件 ID 列表,不指定则启用所有已注册插件 */ - enabledPlugins?: string[]; - /** 是否为编辑器模式 */ - isEditor?: boolean; - /** Canvas ID 用于 UI 输入绑定 */ - canvasId?: string; -} - -/** - * 运行时插件管理器(简化版,用于独立运行时) - * Runtime Plugin Manager (simplified, for standalone runtime) - */ -class RuntimePluginManager { - private plugins: Map = new Map(); - private enabledPlugins: Set = new Set(); - private initialized = false; - - /** - * 注册插件 - */ - register(plugin: IPluginLoader): void { - const id = plugin.descriptor.id; - if (this.plugins.has(id)) { - return; - } - this.plugins.set(id, plugin); - // 默认启用 - if (plugin.descriptor.enabledByDefault !== false) { - this.enabledPlugins.add(id); - } - } - - /** - * 启用插件 - */ - enable(pluginId: string): void { - this.enabledPlugins.add(pluginId); - } - - /** - * 禁用插件 - */ - disable(pluginId: string): void { - this.enabledPlugins.delete(pluginId); - } - - /** - * 加载配置 - */ - loadConfig(config: { enabledPlugins: string[] }): void { - this.enabledPlugins.clear(); - for (const id of config.enabledPlugins) { - this.enabledPlugins.add(id); - } - // 始终启用引擎插件 - for (const [id, plugin] of this.plugins) { - if (plugin.descriptor.isEnginePlugin) { - this.enabledPlugins.add(id); - } - } - } - - /** - * 初始化运行时(注册组件和服务) - */ - async initializeRuntime(services: ServiceContainer): Promise { - if (this.initialized) { - return; - } - - // 注册组件 - for (const [id, plugin] of this.plugins) { - if (!this.enabledPlugins.has(id)) { - continue; - } - const runtimeModule = plugin.runtimeModule; - if (runtimeModule) { - try { - runtimeModule.registerComponents(ComponentRegistry); - } catch (e) { - console.error(`Failed to register components for ${id}:`, e); - } - } - } - - // 注册服务 - for (const [id, plugin] of this.plugins) { - if (!this.enabledPlugins.has(id)) continue; - const runtimeModule = plugin.runtimeModule; - if (runtimeModule?.registerServices) { - try { - runtimeModule.registerServices(services); - } catch (e) { - console.error(`Failed to register services for ${id}:`, e); - } - } - } - - // 调用初始化回调 - for (const [id, plugin] of this.plugins) { - if (!this.enabledPlugins.has(id)) continue; - const runtimeModule = plugin.runtimeModule; - if (runtimeModule?.onInitialize) { - try { - await runtimeModule.onInitialize(); - } catch (e) { - console.error(`Failed to initialize ${id}:`, e); - } - } - } - - this.initialized = true; - } - - /** - * 为场景创建系统 - */ - createSystemsForScene(scene: IScene, context: SystemContext): void { - // Phase 1: 创建所有系统 - // Phase 1: Create all systems - for (const [id, plugin] of this.plugins) { - if (!this.enabledPlugins.has(id)) continue; - const runtimeModule = plugin.runtimeModule; - if (runtimeModule?.createSystems) { - try { - runtimeModule.createSystems(scene, context); - } catch (e) { - console.error(`Failed to create systems for ${id}:`, e); - } - } - } - - // Phase 2: 连接跨插件依赖 - // Phase 2: Wire cross-plugin dependencies - for (const [id, plugin] of this.plugins) { - if (!this.enabledPlugins.has(id)) continue; - const runtimeModule = plugin.runtimeModule; - if (runtimeModule?.onSystemsCreated) { - try { - runtimeModule.onSystemsCreated(scene, context); - } catch (e) { - console.error(`Failed to wire dependencies for ${id}:`, e); - } - } - } - } - - /** - * 获取所有已注册的插件 - */ - getPlugins(): IPluginLoader[] { - return Array.from(this.plugins.values()); - } - - /** - * 检查插件是否启用 - */ - isEnabled(pluginId: string): boolean { - return this.enabledPlugins.has(pluginId); - } - - /** - * 重置 - */ - reset(): void { - this.plugins.clear(); - this.enabledPlugins.clear(); - this.initialized = false; - } -} - -// 单例运行时插件管理器 -const runtimePluginManager = new RuntimePluginManager(); - -/** - * 创建运行时专用的插件加载器 - * Create runtime-only plugin loaders (without editor modules to avoid code splitting issues) - */ -function createRuntimeOnlyPlugin( - descriptor: PluginDescriptor, - runtimeModule: IRuntimeModuleLoader -): IPluginLoader { - return { - descriptor, - runtimeModule, - // No editor module for runtime builds - }; -} - -// 运行时专用插件描述符 | Runtime-only plugin descriptors -const coreDescriptor: PluginDescriptor = { - id: '@esengine/ecs-components', - name: 'Core Components', - version: '1.0.0', - category: 'core', - enabledByDefault: true, - isEnginePlugin: true, - modules: [{ name: 'CoreRuntime', type: 'runtime', entry: './src/index.ts' }] -}; - -const uiDescriptor: PluginDescriptor = { - id: '@esengine/ui', - name: 'UI System', - version: '1.0.0', - category: 'ui', - enabledByDefault: true, - isEnginePlugin: true, - modules: [{ name: 'UIRuntime', type: 'runtime', entry: './src/index.ts' }] -}; - -const tilemapDescriptor: PluginDescriptor = { - id: '@esengine/tilemap', - name: 'Tilemap System', - version: '1.0.0', - category: 'rendering', - enabledByDefault: true, - isEnginePlugin: true, - modules: [{ name: 'TilemapRuntime', type: 'runtime', entry: './src/index.ts' }] -}; - -const behaviorTreeDescriptor: PluginDescriptor = { - id: '@esengine/behavior-tree', - name: 'Behavior Tree', - version: '1.0.0', - category: 'ai', - enabledByDefault: true, - isEnginePlugin: true, - modules: [{ name: 'BehaviorTreeRuntime', type: 'runtime', entry: './src/index.ts' }] -}; - -const physicsDescriptor: PluginDescriptor = { - id: '@esengine/physics-rapier2d', - name: 'Rapier 2D Physics', - version: '1.0.0', - category: 'physics', - enabledByDefault: true, - isEnginePlugin: true, - modules: [{ name: 'PhysicsRuntime', type: 'runtime', entry: './src/runtime.ts' }] -}; - -/** - * 注册所有可用插件 - * 仅注册插件描述信息,不初始化组件和服务 - */ -export function registerAvailablePlugins(): void { - try { - runtimePluginManager.register(createRuntimeOnlyPlugin(coreDescriptor, new CoreRuntimeModule())); - } catch (e) { - console.error('[RuntimeSystems] Failed to register CoreRuntimeModule:', e); - } - - try { - runtimePluginManager.register(createRuntimeOnlyPlugin(uiDescriptor, new UIRuntimeModule())); - } catch (e) { - console.error('[RuntimeSystems] Failed to register UIRuntimeModule:', e); - } - - try { - runtimePluginManager.register(createRuntimeOnlyPlugin(tilemapDescriptor, new TilemapRuntimeModule())); - } catch (e) { - console.error('[RuntimeSystems] Failed to register TilemapRuntimeModule:', e); - } - - try { - runtimePluginManager.register(createRuntimeOnlyPlugin(behaviorTreeDescriptor, new BehaviorTreeRuntimeModule())); - } catch (e) { - console.error('[RuntimeSystems] Failed to register BehaviorTreeRuntimeModule:', e); - } - - try { - runtimePluginManager.register(createRuntimeOnlyPlugin(physicsDescriptor, new PhysicsRuntimeModule())); - } catch (e) { - console.error('[RuntimeSystems] Failed to register PhysicsRuntimeModule:', e); - } -} - -/** - * 初始化运行时(完整流程) - * 用于独立游戏运行时,一次性完成所有初始化 - */ -export async function initializeRuntime( - coreInstance: typeof Core, - config?: RuntimeModuleConfig -): Promise { - registerAvailablePlugins(); - - if (config?.enabledPlugins) { - runtimePluginManager.loadConfig({ enabledPlugins: config.enabledPlugins }); - } else { - // 默认启用所有插件 - for (const plugin of runtimePluginManager.getPlugins()) { - runtimePluginManager.enable(plugin.descriptor.id); - } - } - - await runtimePluginManager.initializeRuntime(coreInstance.services); -} - -/** - * 初始化插件(编辑器用) - * 根据项目配置初始化已启用的插件 - * - * @param coreInstance Core 实例 - * @param enabledPlugins 启用的插件 ID 列表 - */ -export async function initializePluginsForProject( - coreInstance: typeof Core, - enabledPlugins: string[] -): Promise { - // 确保插件已注册 - registerAvailablePlugins(); - - // 加载项目的插件配置 - runtimePluginManager.loadConfig({ enabledPlugins }); - - // 初始化插件(注册组件和服务) - await runtimePluginManager.initializeRuntime(coreInstance.services); -} - -/** - * 创建运行时系统 - */ -export function createRuntimeSystems( - scene: IScene, - bridge: EngineBridge, - config?: RuntimeModuleConfig -): RuntimeSystems { - const isEditor = config?.isEditor ?? false; - - const cameraSystem = new CameraSystem(bridge); - scene.addSystem(cameraSystem); - - const renderSystem = new EngineRenderSystem(bridge, TransformComponent); - - const context: SystemContext = { - core: Core, - engineBridge: bridge, - renderSystem, - isEditor - }; - - runtimePluginManager.createSystemsForScene(scene, context); - - // 注册 UI 渲染提供者到渲染系统 - // Register UI render provider to render system - if (context.uiRenderProvider) { - renderSystem.setUIRenderDataProvider(context.uiRenderProvider); - } - - // 独立运行时始终使用预览模式(屏幕空间 UI) - // Standalone runtime always uses preview mode (screen space UI) - if (!isEditor) { - renderSystem.setPreviewMode(true); - } - - scene.addSystem(renderSystem); - - // 绑定 UIInputSystem 到 canvas(用于 UI 交互) - // Bind UIInputSystem to canvas (for UI interaction) - if (config?.canvasId && context.uiInputSystem) { - const canvas = document.getElementById(config.canvasId) as HTMLCanvasElement; - if (canvas) { - (context.uiInputSystem as UIInputSystem).bindToCanvas(canvas); - } - } - - return { - cameraSystem, - animatorSystem: context.animatorSystem as SpriteAnimatorSystem | undefined, - tilemapSystem: context.tilemapSystem as TilemapRenderingSystem | undefined, - behaviorTreeSystem: context.behaviorTreeSystem as BehaviorTreeExecutionSystem | undefined, - renderSystem, - uiRenderProvider: context.uiRenderProvider as UIRenderDataProvider | undefined - }; -} - diff --git a/packages/platform-web/src/index.ts b/packages/platform-web/src/index.ts index 274c2ea6..83f8739f 100644 --- a/packages/platform-web/src/index.ts +++ b/packages/platform-web/src/index.ts @@ -1,28 +1,32 @@ /** - * Web/H5 平台适配器包 + * @esengine/platform-web + * + * Web/H5 平台适配器 - 仅包含平台差异代码 + * 通用运行时逻辑在 @esengine/runtime-core + * * @packageDocumentation */ -// 引擎桥接 -export { EngineBridge } from './EngineBridge'; -export type { EngineBridgeConfig } from './EngineBridge'; - -// 子系统 +// Web 平台子系统 export { WebCanvasSubsystem } from './subsystems/WebCanvasSubsystem'; export { WebInputSubsystem } from './subsystems/WebInputSubsystem'; export { WebStorageSubsystem } from './subsystems/WebStorageSubsystem'; export { WebWASMSubsystem } from './subsystems/WebWASMSubsystem'; -// 运行时系统配置 -export { - registerAvailablePlugins, - initializeRuntime, - initializePluginsForProject, - createRuntimeSystems -} from './RuntimeSystems'; -export type { RuntimeSystems, RuntimeModuleConfig } from './RuntimeSystems'; +// Web 特定系统 +export { Canvas2DRenderSystem } from './systems/Canvas2DRenderSystem'; -// 工具 export function isWebPlatform(): boolean { return typeof window !== 'undefined' && typeof document !== 'undefined'; } + +export function getWebCanvas(canvasId: string): HTMLCanvasElement | null { + return document.getElementById(canvasId) as HTMLCanvasElement | null; +} + +export function createWebCanvas(width: number, height: number): HTMLCanvasElement { + const canvas = document.createElement('canvas'); + canvas.width = width; + canvas.height = height; + return canvas; +} diff --git a/packages/platform-web/src/runtime.ts b/packages/platform-web/src/runtime.ts index 811d65b6..b3d162bc 100644 --- a/packages/platform-web/src/runtime.ts +++ b/packages/platform-web/src/runtime.ts @@ -1,123 +1,196 @@ /** * Browser Runtime Entry Point * 浏览器运行时入口 + * + * 使用统一的 GameRuntime 架构,静态导入所有插件 + * Uses the unified GameRuntime architecture with static plugin imports */ -import { Core, Scene, SceneSerializer } from '@esengine/ecs-framework'; -import { EngineBridge } from '@esengine/ecs-engine-bindgen'; -import { TransformComponent, SpriteComponent, SpriteAnimatorComponent, CameraComponent } from '@esengine/ecs-components'; -import { AssetManager, EngineIntegration } from '@esengine/asset-system'; -import { initializeRuntime, createRuntimeSystems, type RuntimeSystems } from './RuntimeSystems'; +import { Core } from '@esengine/ecs-framework'; +import { + GameRuntime, + createGameRuntime, + BrowserPlatformAdapter, + runtimePluginManager, + BrowserFileSystemService +} from '@esengine/runtime-core'; -interface RuntimeConfig { +// 静态导入所有运行时插件(与编辑器保持一致) +// Static import all runtime plugins (consistent with editor) +import { EnginePlugin } from '@esengine/engine-core'; +import { CameraPlugin } from '@esengine/camera'; +import { SpritePlugin } from '@esengine/sprite'; +import { AudioPlugin } from '@esengine/audio'; +import { UIPlugin } from '@esengine/ui'; +import { TilemapPlugin } from '@esengine/tilemap'; +import { BehaviorTreePlugin } from '@esengine/behavior-tree'; +// 使用 runtime 子路径导入,包含 WASM 依赖 +import { PhysicsPlugin } from '@esengine/physics-rapier2d/runtime'; + +// 预注册所有插件(在 GameRuntime 初始化前) +// Pre-register all plugins (before GameRuntime initialization) +const ALL_PLUGINS = [ + EnginePlugin, + CameraPlugin, + SpritePlugin, + AudioPlugin, + UIPlugin, + TilemapPlugin, + BehaviorTreePlugin, + PhysicsPlugin, +]; + +// 注册并启用所有插件(浏览器运行时默认启用所有功能) +for (const plugin of ALL_PLUGINS) { + if (plugin) { + runtimePluginManager.register(plugin); + // 确保所有插件都启用(覆盖 enabledByDefault: false) + runtimePluginManager.enable(plugin.descriptor.id); + } +} + +export interface RuntimeConfig { canvasId: string; width?: number; height?: number; + /** 项目配置文件 URL / Project config file URL */ + projectConfigUrl?: string; + /** 资产目录文件 URL / Asset catalog file URL */ + assetCatalogUrl?: string; + /** 资产基础 URL / Asset base URL */ + assetBaseUrl?: string; } +/** + * 编辑器项目配置文件格式 + * Editor project config file format (ecs-editor.config.json) + */ +interface EditorProjectConfig { + projectType?: string; + plugins?: { + enabledPlugins: string[]; + }; + [key: string]: any; +} + +/** + * Browser Runtime Wrapper + * 浏览器运行时包装器 + */ class BrowserRuntime { - private bridge: EngineBridge; - private systems: RuntimeSystems | null = null; - private animationId: number | null = null; - private assetManager: AssetManager; - private engineIntegration: EngineIntegration; - private canvasId: string; + private _runtime: GameRuntime | null = null; + private _canvasId: string; + private _configUrl?: string; + private _assetCatalogUrl?: string; + private _assetBaseUrl?: string; + private _fileSystem: BrowserFileSystemService | null = null; constructor(config: RuntimeConfig) { - this.canvasId = config.canvasId; - if (!Core.Instance) { - Core.create(); + this._canvasId = config.canvasId; + this._configUrl = config.projectConfigUrl; + this._assetCatalogUrl = config.assetCatalogUrl ?? '/asset-catalog.json'; + this._assetBaseUrl = config.assetBaseUrl ?? '/assets'; + } + + /** + * 从配置文件 URL 加载插件配置 + */ + private async _loadConfigFromUrl(): Promise { + if (!this._configUrl) return; + + try { + const response = await fetch(this._configUrl); + if (!response.ok) { + console.warn(`[BrowserRuntime] Failed to load config from ${this._configUrl}: ${response.status}`); + return; + } + + const editorConfig: EditorProjectConfig = await response.json(); + + // 如果有插件配置,应用到 runtimePluginManager + if (editorConfig.plugins?.enabledPlugins) { + runtimePluginManager.loadConfig({ enabledPlugins: editorConfig.plugins.enabledPlugins }); + console.log('[BrowserRuntime] Loaded plugin config:', editorConfig.plugins.enabledPlugins); + } + } catch (error) { + console.warn('[BrowserRuntime] Error loading config file:', error); } - - if (!Core.scene) { - const runtimeScene = new Scene({ name: 'Runtime Scene' }); - Core.setScene(runtimeScene); - } - - this.bridge = new EngineBridge({ - canvasId: config.canvasId, - width: config.width || window.innerWidth, - height: config.height || window.innerHeight - }); - - this.assetManager = new AssetManager(); - this.engineIntegration = new EngineIntegration(this.assetManager, this.bridge); } async initialize(wasmModule: any): Promise { - await this.bridge.initializeWithModule(wasmModule); + // 从配置文件加载插件配置(如果指定了 URL) + await this._loadConfigFromUrl(); - this.bridge.setPathResolver((path: string) => { - if (path.startsWith('http://') || path.startsWith('https://') || path.startsWith('/asset?')) { - return path; - } - return `/asset?path=${encodeURIComponent(path)}`; + // 初始化浏览器文件系统服务(用于资产加载) + // Initialize browser file system service (for asset loading) + this._fileSystem = new BrowserFileSystemService({ + baseUrl: this._assetBaseUrl, + catalogUrl: this._assetCatalogUrl, + enableCache: true + }); + await this._fileSystem.initialize(); + + // 创建浏览器平台适配器 + const platform = new BrowserPlatformAdapter({ + wasmModule: wasmModule }); - this.bridge.setShowGrid(false); - this.bridge.setShowGizmos(false); - - // 初始化模块系统 - await initializeRuntime(Core); - - // 创建运行时系统(传入 canvasId 用于 UI 输入绑定) - this.systems = createRuntimeSystems(Core.scene!, this.bridge, { - canvasId: this.canvasId + // 创建统一运行时 + // 插件已经预注册了,GameRuntime 会检测到并跳过动态加载 + this._runtime = createGameRuntime({ + platform, + canvasId: this._canvasId, + width: window.innerWidth, + height: window.innerHeight, + autoStartRenderLoop: false }); + + await this._runtime.initialize(); + + // 注册文件系统服务到 Core.services(必须在 GameRuntime.initialize 之后,因为 Core 在那时才创建) + // Register file system service to Core.services (must be after GameRuntime.initialize as Core is created there) + const IFileSystemServiceKey = Symbol.for('IFileSystemService'); + if (!Core.services.isRegistered(IFileSystemServiceKey)) { + Core.services.registerInstance(IFileSystemServiceKey, this._fileSystem); + } + + // 设置浏览器特定配置 + this._runtime.setShowGrid(false); + this._runtime.setShowGizmos(false); } async loadScene(sceneUrl: string): Promise { - const response = await fetch(sceneUrl); - const sceneJson = await response.text(); - - if (!Core.scene) { - throw new Error('Core.scene not initialized'); + if (!this._runtime) { + throw new Error('Runtime not initialized'); } - - SceneSerializer.deserialize(Core.scene, sceneJson, { - strategy: 'replace', - preserveIds: true - }); + await this._runtime.loadSceneFromUrl(sceneUrl); } start(): void { - if (this.animationId !== null) return; - - let lastTime = performance.now(); - const loop = () => { - const currentTime = performance.now(); - const deltaTime = (currentTime - lastTime) / 1000; - lastTime = currentTime; - - Core.update(deltaTime); - - this.animationId = requestAnimationFrame(loop); - }; - - loop(); + if (!this._runtime) return; + this._runtime.start(); } stop(): void { - if (this.animationId !== null) { - cancelAnimationFrame(this.animationId); - this.animationId = null; - } + if (!this._runtime) return; + this._runtime.stop(); } handleResize(width: number, height: number): void { - this.bridge.resize(width, height); + if (!this._runtime) return; + this._runtime.resize(width, height); } - getAssetManager(): AssetManager { - return this.assetManager; + get assetManager() { + return this._runtime?.assetManager ?? null; } - getEngineIntegration(): EngineIntegration { - return this.engineIntegration; + get engineIntegration() { + return this._runtime?.engineIntegration ?? null; } - getSystems(): RuntimeSystems | null { - return this.systems; + get gameRuntime(): GameRuntime | null { + return this._runtime; } } @@ -125,8 +198,7 @@ export default { create: (config: RuntimeConfig) => new BrowserRuntime(config), BrowserRuntime, Core, - TransformComponent, - SpriteComponent, - SpriteAnimatorComponent, - CameraComponent + GameRuntime, + createGameRuntime, + BrowserPlatformAdapter }; diff --git a/packages/platform-web/src/systems/Canvas2DRenderSystem.ts b/packages/platform-web/src/systems/Canvas2DRenderSystem.ts index 41689238..e3def8db 100644 --- a/packages/platform-web/src/systems/Canvas2DRenderSystem.ts +++ b/packages/platform-web/src/systems/Canvas2DRenderSystem.ts @@ -4,7 +4,8 @@ */ import { EntitySystem, Matcher, ECSSystem, Core } from '@esengine/ecs-framework'; -import { TransformComponent, SpriteComponent } from '@esengine/ecs-components'; +import { TransformComponent } from '@esengine/engine-core'; +import { SpriteComponent } from '@esengine/sprite'; @ECSSystem('Canvas2DRender', { updateOrder: 1000 }) export class Canvas2DRenderSystem extends EntitySystem { @@ -54,11 +55,12 @@ export class Canvas2DRenderSystem extends EntitySystem { this.ctx.save(); - const x = (transform.position.x || 0) + this.canvas.width / 2; - const y = this.canvas.height / 2 - (transform.position.y || 0); - const width = (sprite.width || 64) * (transform.scale.x || 1); - const height = (sprite.height || 64) * (transform.scale.y || 1); - const rotation = -(transform.rotation.z || 0) * Math.PI / 180; + // 使用世界变换(由 TransformSystem 计算,考虑父级变换) + const x = (transform.worldPosition.x || 0) + this.canvas.width / 2; + const y = this.canvas.height / 2 - (transform.worldPosition.y || 0); + const width = (sprite.width || 64) * (transform.worldScale.x || 1); + const height = (sprite.height || 64) * (transform.worldScale.y || 1); + const rotation = -(transform.worldRotation.z || 0) * Math.PI / 180; this.ctx.translate(x, y); this.ctx.rotate(rotation); diff --git a/packages/platform-web/tsconfig.json b/packages/platform-web/tsconfig.json index b90bddc2..1f5f66b7 100644 --- a/packages/platform-web/tsconfig.json +++ b/packages/platform-web/tsconfig.json @@ -20,5 +20,8 @@ "noEmit": false }, "include": ["src/**/*"], - "exclude": ["node_modules", "dist"] + "exclude": ["node_modules", "dist"], + "references": [ + { "path": "../platform-common" } + ] } diff --git a/packages/platform-wechat/package.json b/packages/platform-wechat/package.json index 3c358d5c..1dbf8289 100644 --- a/packages/platform-wechat/package.json +++ b/packages/platform-wechat/package.json @@ -31,7 +31,7 @@ ], "author": "yhh", "license": "MIT", - "peerDependencies": { + "dependencies": { "@esengine/ecs-framework": "workspace:*", "@esengine/platform-common": "workspace:*" }, diff --git a/packages/runtime-core/package.json b/packages/runtime-core/package.json new file mode 100644 index 00000000..cc43ea9d --- /dev/null +++ b/packages/runtime-core/package.json @@ -0,0 +1,41 @@ +{ + "name": "@esengine/runtime-core", + "version": "1.0.0", + "description": "Runtime core - plugin management and system initialization", + "main": "dist/index.js", + "module": "dist/index.js", + "types": "dist/index.d.ts", + "type": "module", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "files": [ + "dist" + ], + "scripts": { + "build": "tsup", + "build:watch": "tsup --watch", + "type-check": "tsc --noEmit", + "clean": "rimraf dist" + }, + "devDependencies": { + "@esengine/ecs-framework": "workspace:*", + "@esengine/ecs-engine-bindgen": "workspace:*", + "@esengine/engine-core": "workspace:*", + "@esengine/asset-system": "workspace:*", + "@esengine/build-config": "workspace:*", + "rimraf": "^5.0.5", + "tsup": "^8.0.0", + "typescript": "^5.3.3" + }, + "keywords": [ + "ecs", + "runtime", + "plugin" + ], + "author": "yhh", + "license": "MIT" +} diff --git a/packages/runtime-core/src/GameRuntime.ts b/packages/runtime-core/src/GameRuntime.ts new file mode 100644 index 00000000..ce1b5e9a --- /dev/null +++ b/packages/runtime-core/src/GameRuntime.ts @@ -0,0 +1,824 @@ +/** + * Unified Game Runtime + * 统一游戏运行时 + * + * 这是编辑器预览和独立运行的统一入口点 + * This is the unified entry point for editor preview and standalone runtime + */ + +import { Core, Scene, SceneSerializer, HierarchySystem } from '@esengine/ecs-framework'; +import { EngineBridge, EngineRenderSystem, CameraSystem } from '@esengine/ecs-engine-bindgen'; +import { TransformComponent, TransformSystem } from '@esengine/engine-core'; +import { AssetManager, EngineIntegration } from '@esengine/asset-system'; +import { + runtimePluginManager, + type SystemContext, + type IRuntimeModule +} from './PluginManager'; +import { + loadEnabledPlugins, + type PluginPackageInfo, + type ProjectPluginConfig +} from './PluginLoader'; +import { + BUILTIN_PLUGIN_PACKAGES, + mergeProjectConfig, + type ProjectConfig +} from './ProjectConfig'; +import type { IPlatformAdapter, PlatformAdapterConfig } from './IPlatformAdapter'; + +/** + * 运行时配置 + * Runtime configuration + */ +export interface GameRuntimeConfig { + /** 平台适配器 */ + platform: IPlatformAdapter; + /** 项目配置 */ + projectConfig?: Partial; + /** Canvas ID */ + canvasId: string; + /** 初始宽度 */ + width?: number; + /** 初始高度 */ + height?: number; + /** 是否自动启动渲染循环 */ + autoStartRenderLoop?: boolean; + /** UI 画布尺寸 */ + uiCanvasSize?: { width: number; height: number }; + /** + * 跳过内部插件加载 + * 编辑器模式下,插件由 editor-core 的 PluginManager 管理 + * Skip internal plugin loading - editor mode uses editor-core's PluginManager + */ + skipPluginLoading?: boolean; +} + +/** + * 运行时状态 + * Runtime state + */ +export interface RuntimeState { + initialized: boolean; + running: boolean; + paused: boolean; +} + +/** + * 统一游戏运行时 + * Unified Game Runtime + * + * 提供编辑器预览和独立运行的统一实现 + * Provides unified implementation for editor preview and standalone runtime + */ +export class GameRuntime { + private _platform: IPlatformAdapter; + private _bridge: EngineBridge | null = null; + private _scene: Scene | null = null; + private _renderSystem: EngineRenderSystem | null = null; + private _cameraSystem: CameraSystem | null = null; + private _assetManager: AssetManager | null = null; + private _engineIntegration: EngineIntegration | null = null; + private _projectConfig: ProjectConfig; + private _config: GameRuntimeConfig; + + private _state: RuntimeState = { + initialized: false, + running: false, + paused: false + }; + + private _animationFrameId: number | null = null; + private _lastTime = 0; + + // 系统上下文,供插件使用 + private _systemContext: SystemContext | null = null; + + // 场景快照(用于编辑器预览后恢复) + private _sceneSnapshot: string | null = null; + + // Gizmo 注册表注入函数 + private _gizmoDataProvider?: (component: any, entity: any, isSelected: boolean) => any; + private _hasGizmoProvider?: (component: any) => boolean; + + constructor(config: GameRuntimeConfig) { + this._config = config; + this._platform = config.platform; + this._projectConfig = mergeProjectConfig(config.projectConfig || {}); + } + + /** + * 获取运行时状态 + */ + get state(): RuntimeState { + return { ...this._state }; + } + + /** + * 获取场景 + */ + get scene(): Scene | null { + return this._scene; + } + + /** + * 获取引擎桥接 + */ + get bridge(): EngineBridge | null { + return this._bridge; + } + + /** + * 获取渲染系统 + */ + get renderSystem(): EngineRenderSystem | null { + return this._renderSystem; + } + + /** + * 获取资产管理器 + */ + get assetManager(): AssetManager | null { + return this._assetManager; + } + + /** + * 获取引擎集成 + */ + get engineIntegration(): EngineIntegration | null { + return this._engineIntegration; + } + + /** + * 获取系统上下文 + */ + get systemContext(): SystemContext | null { + return this._systemContext; + } + + /** + * 更新系统上下文(用于编辑器模式下同步外部创建的系统引用) + * Update system context (for syncing externally created system references in editor mode) + */ + updateSystemContext(updates: Partial): void { + if (this._systemContext) { + Object.assign(this._systemContext, updates); + } + } + + /** + * 获取平台适配器 + */ + get platform(): IPlatformAdapter { + return this._platform; + } + + /** + * 初始化运行时 + * Initialize runtime + */ + async initialize(): Promise { + if (this._state.initialized) { + return; + } + + try { + // 1. 初始化平台 + await this._platform.initialize({ + canvasId: this._config.canvasId, + width: this._config.width, + height: this._config.height, + isEditor: this._platform.isEditorMode() + }); + + // 2. 获取 WASM 模块并创建引擎桥接 + const wasmModule = await this._platform.getWasmModule(); + this._bridge = new EngineBridge({ + canvasId: this._config.canvasId, + width: this._config.width, + height: this._config.height + }); + await this._bridge.initializeWithModule(wasmModule); + + // 3. 设置路径解析器 + this._bridge.setPathResolver((path: string) => { + return this._platform.pathResolver.resolve(path); + }); + + // 4. 初始化 ECS Core + if (!Core.Instance) { + Core.create({ debug: false }); + } + + // 5. 创建或获取场景 + if (Core.scene) { + this._scene = Core.scene as Scene; + } else { + this._scene = new Scene({ name: 'GameScene' }); + Core.setScene(this._scene); + } + + // 6. 添加基础系统 + this._scene.addSystem(new HierarchySystem()); + this._scene.addSystem(new TransformSystem()); + + this._cameraSystem = new CameraSystem(this._bridge); + this._scene.addSystem(this._cameraSystem); + + this._renderSystem = new EngineRenderSystem(this._bridge, TransformComponent); + + // 7. 设置 UI 画布尺寸 + if (this._config.uiCanvasSize) { + this._renderSystem.setUICanvasSize( + this._config.uiCanvasSize.width, + this._config.uiCanvasSize.height + ); + } else { + this._renderSystem.setUICanvasSize(1920, 1080); + } + + // 8. 创建资产系统 + this._assetManager = new AssetManager(); + this._engineIntegration = new EngineIntegration(this._assetManager, this._bridge); + + // 9. 加载并初始化插件(编辑器模式下跳过,由 editor-core 的 PluginManager 处理) + if (!this._config.skipPluginLoading) { + await this._initializePlugins(); + } + + // 10. 创建系统上下文 + this._systemContext = { + isEditor: this._platform.isEditorMode(), + engineBridge: this._bridge, + renderSystem: this._renderSystem, + assetManager: this._assetManager + }; + + // 11. 让插件创建系统(编辑器模式下跳过,由 EngineService.initializeModuleSystems 处理) + if (!this._config.skipPluginLoading) { + runtimePluginManager.createSystemsForScene(this._scene, this._systemContext); + } + + // 11. 设置 UI 渲染数据提供者(如果有) + if (this._systemContext.uiRenderProvider) { + this._renderSystem.setUIRenderDataProvider(this._systemContext.uiRenderProvider); + } + + // 12. 添加渲染系统(在所有其他系统之后) + this._scene.addSystem(this._renderSystem); + + // 13. 启动默认 world + const defaultWorld = Core.worldManager.getWorld('__default__'); + if (defaultWorld && !defaultWorld.isActive) { + defaultWorld.start(); + } + + // 14. 编辑器模式下的特殊处理 + if (this._platform.isEditorMode()) { + // 禁用游戏逻辑系统 + this._disableGameLogicSystems(); + } + + this._state.initialized = true; + + // 15. 自动启动渲染循环 + if (this._config.autoStartRenderLoop !== false) { + this._startRenderLoop(); + } + } catch (error) { + console.error('[GameRuntime] Initialization failed:', error); + throw error; + } + } + + /** + * 加载并初始化插件 + */ + private async _initializePlugins(): Promise { + // 检查是否已有插件注册(静态导入场景) + // Check if plugins are already registered (static import scenario) + const hasPlugins = runtimePluginManager.getPlugins().length > 0; + + if (!hasPlugins) { + // 没有预注册的插件,尝试动态加载 + // No pre-registered plugins, try dynamic loading + await loadEnabledPlugins( + { plugins: this._projectConfig.plugins }, + BUILTIN_PLUGIN_PACKAGES + ); + } + + // 初始化插件(注册组件和服务) + await runtimePluginManager.initializeRuntime(Core.services); + } + + /** + * 禁用游戏逻辑系统(编辑器模式) + */ + private _disableGameLogicSystems(): void { + const ctx = this._systemContext; + if (!ctx) return; + + // 这些系统由插件创建,通过 context 传递引用 + if (ctx.animatorSystem) { + ctx.animatorSystem.enabled = false; + } + if (ctx.behaviorTreeSystem) { + ctx.behaviorTreeSystem.enabled = false; + } + if (ctx.physicsSystem) { + ctx.physicsSystem.enabled = false; + } + } + + /** + * 启用游戏逻辑系统(预览/运行模式) + */ + private _enableGameLogicSystems(): void { + const ctx = this._systemContext; + if (!ctx) return; + + if (ctx.animatorSystem) { + ctx.animatorSystem.enabled = true; + } + if (ctx.behaviorTreeSystem) { + ctx.behaviorTreeSystem.enabled = true; + ctx.behaviorTreeSystem.startAllAutoStartTrees?.(); + } + if (ctx.physicsSystem) { + ctx.physicsSystem.enabled = true; + } + } + + /** + * 启动渲染循环 + */ + private _startRenderLoop(): void { + if (this._animationFrameId !== null) { + return; + } + this._lastTime = performance.now(); + this._renderLoop(); + } + + /** + * 渲染循环 + */ + private _renderLoop = (): void => { + const currentTime = performance.now(); + const deltaTime = (currentTime - this._lastTime) / 1000; + this._lastTime = currentTime; + + // 更新 ECS + Core.update(deltaTime); + + this._animationFrameId = requestAnimationFrame(this._renderLoop); + }; + + /** + * 停止渲染循环 + */ + private _stopRenderLoop(): void { + if (this._animationFrameId !== null) { + cancelAnimationFrame(this._animationFrameId); + this._animationFrameId = null; + } + } + + /** + * 开始运行(启用游戏逻辑) + * Start running (enable game logic) + */ + start(): void { + if (!this._state.initialized || this._state.running) { + return; + } + + this._state.running = true; + this._state.paused = false; + + // 启用预览模式 + if (this._renderSystem) { + this._renderSystem.setPreviewMode(true); + } + + // 启用游戏逻辑系统 + this._enableGameLogicSystems(); + + // 绑定 UI 输入 + const ctx = this._systemContext; + if (ctx?.uiInputSystem && this._config.canvasId) { + const canvas = document.getElementById(this._config.canvasId) as HTMLCanvasElement; + if (canvas) { + ctx.uiInputSystem.bindToCanvas(canvas); + } + } + + // 确保渲染循环在运行 + this._startRenderLoop(); + } + + /** + * 暂停运行 + * Pause running + */ + pause(): void { + if (!this._state.running || this._state.paused) { + return; + } + this._state.paused = true; + } + + /** + * 恢复运行 + * Resume running + */ + resume(): void { + if (!this._state.running || !this._state.paused) { + return; + } + this._state.paused = false; + } + + /** + * 停止运行(禁用游戏逻辑) + * Stop running (disable game logic) + */ + stop(): void { + if (!this._state.running) { + return; + } + + this._state.running = false; + this._state.paused = false; + + // 禁用预览模式 + if (this._renderSystem) { + this._renderSystem.setPreviewMode(false); + } + + // 解绑 UI 输入 + const ctx = this._systemContext; + if (ctx?.uiInputSystem) { + ctx.uiInputSystem.unbind?.(); + } + + // 禁用游戏逻辑系统 + this._disableGameLogicSystems(); + + // 重置物理系统 + if (ctx?.physicsSystem) { + ctx.physicsSystem.reset?.(); + } + } + + /** + * 单步执行 + * Step forward one frame + */ + step(): void { + if (!this._state.initialized) { + return; + } + + // 启用系统执行一帧 + this._enableGameLogicSystems(); + Core.update(1 / 60); + this._disableGameLogicSystems(); + } + + /** + * 加载场景数据 + * Load scene data + */ + async loadScene(sceneData: string | object): Promise { + if (!this._scene) { + throw new Error('Scene not initialized'); + } + + const jsonStr = typeof sceneData === 'string' + ? sceneData + : JSON.stringify(sceneData); + + SceneSerializer.deserialize(this._scene, jsonStr, { + strategy: 'replace', + preserveIds: true + }); + } + + /** + * 从 URL 加载场景 + * Load scene from URL + */ + async loadSceneFromUrl(url: string): Promise { + const response = await fetch(url); + if (!response.ok) { + throw new Error(`Failed to load scene from ${url}: ${response.status}`); + } + const sceneJson = await response.text(); + await this.loadScene(sceneJson); + } + + /** + * 调整视口大小 + * Resize viewport + */ + resize(width: number, height: number): void { + if (this._bridge) { + this._bridge.resize(width, height); + } + this._platform.resize(width, height); + } + + /** + * 设置相机 + * Set camera + */ + setCamera(config: { x: number; y: number; zoom: number; rotation?: number }): void { + if (this._bridge) { + this._bridge.setCamera({ + x: config.x, + y: config.y, + zoom: config.zoom, + rotation: config.rotation ?? 0 + }); + } + } + + /** + * 获取相机状态 + * Get camera state + */ + getCamera(): { x: number; y: number; zoom: number; rotation: number } { + if (this._bridge) { + return this._bridge.getCamera(); + } + return { x: 0, y: 0, zoom: 1, rotation: 0 }; + } + + /** + * 设置网格显示 + * Set grid visibility + */ + setShowGrid(show: boolean): void { + if (this._bridge) { + this._bridge.setShowGrid(show); + } + } + + /** + * 设置 Gizmo 显示 + * Set gizmo visibility + */ + setShowGizmos(show: boolean): void { + if (this._renderSystem) { + this._renderSystem.setShowGizmos(show); + } + } + + /** + * 设置清除颜色 + * Set clear color + */ + setClearColor(r: number, g: number, b: number, a: number = 1.0): void { + if (this._bridge) { + this._bridge.setClearColor(r, g, b, a); + } + } + + /** + * 获取统计信息 + * Get stats + */ + getStats(): { fps: number; drawCalls: number; spriteCount: number } { + if (!this._renderSystem) { + return { fps: 0, drawCalls: 0, spriteCount: 0 }; + } + + const engineStats = this._renderSystem.getStats(); + return { + fps: engineStats?.fps ?? 0, + drawCalls: engineStats?.drawCalls ?? 0, + spriteCount: this._renderSystem.spriteCount + }; + } + + // ===== 编辑器特有功能 ===== + // ===== Editor-specific features ===== + + /** + * 设置 Gizmo 注册表(编辑器模式) + * Set gizmo registry (editor mode) + */ + setGizmoRegistry( + gizmoDataProvider: (component: any, entity: any, isSelected: boolean) => any, + hasGizmoProvider: (component: any) => boolean + ): void { + this._gizmoDataProvider = gizmoDataProvider; + this._hasGizmoProvider = hasGizmoProvider; + + if (this._renderSystem) { + this._renderSystem.setGizmoRegistry(gizmoDataProvider, hasGizmoProvider); + } + } + + /** + * 设置选中的实体 ID(编辑器模式) + * Set selected entity IDs (editor mode) + */ + setSelectedEntityIds(ids: number[]): void { + if (this._renderSystem) { + this._renderSystem.setSelectedEntityIds(ids); + } + } + + /** + * 设置变换工具模式(编辑器模式) + * Set transform tool mode (editor mode) + */ + setTransformMode(mode: 'select' | 'move' | 'rotate' | 'scale'): void { + if (this._renderSystem) { + this._renderSystem.setTransformMode(mode); + } + } + + /** + * 获取变换工具模式 + * Get transform tool mode + */ + getTransformMode(): 'select' | 'move' | 'rotate' | 'scale' { + return this._renderSystem?.getTransformMode() ?? 'select'; + } + + /** + * 设置 UI 画布尺寸 + * Set UI canvas size + */ + setUICanvasSize(width: number, height: number): void { + if (this._renderSystem) { + this._renderSystem.setUICanvasSize(width, height); + } + } + + /** + * 获取 UI 画布尺寸 + * Get UI canvas size + */ + getUICanvasSize(): { width: number; height: number } { + return this._renderSystem?.getUICanvasSize() ?? { width: 0, height: 0 }; + } + + /** + * 设置 UI 画布边界显示 + * Set UI canvas boundary visibility + */ + setShowUICanvasBoundary(show: boolean): void { + if (this._renderSystem) { + this._renderSystem.setShowUICanvasBoundary(show); + } + } + + /** + * 获取 UI 画布边界显示状态 + * Get UI canvas boundary visibility + */ + getShowUICanvasBoundary(): boolean { + return this._renderSystem?.getShowUICanvasBoundary() ?? true; + } + + // ===== 场景快照 API ===== + // ===== Scene Snapshot API ===== + + /** + * 保存场景快照 + * Save scene snapshot + */ + saveSceneSnapshot(): boolean { + if (!this._scene) { + console.warn('[GameRuntime] Cannot save snapshot: no scene'); + return false; + } + + try { + this._sceneSnapshot = SceneSerializer.serialize(this._scene, { + format: 'json', + pretty: false, + includeMetadata: false + }) as string; + return true; + } catch (error) { + console.error('[GameRuntime] Failed to save snapshot:', error); + return false; + } + } + + /** + * 恢复场景快照 + * Restore scene snapshot + */ + async restoreSceneSnapshot(): Promise { + if (!this._scene || !this._sceneSnapshot) { + console.warn('[GameRuntime] Cannot restore: no scene or snapshot'); + return false; + } + + try { + // 清除缓存 + const ctx = this._systemContext; + if (ctx?.tilemapSystem) { + ctx.tilemapSystem.clearCache?.(); + } + + // 反序列化场景 + SceneSerializer.deserialize(this._scene, this._sceneSnapshot, { + strategy: 'replace', + preserveIds: true + }); + + this._sceneSnapshot = null; + return true; + } catch (error) { + console.error('[GameRuntime] Failed to restore snapshot:', error); + return false; + } + } + + /** + * 检查是否有快照 + * Check if snapshot exists + */ + hasSnapshot(): boolean { + return this._sceneSnapshot !== null; + } + + // ===== 多视口 API ===== + // ===== Multi-viewport API ===== + + /** + * 注册视口 + * Register viewport + */ + registerViewport(id: string, canvasId: string): void { + if (this._bridge) { + this._bridge.registerViewport(id, canvasId); + } + } + + /** + * 注销视口 + * Unregister viewport + */ + unregisterViewport(id: string): void { + if (this._bridge) { + this._bridge.unregisterViewport(id); + } + } + + /** + * 设置活动视口 + * Set active viewport + */ + setActiveViewport(id: string): boolean { + if (this._bridge) { + return this._bridge.setActiveViewport(id); + } + return false; + } + + /** + * 释放资源 + * Dispose resources + */ + dispose(): void { + this.stop(); + this._stopRenderLoop(); + + if (this._assetManager) { + this._assetManager.dispose(); + this._assetManager = null; + } + + this._engineIntegration = null; + this._scene = null; + + if (this._bridge) { + this._bridge.dispose(); + this._bridge = null; + } + + this._renderSystem = null; + this._cameraSystem = null; + this._systemContext = null; + this._platform.dispose(); + + this._state.initialized = false; + } +} + +/** + * 创建游戏运行时实例 + * Create game runtime instance + */ +export function createGameRuntime(config: GameRuntimeConfig): GameRuntime { + return new GameRuntime(config); +} diff --git a/packages/runtime-core/src/IPlatformAdapter.ts b/packages/runtime-core/src/IPlatformAdapter.ts new file mode 100644 index 00000000..739a2231 --- /dev/null +++ b/packages/runtime-core/src/IPlatformAdapter.ts @@ -0,0 +1,152 @@ +/** + * Platform Adapter Interface + * 平台适配器接口 + * + * 定义不同平台(编辑器、浏览器、原生)需要实现的适配器接口 + * Defines the adapter interface that different platforms need to implement + */ + +/** + * 资源路径解析器 + * Asset path resolver + */ +export interface IPathResolver { + /** + * 解析资源路径为可加载的 URL + * Resolve asset path to a loadable URL + */ + resolve(path: string): string; +} + +/** + * 平台能力标识 + * Platform capability flags + */ +export interface PlatformCapabilities { + /** 是否支持文件系统访问 / Supports file system access */ + fileSystem: boolean; + /** 是否支持热重载 / Supports hot reload */ + hotReload: boolean; + /** 是否支持 Gizmo 显示 / Supports gizmo display */ + gizmos: boolean; + /** 是否支持网格显示 / Supports grid display */ + grid: boolean; + /** 是否支持场景编辑 / Supports scene editing */ + sceneEditing: boolean; +} + +/** + * 平台适配器配置 + * Platform adapter configuration + */ +export interface PlatformAdapterConfig { + /** Canvas 元素 ID */ + canvasId: string; + /** 初始宽度 */ + width?: number; + /** 初始高度 */ + height?: number; + /** 是否为编辑器模式 */ + isEditor?: boolean; +} + +/** + * 平台适配器接口 + * Platform adapter interface + * + * 不同平台通过实现此接口来提供平台特定的功能 + * Different platforms implement this interface to provide platform-specific functionality + */ +export interface IPlatformAdapter { + /** + * 平台名称 + * Platform name + */ + readonly name: string; + + /** + * 平台能力 + * Platform capabilities + */ + readonly capabilities: PlatformCapabilities; + + /** + * 路径解析器 + * Path resolver + */ + readonly pathResolver: IPathResolver; + + /** + * 初始化平台 + * Initialize platform + */ + initialize(config: PlatformAdapterConfig): Promise; + + /** + * 获取 WASM 模块 + * Get WASM module + * + * 不同平台可能以不同方式加载 WASM + * Different platforms may load WASM in different ways + */ + getWasmModule(): Promise; + + /** + * 获取 Canvas 元素 + * Get canvas element + */ + getCanvas(): HTMLCanvasElement | null; + + /** + * 调整视口大小 + * Resize viewport + */ + resize(width: number, height: number): void; + + /** + * 获取当前视口尺寸 + * Get current viewport size + */ + getViewportSize(): { width: number; height: number }; + + /** + * 是否为编辑器模式 + * Whether in editor mode + */ + isEditorMode(): boolean; + + /** + * 设置是否显示网格(仅编辑器模式有效) + * Set whether to show grid (only effective in editor mode) + */ + setShowGrid?(show: boolean): void; + + /** + * 设置是否显示 Gizmos(仅编辑器模式有效) + * Set whether to show gizmos (only effective in editor mode) + */ + setShowGizmos?(show: boolean): void; + + /** + * 释放资源 + * Dispose resources + */ + dispose(): void; +} + +/** + * 默认路径解析器(直接返回路径) + * Default path resolver (returns path as-is) + */ +export class DefaultPathResolver implements IPathResolver { + resolve(path: string): string { + // 如果已经是 URL,直接返回 + if (path.startsWith('http://') || + path.startsWith('https://') || + path.startsWith('data:') || + path.startsWith('blob:')) { + return path; + } + return path; + } +} diff --git a/packages/runtime-core/src/PluginLoader.ts b/packages/runtime-core/src/PluginLoader.ts new file mode 100644 index 00000000..2b7eb2ba --- /dev/null +++ b/packages/runtime-core/src/PluginLoader.ts @@ -0,0 +1,118 @@ +import { runtimePluginManager, type IPlugin } from './PluginManager'; + +export interface PluginPackageInfo { + plugin: boolean; + pluginExport: string; + category?: string; + isEnginePlugin?: boolean; +} + +export interface PluginConfig { + enabled: boolean; + options?: Record; +} + +export interface ProjectPluginConfig { + plugins: Record; +} + +interface LoadedPluginInfo { + id: string; + plugin: IPlugin; + packageInfo: PluginPackageInfo; +} + +const loadedPlugins = new Map(); + +/** + * 从模块动态加载插件 + * @param packageId 包 ID,如 '@esengine/sprite' + * @param packageInfo 包的 esengine 配置 + */ +export async function loadPlugin( + packageId: string, + packageInfo: PluginPackageInfo +): Promise { + if (loadedPlugins.has(packageId)) { + return loadedPlugins.get(packageId)!.plugin; + } + + try { + const module = await import(/* @vite-ignore */ packageId); + const exportName = packageInfo.pluginExport || 'default'; + const plugin = module[exportName] as IPlugin; + + if (!plugin || !plugin.descriptor) { + console.warn(`[PluginLoader] Invalid plugin export from ${packageId}`); + return null; + } + + loadedPlugins.set(packageId, { + id: packageId, + plugin, + packageInfo + }); + + return plugin; + } catch (error) { + console.error(`[PluginLoader] Failed to load plugin ${packageId}:`, error); + return null; + } +} + +/** + * 根据项目配置加载所有启用的插件 + */ +export async function loadEnabledPlugins( + config: ProjectPluginConfig, + packageInfoMap: Record +): Promise { + const sortedPlugins: Array<{ id: string; info: PluginPackageInfo }> = []; + + for (const [id, pluginConfig] of Object.entries(config.plugins)) { + if (!pluginConfig.enabled) continue; + + const packageInfo = packageInfoMap[id]; + if (!packageInfo) { + console.warn(`[PluginLoader] No package info for ${id}, skipping`); + continue; + } + + sortedPlugins.push({ id, info: packageInfo }); + } + + // 引擎核心插件优先加载 + sortedPlugins.sort((a, b) => { + if (a.info.isEnginePlugin && !b.info.isEnginePlugin) return -1; + if (!a.info.isEnginePlugin && b.info.isEnginePlugin) return 1; + return 0; + }); + + for (const { id, info } of sortedPlugins) { + const plugin = await loadPlugin(id, info); + if (plugin) { + runtimePluginManager.register(plugin); + } + } +} + +/** + * 注册预加载的插件(用于已静态导入的插件) + */ +export function registerStaticPlugin(plugin: IPlugin): void { + runtimePluginManager.register(plugin); +} + +/** + * 获取已加载的插件列表 + */ +export function getLoadedPlugins(): IPlugin[] { + return Array.from(loadedPlugins.values()).map(info => info.plugin); +} + +/** + * 重置插件加载器状态 + */ +export function resetPluginLoader(): void { + loadedPlugins.clear(); +} diff --git a/packages/runtime-core/src/PluginManager.ts b/packages/runtime-core/src/PluginManager.ts new file mode 100644 index 00000000..c3e10e63 --- /dev/null +++ b/packages/runtime-core/src/PluginManager.ts @@ -0,0 +1,166 @@ +/** + * Runtime Plugin Manager + * 运行时插件管理器 + */ + +import { ComponentRegistry, ServiceContainer } from '@esengine/ecs-framework'; +import type { IScene } from '@esengine/ecs-framework'; + +export interface SystemContext { + isEditor: boolean; + [key: string]: any; +} + +export interface PluginDescriptor { + id: string; + name: string; + version: string; + description?: string; + category?: string; + enabledByDefault?: boolean; + isEnginePlugin?: boolean; +} + +export interface IRuntimeModule { + registerComponents?(registry: typeof ComponentRegistry): void; + registerServices?(services: ServiceContainer): void; + createSystems?(scene: IScene, context: SystemContext): void; + onSystemsCreated?(scene: IScene, context: SystemContext): void; + onInitialize?(): Promise; + onDestroy?(): void; +} + +export interface IPlugin { + readonly descriptor: PluginDescriptor; + readonly runtimeModule?: IRuntimeModule; +} + +export class RuntimePluginManager { + private _plugins = new Map(); + private _enabledPlugins = new Set(); + private _bInitialized = false; + + register(plugin: IPlugin): void { + const id = plugin.descriptor.id; + if (this._plugins.has(id)) { + return; + } + this._plugins.set(id, plugin); + if (plugin.descriptor.enabledByDefault !== false) { + this._enabledPlugins.add(id); + } + } + + enable(pluginId: string): void { + this._enabledPlugins.add(pluginId); + } + + disable(pluginId: string): void { + this._enabledPlugins.delete(pluginId); + } + + isEnabled(pluginId: string): boolean { + return this._enabledPlugins.has(pluginId); + } + + loadConfig(config: { enabledPlugins: string[] }): void { + this._enabledPlugins.clear(); + for (const id of config.enabledPlugins) { + this._enabledPlugins.add(id); + } + // 始终启用引擎核心插件 + for (const [id, plugin] of this._plugins) { + if (plugin.descriptor.isEnginePlugin) { + this._enabledPlugins.add(id); + } + } + } + + async initializeRuntime(services: ServiceContainer): Promise { + if (this._bInitialized) { + return; + } + + for (const [id, plugin] of this._plugins) { + if (!this._enabledPlugins.has(id)) continue; + const mod = plugin.runtimeModule; + if (mod?.registerComponents) { + try { + mod.registerComponents(ComponentRegistry); + } catch (e) { + console.error(`[PluginManager] Failed to register components for ${id}:`, e); + } + } + } + + for (const [id, plugin] of this._plugins) { + if (!this._enabledPlugins.has(id)) continue; + const mod = plugin.runtimeModule; + if (mod?.registerServices) { + try { + mod.registerServices(services); + } catch (e) { + console.error(`[PluginManager] Failed to register services for ${id}:`, e); + } + } + } + + for (const [id, plugin] of this._plugins) { + if (!this._enabledPlugins.has(id)) continue; + const mod = plugin.runtimeModule; + if (mod?.onInitialize) { + try { + await mod.onInitialize(); + } catch (e) { + console.error(`[PluginManager] Failed to initialize ${id}:`, e); + } + } + } + + this._bInitialized = true; + } + + createSystemsForScene(scene: IScene, context: SystemContext): void { + // Phase 1: 创建系统 + for (const [id, plugin] of this._plugins) { + if (!this._enabledPlugins.has(id)) continue; + const mod = plugin.runtimeModule; + if (mod?.createSystems) { + try { + mod.createSystems(scene, context); + } catch (e) { + console.error(`[PluginManager] Failed to create systems for ${id}:`, e); + } + } + } + + // Phase 2: 连接跨插件依赖 + for (const [id, plugin] of this._plugins) { + if (!this._enabledPlugins.has(id)) continue; + const mod = plugin.runtimeModule; + if (mod?.onSystemsCreated) { + try { + mod.onSystemsCreated(scene, context); + } catch (e) { + console.error(`[PluginManager] Failed to wire dependencies for ${id}:`, e); + } + } + } + } + + getPlugins(): IPlugin[] { + return Array.from(this._plugins.values()); + } + + getPlugin(id: string): IPlugin | undefined { + return this._plugins.get(id); + } + + reset(): void { + this._plugins.clear(); + this._enabledPlugins.clear(); + this._bInitialized = false; + } +} + +export const runtimePluginManager = new RuntimePluginManager(); diff --git a/packages/runtime-core/src/ProjectConfig.ts b/packages/runtime-core/src/ProjectConfig.ts new file mode 100644 index 00000000..9297438b --- /dev/null +++ b/packages/runtime-core/src/ProjectConfig.ts @@ -0,0 +1,128 @@ +import type { PluginPackageInfo, PluginConfig } from './PluginLoader'; + +export interface ProjectConfig { + name: string; + version: string; + plugins: Record; +} + +/** + * 内置引擎插件的包信息 + * 这些信息在构建时从各包的 package.json 中提取 + */ +export const BUILTIN_PLUGIN_PACKAGES: Record = { + '@esengine/engine-core': { + plugin: true, + pluginExport: 'EnginePlugin', + category: 'core', + isEnginePlugin: true + }, + '@esengine/camera': { + plugin: true, + pluginExport: 'CameraPlugin', + category: 'core', + isEnginePlugin: true + }, + '@esengine/sprite': { + plugin: true, + pluginExport: 'SpritePlugin', + category: 'rendering', + isEnginePlugin: true + }, + '@esengine/audio': { + plugin: true, + pluginExport: 'AudioPlugin', + category: 'audio', + isEnginePlugin: true + }, + '@esengine/ui': { + plugin: true, + pluginExport: 'UIPlugin', + category: 'ui' + }, + '@esengine/tilemap': { + plugin: true, + pluginExport: 'TilemapPlugin', + category: 'tilemap' + }, + '@esengine/behavior-tree': { + plugin: true, + pluginExport: 'BehaviorTreePlugin', + category: 'ai' + }, + '@esengine/physics-rapier2d': { + plugin: true, + pluginExport: 'PhysicsPlugin', + category: 'physics' + } +}; + +/** + * 创建默认项目配置 + */ +export function createDefaultProjectConfig(): ProjectConfig { + return { + name: 'New Project', + version: '1.0.0', + plugins: { + '@esengine/engine-core': { enabled: true }, + '@esengine/camera': { enabled: true }, + '@esengine/sprite': { enabled: true }, + '@esengine/audio': { enabled: true }, + '@esengine/ui': { enabled: true }, + '@esengine/tilemap': { enabled: false }, + '@esengine/behavior-tree': { enabled: false }, + '@esengine/physics-rapier2d': { enabled: false } + } + }; +} + +/** + * 合并用户配置与默认配置 + */ +export function mergeProjectConfig( + userConfig: Partial +): ProjectConfig { + const defaultConfig = createDefaultProjectConfig(); + + return { + name: userConfig.name || defaultConfig.name, + version: userConfig.version || defaultConfig.version, + plugins: { + ...defaultConfig.plugins, + ...userConfig.plugins + } + }; +} + +/** + * 从编辑器的 enabledPlugins 列表创建项目配置 + * Create project config from editor's enabledPlugins list + * + * @param enabledPlugins - 启用的插件 ID 列表 / List of enabled plugin IDs + */ +export function createProjectConfigFromEnabledList( + enabledPlugins: string[] +): ProjectConfig { + const defaultConfig = createDefaultProjectConfig(); + + // 先禁用所有非核心插件 + // First disable all non-core plugins + const plugins: Record = {}; + + for (const id of Object.keys(defaultConfig.plugins)) { + const packageInfo = BUILTIN_PLUGIN_PACKAGES[id]; + // 核心插件始终启用 + // Core plugins are always enabled + if (packageInfo?.isEnginePlugin) { + plugins[id] = { enabled: true }; + } else { + plugins[id] = { enabled: enabledPlugins.includes(id) }; + } + } + + return { + ...defaultConfig, + plugins + }; +} diff --git a/packages/runtime-core/src/RuntimeBootstrap.ts b/packages/runtime-core/src/RuntimeBootstrap.ts new file mode 100644 index 00000000..93933e5c --- /dev/null +++ b/packages/runtime-core/src/RuntimeBootstrap.ts @@ -0,0 +1,66 @@ +/** + * Runtime Bootstrap + * 运行时启动器 - 提供通用的初始化流程 + */ + +import { Core } from '@esengine/ecs-framework'; +import type { IScene } from '@esengine/ecs-framework'; +import { + runtimePluginManager, + type IPlugin, + type IRuntimeModule, + type PluginDescriptor, + type SystemContext +} from './PluginManager'; + +export interface RuntimeConfig { + enabledPlugins?: string[]; + isEditor?: boolean; +} + +/** + * 创建插件(简化工厂) + */ +export function createPlugin( + descriptor: PluginDescriptor, + runtimeModule: IRuntimeModule +): IPlugin { + return { descriptor, runtimeModule }; +} + +/** + * 注册插件到运行时 + */ +export function registerPlugin(plugin: IPlugin): void { + runtimePluginManager.register(plugin); +} + +/** + * 初始化运行时 + * @param config 运行时配置 + */ +export async function initializeRuntime(config?: RuntimeConfig): Promise { + if (config?.enabledPlugins) { + runtimePluginManager.loadConfig({ enabledPlugins: config.enabledPlugins }); + } else { + for (const plugin of runtimePluginManager.getPlugins()) { + runtimePluginManager.enable(plugin.descriptor.id); + } + } + + await runtimePluginManager.initializeRuntime(Core.services); +} + +/** + * 为场景创建系统 + */ +export function createSystemsForScene(scene: IScene, context: SystemContext): void { + runtimePluginManager.createSystemsForScene(scene, context); +} + +/** + * 重置运行时(用于热重载等场景) + */ +export function resetRuntime(): void { + runtimePluginManager.reset(); +} diff --git a/packages/runtime-core/src/adapters/BrowserPlatformAdapter.ts b/packages/runtime-core/src/adapters/BrowserPlatformAdapter.ts new file mode 100644 index 00000000..804ffbda --- /dev/null +++ b/packages/runtime-core/src/adapters/BrowserPlatformAdapter.ts @@ -0,0 +1,143 @@ +/** + * Browser Platform Adapter + * 浏览器平台适配器 + * + * 用于独立浏览器运行时的平台适配器 + * Platform adapter for standalone browser runtime + */ + +import type { + IPlatformAdapter, + IPathResolver, + PlatformCapabilities, + PlatformAdapterConfig +} from '../IPlatformAdapter'; + +/** + * 浏览器路径解析器 + * Browser path resolver + */ +export class BrowserPathResolver implements IPathResolver { + private _baseUrl: string; + + constructor(baseUrl: string = '') { + this._baseUrl = baseUrl; + } + + resolve(path: string): string { + // 如果已经是完整 URL,直接返回 + if (path.startsWith('http://') || + path.startsWith('https://') || + path.startsWith('data:') || + path.startsWith('blob:') || + path.startsWith('/asset?')) { + return path; + } + + // 相对路径,添加资产请求前缀 + return `/asset?path=${encodeURIComponent(path)}`; + } + + /** + * 更新基础 URL + */ + setBaseUrl(baseUrl: string): void { + this._baseUrl = baseUrl; + } +} + +/** + * 浏览器平台适配器配置 + */ +export interface BrowserPlatformConfig { + /** WASM 模块(预加载的)*/ + wasmModule?: any; + /** WASM 模块加载器(异步加载)*/ + wasmModuleLoader?: () => Promise; + /** 资产基础 URL */ + assetBaseUrl?: string; +} + +/** + * 浏览器平台适配器 + * Browser platform adapter + */ +export class BrowserPlatformAdapter implements IPlatformAdapter { + readonly name = 'browser'; + + readonly capabilities: PlatformCapabilities = { + fileSystem: false, + hotReload: false, + gizmos: false, + grid: false, + sceneEditing: false + }; + + private _pathResolver: BrowserPathResolver; + private _canvas: HTMLCanvasElement | null = null; + private _config: BrowserPlatformConfig; + private _viewportSize = { width: 0, height: 0 }; + + constructor(config: BrowserPlatformConfig = {}) { + this._config = config; + this._pathResolver = new BrowserPathResolver(config.assetBaseUrl || ''); + } + + get pathResolver(): IPathResolver { + return this._pathResolver; + } + + async initialize(config: PlatformAdapterConfig): Promise { + // 获取 Canvas + this._canvas = document.getElementById(config.canvasId) as HTMLCanvasElement; + if (!this._canvas) { + throw new Error(`Canvas not found: ${config.canvasId}`); + } + + // 设置尺寸 + const width = config.width || window.innerWidth; + const height = config.height || window.innerHeight; + this._canvas.width = width; + this._canvas.height = height; + this._viewportSize = { width, height }; + } + + async getWasmModule(): Promise { + // 如果已提供模块,直接返回 + if (this._config.wasmModule) { + return this._config.wasmModule; + } + + // 如果提供了加载器,使用加载器 + if (this._config.wasmModuleLoader) { + return this._config.wasmModuleLoader(); + } + + // 默认:尝试动态导入 + throw new Error('No WASM module or loader provided'); + } + + getCanvas(): HTMLCanvasElement | null { + return this._canvas; + } + + resize(width: number, height: number): void { + if (this._canvas) { + this._canvas.width = width; + this._canvas.height = height; + } + this._viewportSize = { width, height }; + } + + getViewportSize(): { width: number; height: number } { + return { ...this._viewportSize }; + } + + isEditorMode(): boolean { + return false; + } + + dispose(): void { + this._canvas = null; + } +} diff --git a/packages/runtime-core/src/adapters/EditorPlatformAdapter.ts b/packages/runtime-core/src/adapters/EditorPlatformAdapter.ts new file mode 100644 index 00000000..356d003f --- /dev/null +++ b/packages/runtime-core/src/adapters/EditorPlatformAdapter.ts @@ -0,0 +1,185 @@ +/** + * Editor Platform Adapter + * 编辑器平台适配器 + * + * 用于 Tauri 编辑器内嵌预览的平台适配器 + * Platform adapter for Tauri editor embedded preview + */ + +import type { + IPlatformAdapter, + IPathResolver, + PlatformCapabilities, + PlatformAdapterConfig +} from '../IPlatformAdapter'; + +/** + * 编辑器路径解析器 + * Editor path resolver + * + * 使用 Tauri 的 convertFileSrc 转换本地文件路径 + * Uses Tauri's convertFileSrc to convert local file paths + */ +export class EditorPathResolver implements IPathResolver { + private _pathTransformer: (path: string) => string; + + constructor(pathTransformer: (path: string) => string) { + this._pathTransformer = pathTransformer; + } + + resolve(path: string): string { + // 如果已经是 URL,直接返回 + if (path.startsWith('http://') || + path.startsWith('https://') || + path.startsWith('data:') || + path.startsWith('blob:') || + path.startsWith('asset://')) { + return path; + } + + // 使用 Tauri 路径转换器 + return this._pathTransformer(path); + } + + /** + * 更新路径转换器 + */ + setPathTransformer(transformer: (path: string) => string): void { + this._pathTransformer = transformer; + } +} + +/** + * 编辑器平台适配器配置 + */ +export interface EditorPlatformConfig { + /** WASM 模块(预加载的)*/ + wasmModule: any; + /** 路径转换函数(使用 Tauri 的 convertFileSrc)*/ + pathTransformer: (path: string) => string; + /** Gizmo 数据提供者 */ + gizmoDataProvider?: (component: any, entity: any, isSelected: boolean) => any; + /** Gizmo 检查函数 */ + hasGizmoProvider?: (component: any) => boolean; +} + +/** + * 编辑器平台适配器 + * Editor platform adapter + */ +export class EditorPlatformAdapter implements IPlatformAdapter { + readonly name = 'editor'; + + readonly capabilities: PlatformCapabilities = { + fileSystem: true, + hotReload: true, + gizmos: true, + grid: true, + sceneEditing: true + }; + + private _pathResolver: EditorPathResolver; + private _canvas: HTMLCanvasElement | null = null; + private _config: EditorPlatformConfig; + private _viewportSize = { width: 0, height: 0 }; + private _showGrid = true; + private _showGizmos = true; + + constructor(config: EditorPlatformConfig) { + this._config = config; + this._pathResolver = new EditorPathResolver(config.pathTransformer); + } + + get pathResolver(): IPathResolver { + return this._pathResolver; + } + + /** + * 获取 Gizmo 数据提供者 + */ + get gizmoDataProvider() { + return this._config.gizmoDataProvider; + } + + /** + * 获取 Gizmo 检查函数 + */ + get hasGizmoProvider() { + return this._config.hasGizmoProvider; + } + + async initialize(config: PlatformAdapterConfig): Promise { + // 获取 Canvas + this._canvas = document.getElementById(config.canvasId) as HTMLCanvasElement; + if (!this._canvas) { + throw new Error(`Canvas not found: ${config.canvasId}`); + } + + // 处理 DPR 缩放 + const dpr = window.devicePixelRatio || 1; + const container = this._canvas.parentElement; + + if (container) { + const rect = container.getBoundingClientRect(); + const width = config.width || Math.floor(rect.width * dpr); + const height = config.height || Math.floor(rect.height * dpr); + + this._canvas.width = width; + this._canvas.height = height; + this._canvas.style.width = `${rect.width}px`; + this._canvas.style.height = `${rect.height}px`; + + this._viewportSize = { width, height }; + } else { + const width = config.width || window.innerWidth; + const height = config.height || window.innerHeight; + this._canvas.width = width; + this._canvas.height = height; + this._viewportSize = { width, height }; + } + } + + async getWasmModule(): Promise { + return this._config.wasmModule; + } + + getCanvas(): HTMLCanvasElement | null { + return this._canvas; + } + + resize(width: number, height: number): void { + if (this._canvas) { + this._canvas.width = width; + this._canvas.height = height; + } + this._viewportSize = { width, height }; + } + + getViewportSize(): { width: number; height: number } { + return { ...this._viewportSize }; + } + + isEditorMode(): boolean { + return true; + } + + setShowGrid(show: boolean): void { + this._showGrid = show; + } + + getShowGrid(): boolean { + return this._showGrid; + } + + setShowGizmos(show: boolean): void { + this._showGizmos = show; + } + + getShowGizmos(): boolean { + return this._showGizmos; + } + + dispose(): void { + this._canvas = null; + } +} diff --git a/packages/runtime-core/src/adapters/index.ts b/packages/runtime-core/src/adapters/index.ts new file mode 100644 index 00000000..3688ab60 --- /dev/null +++ b/packages/runtime-core/src/adapters/index.ts @@ -0,0 +1,7 @@ +/** + * Platform Adapters + * 平台适配器 + */ + +export { BrowserPlatformAdapter, BrowserPathResolver, type BrowserPlatformConfig } from './BrowserPlatformAdapter'; +export { EditorPlatformAdapter, EditorPathResolver, type EditorPlatformConfig } from './EditorPlatformAdapter'; diff --git a/packages/runtime-core/src/index.ts b/packages/runtime-core/src/index.ts new file mode 100644 index 00000000..24fdf1c8 --- /dev/null +++ b/packages/runtime-core/src/index.ts @@ -0,0 +1,72 @@ +export { + RuntimePluginManager, + runtimePluginManager, + type SystemContext, + type PluginDescriptor, + type IRuntimeModule, + type IPlugin +} from './PluginManager'; + +export { + createPlugin, + registerPlugin, + initializeRuntime, + createSystemsForScene, + resetRuntime, + type RuntimeConfig +} from './RuntimeBootstrap'; + +export { + loadPlugin, + loadEnabledPlugins, + registerStaticPlugin, + getLoadedPlugins, + resetPluginLoader, + type PluginPackageInfo, + type PluginConfig, + type ProjectPluginConfig +} from './PluginLoader'; + +export { + BUILTIN_PLUGIN_PACKAGES, + createDefaultProjectConfig, + mergeProjectConfig, + createProjectConfigFromEnabledList, + type ProjectConfig +} from './ProjectConfig'; + +// Platform Adapter +export { + DefaultPathResolver, + type IPlatformAdapter, + type IPathResolver, + type PlatformCapabilities, + type PlatformAdapterConfig +} from './IPlatformAdapter'; + +// Game Runtime +export { + GameRuntime, + createGameRuntime, + type GameRuntimeConfig, + type RuntimeState +} from './GameRuntime'; + +// Platform Adapters +export { + BrowserPlatformAdapter, + BrowserPathResolver, + type BrowserPlatformConfig, + EditorPlatformAdapter, + EditorPathResolver, + type EditorPlatformConfig +} from './adapters'; + +// Browser File System Service +export { + BrowserFileSystemService, + createBrowserFileSystem, + type AssetCatalog, + type AssetCatalogEntry, + type BrowserFileSystemOptions +} from './services/BrowserFileSystemService'; diff --git a/packages/runtime-core/src/services/BrowserFileSystemService.ts b/packages/runtime-core/src/services/BrowserFileSystemService.ts new file mode 100644 index 00000000..bccd56ad --- /dev/null +++ b/packages/runtime-core/src/services/BrowserFileSystemService.ts @@ -0,0 +1,306 @@ +/** + * Browser File System Service + * 浏览器文件系统服务 + * + * 在浏览器运行时环境中,通过 HTTP fetch 加载资产文件。 + * 使用资产目录(asset-catalog.json)来解析 GUID 到实际 URL。 + * + * In browser runtime environment, loads asset files via HTTP fetch. + * Uses asset catalog to resolve GUIDs to actual URLs. + */ + +/** + * Asset catalog entry + */ +export interface AssetCatalogEntry { + guid: string; + path: string; + type: string; + size: number; + hash: string; +} + +/** + * Asset catalog loaded from JSON + */ +export interface AssetCatalog { + version: string; + createdAt: number; + entries: Record; +} + +/** + * Browser file system service options + */ +export interface BrowserFileSystemOptions { + /** Base URL for assets (e.g., '/assets' or 'https://cdn.example.com/assets') */ + baseUrl?: string; + /** Asset catalog URL */ + catalogUrl?: string; + /** Enable caching */ + enableCache?: boolean; +} + +/** + * Browser File System Service + * + * Provides file system-like API for browser environments + * by fetching files over HTTP. + */ +export class BrowserFileSystemService { + private _baseUrl: string; + private _catalogUrl: string; + private _catalog: AssetCatalog | null = null; + private _cache = new Map(); + private _enableCache: boolean; + private _initialized = false; + + constructor(options: BrowserFileSystemOptions = {}) { + this._baseUrl = options.baseUrl ?? '/assets'; + this._catalogUrl = options.catalogUrl ?? '/asset-catalog.json'; + this._enableCache = options.enableCache ?? true; + } + + /** + * Initialize service and load catalog + */ + async initialize(): Promise { + if (this._initialized) return; + + try { + await this._loadCatalog(); + this._initialized = true; + console.log('[BrowserFileSystem] Initialized with', + Object.keys(this._catalog?.entries ?? {}).length, 'assets'); + } catch (error) { + console.warn('[BrowserFileSystem] Failed to load catalog:', error); + // Continue without catalog - will use path-based loading + this._initialized = true; + } + } + + /** + * Load asset catalog + */ + private async _loadCatalog(): Promise { + const response = await fetch(this._catalogUrl); + if (!response.ok) { + throw new Error(`Failed to fetch catalog: ${response.status}`); + } + this._catalog = await response.json(); + } + + /** + * Read file content as string + * @param path - Can be GUID, relative path, or absolute path + */ + async readFile(path: string): Promise { + const url = this._resolveUrl(path); + + // Check cache + if (this._enableCache && this._cache.has(url)) { + return this._cache.get(url)!; + } + + const response = await fetch(url); + if (!response.ok) { + throw new Error(`Failed to fetch file: ${url} (${response.status})`); + } + + const content = await response.text(); + + // Cache result + if (this._enableCache) { + this._cache.set(url, content); + } + + return content; + } + + /** + * Read file as binary (ArrayBuffer) + */ + async readBinary(path: string): Promise { + const url = this._resolveUrl(path); + + const response = await fetch(url); + if (!response.ok) { + throw new Error(`Failed to fetch binary: ${url} (${response.status})`); + } + + return response.arrayBuffer(); + } + + /** + * Read file as Blob + */ + async readBlob(path: string): Promise { + const url = this._resolveUrl(path); + + const response = await fetch(url); + if (!response.ok) { + throw new Error(`Failed to fetch blob: ${url} (${response.status})`); + } + + return response.blob(); + } + + /** + * Check if file exists (via HEAD request) + */ + async exists(path: string): Promise { + const url = this._resolveUrl(path); + + try { + const response = await fetch(url, { method: 'HEAD' }); + return response.ok; + } catch { + return false; + } + } + + /** + * Resolve path to URL + * Handles GUID, relative path, and absolute path + */ + private _resolveUrl(path: string): string { + // Check if it's a GUID and we have a catalog + if (this._catalog && this._isGuid(path)) { + const entry = this._catalog.entries[path]; + if (entry) { + return this._pathToUrl(entry.path); + } + } + + // Check if it's an absolute Windows path (e.g., F:\...) + if (/^[A-Za-z]:[\\/]/.test(path)) { + // Try to extract relative path from absolute path + const relativePath = this._extractRelativePath(path); + if (relativePath) { + return this._pathToUrl(relativePath); + } + // Fallback: use just the filename + const filename = path.split(/[\\/]/).pop(); + return `${this._baseUrl}/${filename}`; + } + + // Check if it's already a URL + if (path.startsWith('http://') || path.startsWith('https://') || path.startsWith('/')) { + return path; + } + + // Treat as relative path + return this._pathToUrl(path); + } + + /** + * Convert relative path to URL + */ + private _pathToUrl(relativePath: string): string { + // Normalize path separators + const normalized = relativePath.replace(/\\/g, '/'); + + // Remove leading 'assets/' if baseUrl already includes it + let cleanPath = normalized; + if (cleanPath.startsWith('assets/') && this._baseUrl.endsWith('/assets')) { + cleanPath = cleanPath.substring(7); + } + + // Ensure no double slashes + const base = this._baseUrl.endsWith('/') ? this._baseUrl.slice(0, -1) : this._baseUrl; + const path = cleanPath.startsWith('/') ? cleanPath.slice(1) : cleanPath; + + return `${base}/${path}`; + } + + /** + * Extract relative path from absolute path + */ + private _extractRelativePath(absolutePath: string): string | null { + const normalized = absolutePath.replace(/\\/g, '/'); + + // Look for 'assets/' in the path + const assetsIndex = normalized.toLowerCase().indexOf('/assets/'); + if (assetsIndex >= 0) { + return normalized.substring(assetsIndex + 1); // Include 'assets/' + } + + return null; + } + + /** + * Check if string looks like a GUID + */ + private _isGuid(str: string): boolean { + return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(str); + } + + /** + * Get asset metadata from catalog + */ + getAssetMetadata(guidOrPath: string): AssetCatalogEntry | null { + if (!this._catalog) return null; + + // Try as GUID + if (this._catalog.entries[guidOrPath]) { + return this._catalog.entries[guidOrPath]; + } + + // Try as path + for (const entry of Object.values(this._catalog.entries)) { + if (entry.path === guidOrPath) { + return entry; + } + } + + return null; + } + + /** + * Get all assets of a specific type + */ + getAssetsByType(type: string): AssetCatalogEntry[] { + if (!this._catalog) return []; + + return Object.values(this._catalog.entries) + .filter(entry => entry.type === type); + } + + /** + * Clear cache + */ + clearCache(): void { + this._cache.clear(); + } + + /** + * Get catalog + */ + get catalog(): AssetCatalog | null { + return this._catalog; + } + + /** + * Check if initialized + */ + get isInitialized(): boolean { + return this._initialized; + } + + /** + * Dispose service and clear resources + * Required by IService interface + */ + dispose(): void { + this._cache.clear(); + this._catalog = null; + this._initialized = false; + } +} + +/** + * Create and register browser file system service + */ +export function createBrowserFileSystem(options?: BrowserFileSystemOptions): BrowserFileSystemService { + return new BrowserFileSystemService(options); +} diff --git a/packages/runtime-core/tsconfig.build.json b/packages/runtime-core/tsconfig.build.json new file mode 100644 index 00000000..f39a0594 --- /dev/null +++ b/packages/runtime-core/tsconfig.build.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "composite": false, + "declaration": true, + "declarationMap": true, + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "**/*.test.ts"] +} diff --git a/packages/runtime-core/tsconfig.json b/packages/runtime-core/tsconfig.json new file mode 100644 index 00000000..be81e3e6 --- /dev/null +++ b/packages/runtime-core/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"], + "references": [ + { "path": "../core" }, + { "path": "../ecs-engine-bindgen" }, + { "path": "../engine-core" }, + { "path": "../asset-system" } + ] +} diff --git a/packages/runtime-core/tsup.config.ts b/packages/runtime-core/tsup.config.ts new file mode 100644 index 00000000..f704a430 --- /dev/null +++ b/packages/runtime-core/tsup.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'tsup'; +import { runtimeOnlyPreset } from '../build-config/src/presets/plugin-tsup'; + +export default defineConfig({ + ...runtimeOnlyPreset(), + tsconfig: 'tsconfig.build.json' +}); diff --git a/packages/sprite-editor/package.json b/packages/sprite-editor/package.json new file mode 100644 index 00000000..234a67b0 --- /dev/null +++ b/packages/sprite-editor/package.json @@ -0,0 +1,44 @@ +{ + "name": "@esengine/sprite-editor", + "version": "1.0.0", + "description": "Editor components for sprite system", + "main": "dist/index.js", + "module": "dist/index.js", + "types": "dist/index.d.ts", + "type": "module", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "files": [ + "dist" + ], + "scripts": { + "build": "tsup", + "build:watch": "tsup --watch", + "type-check": "tsc --noEmit", + "clean": "rimraf dist" + }, + "devDependencies": { + "@esengine/ecs-framework": "workspace:*", + "@esengine/engine-core": "workspace:*", + "@esengine/sprite": "workspace:*", + "@esengine/editor-core": "workspace:*", + "@esengine/build-config": "workspace:*", + "react": "^18.3.1", + "@types/react": "^18.2.0", + "rimraf": "^5.0.5", + "tsup": "^8.0.0", + "typescript": "^5.3.3" + }, + "keywords": [ + "ecs", + "sprite", + "editor", + "animation" + ], + "author": "yhh", + "license": "MIT" +} diff --git a/packages/sprite-editor/src/index.ts b/packages/sprite-editor/src/index.ts new file mode 100644 index 00000000..7ca9e4fc --- /dev/null +++ b/packages/sprite-editor/src/index.ts @@ -0,0 +1,147 @@ +/** + * @esengine/sprite-editor + * + * Editor support for @esengine/sprite - inspectors, field editors, and entity templates + * 精灵编辑器支持 - 检视器、字段编辑器和实体模板 + */ + +import type { Entity, ServiceContainer } from '@esengine/ecs-framework'; +import { Core } from '@esengine/ecs-framework'; +import type { + IEditorModuleLoader, + EntityCreationTemplate +} from '@esengine/editor-core'; +import { + EntityStoreService, + MessageHub, + ComponentRegistry +} from '@esengine/editor-core'; +import { TransformComponent } from '@esengine/engine-core'; + +// Runtime imports from @esengine/sprite +import { + SpriteComponent, + SpriteAnimatorComponent +} from '@esengine/sprite'; + +/** + * 精灵编辑器模块 + * Sprite Editor Module + */ +export class SpriteEditorModule implements IEditorModuleLoader { + async install(services: ServiceContainer): Promise { + // 注册 Sprite 组件到编辑器组件注册表 | Register Sprite components to editor component registry + const componentRegistry = services.resolve(ComponentRegistry); + if (componentRegistry) { + const spriteComponents = [ + { + name: 'Sprite', + type: SpriteComponent, + category: 'components.category.rendering', + description: '2D sprite rendering component', + icon: 'Image' + }, + { + name: 'SpriteAnimator', + type: SpriteAnimatorComponent, + category: 'components.category.rendering', + description: 'Sprite frame animation component', + icon: 'Film' + }, + ]; + + for (const comp of spriteComponents) { + componentRegistry.register({ + name: comp.name, + type: comp.type, + category: comp.category, + description: comp.description, + icon: comp.icon + }); + } + } + } + + async uninstall(): Promise { + // Nothing to cleanup + } + + getEntityCreationTemplates(): EntityCreationTemplate[] { + return [ + // Sprite Entity + { + id: 'create-sprite', + label: 'Sprite', + icon: 'Image', + category: 'rendering', + order: 100, + create: (): number => { + return this.createSpriteEntity('Sprite'); + } + }, + + // Animated Sprite Entity + { + id: 'create-animated-sprite', + label: 'Animated Sprite', + icon: 'Film', + category: 'rendering', + order: 101, + create: (): number => { + return this.createSpriteEntity('AnimatedSprite', (entity) => { + const animator = new SpriteAnimatorComponent(); + animator.autoPlay = true; + entity.addComponent(animator); + }); + } + }, + ]; + } + + /** + * 创建 Sprite 实体的辅助方法 + * Helper method to create Sprite entity + */ + private createSpriteEntity(baseName: string, configure?: (entity: Entity) => void): number { + const scene = Core.scene; + if (!scene) { + throw new Error('Scene not available'); + } + + const entityStore = Core.services.resolve(EntityStoreService); + const messageHub = Core.services.resolve(MessageHub); + + if (!entityStore || !messageHub) { + throw new Error('EntityStoreService or MessageHub not available'); + } + + const existingCount = entityStore.getAllEntities() + .filter((e: Entity) => e.name.startsWith(baseName)).length; + const entityName = existingCount > 0 ? `${baseName} ${existingCount + 1}` : baseName; + + const entity = scene.createEntity(entityName); + + // Add Transform component + const transform = new TransformComponent(); + entity.addComponent(transform); + + // Add Sprite component + const sprite = new SpriteComponent(); + entity.addComponent(sprite); + + if (configure) { + configure(entity); + } + + entityStore.addEntity(entity); + messageHub.publish('entity:added', { entity }); + messageHub.publish('scene:modified', {}); + entityStore.selectEntity(entity); + + return entity.id; + } +} + +export const spriteEditorModule = new SpriteEditorModule(); + +export default spriteEditorModule; diff --git a/packages/sprite-editor/tsconfig.build.json b/packages/sprite-editor/tsconfig.build.json new file mode 100644 index 00000000..29e209e8 --- /dev/null +++ b/packages/sprite-editor/tsconfig.build.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "composite": false, + "declaration": true, + "declarationMap": true, + "outDir": "./dist", + "rootDir": "./src", + "jsx": "react-jsx" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "**/*.test.ts"] +} diff --git a/packages/sprite-editor/tsconfig.json b/packages/sprite-editor/tsconfig.json new file mode 100644 index 00000000..a1614d97 --- /dev/null +++ b/packages/sprite-editor/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./dist", + "rootDir": "./src", + "jsx": "react-jsx" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"], + "references": [ + { "path": "../core" }, + { "path": "../sprite" }, + { "path": "../editor-core" } + ] +} diff --git a/packages/sprite-editor/tsup.config.ts b/packages/sprite-editor/tsup.config.ts new file mode 100644 index 00000000..b4f49f5d --- /dev/null +++ b/packages/sprite-editor/tsup.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'tsup'; +import { editorOnlyPreset } from '../build-config/src/presets/plugin-tsup'; + +export default defineConfig({ + ...editorOnlyPreset(), + tsconfig: 'tsconfig.build.json' +}); diff --git a/packages/sprite/package.json b/packages/sprite/package.json new file mode 100644 index 00000000..9375d9c9 --- /dev/null +++ b/packages/sprite/package.json @@ -0,0 +1,48 @@ +{ + "name": "@esengine/sprite", + "version": "1.0.0", + "description": "ECS-based 2D sprite rendering and animation system", + "esengine": { + "plugin": true, + "pluginExport": "SpritePlugin", + "category": "rendering", + "isEnginePlugin": true + }, + "main": "dist/index.js", + "module": "dist/index.js", + "types": "dist/index.d.ts", + "type": "module", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "files": [ + "dist" + ], + "scripts": { + "build": "tsup", + "build:watch": "tsup --watch", + "type-check": "tsc --noEmit", + "clean": "rimraf dist" + }, + "devDependencies": { + "@esengine/ecs-framework": "workspace:*", + "@esengine/asset-system": "workspace:*", + "@esengine/engine-core": "workspace:*", + "@esengine/build-config": "workspace:*", + "rimraf": "^5.0.5", + "tsup": "^8.0.0", + "typescript": "^5.3.3" + }, + "keywords": [ + "ecs", + "sprite", + "animation", + "2d", + "webgl" + ], + "author": "yhh", + "license": "MIT" +} diff --git a/packages/components/src/SpriteAnimatorComponent.ts b/packages/sprite/src/SpriteAnimatorComponent.ts similarity index 100% rename from packages/components/src/SpriteAnimatorComponent.ts rename to packages/sprite/src/SpriteAnimatorComponent.ts diff --git a/packages/components/src/SpriteComponent.ts b/packages/sprite/src/SpriteComponent.ts similarity index 100% rename from packages/components/src/SpriteComponent.ts rename to packages/sprite/src/SpriteComponent.ts diff --git a/packages/sprite/src/SpriteRuntimeModule.ts b/packages/sprite/src/SpriteRuntimeModule.ts new file mode 100644 index 00000000..5b93ccab --- /dev/null +++ b/packages/sprite/src/SpriteRuntimeModule.ts @@ -0,0 +1,42 @@ +import type { ComponentRegistry as ComponentRegistryType, IScene } from '@esengine/ecs-framework'; +import type { IRuntimeModule, IPlugin, PluginDescriptor, SystemContext } from '@esengine/engine-core'; +import { SpriteComponent } from './SpriteComponent'; +import { SpriteAnimatorComponent } from './SpriteAnimatorComponent'; +import { SpriteAnimatorSystem } from './systems/SpriteAnimatorSystem'; + +export type { SystemContext, PluginDescriptor, IRuntimeModule as IRuntimeModuleLoader, IPlugin as IPluginLoader }; + +class SpriteRuntimeModule implements IRuntimeModule { + registerComponents(registry: typeof ComponentRegistryType): void { + registry.register(SpriteComponent); + registry.register(SpriteAnimatorComponent); + } + + createSystems(scene: IScene, context: SystemContext): void { + const animatorSystem = new SpriteAnimatorSystem(); + + if (context.isEditor) { + animatorSystem.enabled = false; + } + + scene.addSystem(animatorSystem); + (context as any).animatorSystem = animatorSystem; + } +} + +const descriptor: PluginDescriptor = { + id: '@esengine/sprite', + name: 'Sprite Components', + version: '1.0.0', + description: 'Sprite and SpriteAnimator components for 2D rendering', + category: 'rendering', + enabledByDefault: true, + isEnginePlugin: true +}; + +export const SpritePlugin: IPlugin = { + descriptor, + runtimeModule: new SpriteRuntimeModule() +}; + +export { SpriteRuntimeModule }; diff --git a/packages/sprite/src/index.ts b/packages/sprite/src/index.ts new file mode 100644 index 00000000..074306aa --- /dev/null +++ b/packages/sprite/src/index.ts @@ -0,0 +1,5 @@ +export { SpriteComponent } from './SpriteComponent'; +export { SpriteAnimatorComponent } from './SpriteAnimatorComponent'; +export type { AnimationFrame, AnimationClip } from './SpriteAnimatorComponent'; +export { SpriteAnimatorSystem } from './systems/SpriteAnimatorSystem'; +export { SpriteRuntimeModule, SpritePlugin } from './SpriteRuntimeModule'; diff --git a/packages/components/src/systems/SpriteAnimatorSystem.ts b/packages/sprite/src/systems/SpriteAnimatorSystem.ts similarity index 100% rename from packages/components/src/systems/SpriteAnimatorSystem.ts rename to packages/sprite/src/systems/SpriteAnimatorSystem.ts diff --git a/packages/sprite/tsconfig.build.json b/packages/sprite/tsconfig.build.json new file mode 100644 index 00000000..f39a0594 --- /dev/null +++ b/packages/sprite/tsconfig.build.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "composite": false, + "declaration": true, + "declarationMap": true, + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "**/*.test.ts"] +} diff --git a/packages/sprite/tsconfig.json b/packages/sprite/tsconfig.json new file mode 100644 index 00000000..8b270592 --- /dev/null +++ b/packages/sprite/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "composite": true, + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"], + "references": [ + { "path": "../core" }, + { "path": "../asset-system" } + ] +} diff --git a/packages/sprite/tsup.config.ts b/packages/sprite/tsup.config.ts new file mode 100644 index 00000000..f704a430 --- /dev/null +++ b/packages/sprite/tsup.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'tsup'; +import { runtimeOnlyPreset } from '../build-config/src/presets/plugin-tsup'; + +export default defineConfig({ + ...runtimeOnlyPreset(), + tsconfig: 'tsconfig.build.json' +}); diff --git a/packages/tilemap-editor/package.json b/packages/tilemap-editor/package.json new file mode 100644 index 00000000..ade22014 --- /dev/null +++ b/packages/tilemap-editor/package.json @@ -0,0 +1,47 @@ +{ + "name": "@esengine/tilemap-editor", + "version": "1.0.0", + "description": "Editor support for @esengine/tilemap - tilemap editor, tools, and panels", + "main": "dist/index.js", + "module": "dist/index.js", + "types": "dist/index.d.ts", + "type": "module", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "files": [ + "dist" + ], + "scripts": { + "build": "tsup", + "build:watch": "tsup --watch", + "type-check": "tsc --noEmit", + "clean": "rimraf dist" + }, + "dependencies": { + "@esengine/tilemap": "workspace:*" + }, + "devDependencies": { + "@esengine/ecs-framework": "workspace:*", + "@esengine/engine-core": "workspace:*", + "@esengine/editor-core": "workspace:*", + "@esengine/build-config": "workspace:*", + "lucide-react": "^0.545.0", + "react": "^18.3.1", + "zustand": "^5.0.8", + "@types/react": "^18.3.12", + "rimraf": "^5.0.5", + "tsup": "^8.0.0", + "typescript": "^5.3.3" + }, + "keywords": [ + "ecs", + "tilemap", + "editor" + ], + "author": "", + "license": "MIT" +} diff --git a/packages/tilemap/src/editor/components/TilemapCanvas.tsx b/packages/tilemap-editor/src/components/TilemapCanvas.tsx similarity index 99% rename from packages/tilemap/src/editor/components/TilemapCanvas.tsx rename to packages/tilemap-editor/src/components/TilemapCanvas.tsx index 3161f734..1ae45435 100644 --- a/packages/tilemap/src/editor/components/TilemapCanvas.tsx +++ b/packages/tilemap-editor/src/components/TilemapCanvas.tsx @@ -3,7 +3,7 @@ */ import React, { useRef, useEffect, useState, useCallback } from 'react'; -import type { TilemapComponent } from '../../TilemapComponent'; +import type { TilemapComponent } from '@esengine/tilemap'; import { useTilemapEditorStore } from '../stores/TilemapEditorStore'; import type { ITilemapTool, ToolContext } from '../tools/ITilemapTool'; import { BrushTool } from '../tools/BrushTool'; diff --git a/packages/tilemap/src/editor/components/TilesetPreview.tsx b/packages/tilemap-editor/src/components/TilesetPreview.tsx similarity index 100% rename from packages/tilemap/src/editor/components/TilesetPreview.tsx rename to packages/tilemap-editor/src/components/TilesetPreview.tsx diff --git a/packages/tilemap/src/editor/components/panels/LayerPanel.tsx b/packages/tilemap-editor/src/components/panels/LayerPanel.tsx similarity index 99% rename from packages/tilemap/src/editor/components/panels/LayerPanel.tsx rename to packages/tilemap-editor/src/components/panels/LayerPanel.tsx index d6bea3ec..90bde5ac 100644 --- a/packages/tilemap/src/editor/components/panels/LayerPanel.tsx +++ b/packages/tilemap-editor/src/components/panels/LayerPanel.tsx @@ -6,7 +6,7 @@ import React, { useState, useCallback } from 'react'; import { Eye, EyeOff, Lock, Unlock, Plus, Trash2, ChevronUp, ChevronDown, Paintbrush, Shield, Grid3X3 } from 'lucide-react'; import { useTilemapEditorStore, type LayerState } from '../../stores/TilemapEditorStore'; -import type { TilemapComponent } from '../../../TilemapComponent'; +import type { TilemapComponent } from '@esengine/tilemap'; interface LayerPanelProps { tilemap: TilemapComponent | null; diff --git a/packages/tilemap/src/editor/components/panels/TileSetSelectorPanel.tsx b/packages/tilemap-editor/src/components/panels/TileSetSelectorPanel.tsx similarity index 100% rename from packages/tilemap/src/editor/components/panels/TileSetSelectorPanel.tsx rename to packages/tilemap-editor/src/components/panels/TileSetSelectorPanel.tsx diff --git a/packages/tilemap/src/editor/components/panels/TilemapDetailsPanel.tsx b/packages/tilemap-editor/src/components/panels/TilemapDetailsPanel.tsx similarity index 99% rename from packages/tilemap/src/editor/components/panels/TilemapDetailsPanel.tsx rename to packages/tilemap-editor/src/components/panels/TilemapDetailsPanel.tsx index fed14a16..9f9fc177 100644 --- a/packages/tilemap/src/editor/components/panels/TilemapDetailsPanel.tsx +++ b/packages/tilemap-editor/src/components/panels/TilemapDetailsPanel.tsx @@ -18,7 +18,7 @@ import { EyeOff } from 'lucide-react'; import { useTilemapEditorStore, type LayerState } from '../../stores/TilemapEditorStore'; -import type { TilemapComponent } from '../../../TilemapComponent'; +import type { TilemapComponent } from '@esengine/tilemap'; import '../../styles/TilemapDetailsPanel.css'; interface TilemapDetailsPanelProps { diff --git a/packages/tilemap/src/editor/components/panels/TilemapEditorPanel.tsx b/packages/tilemap-editor/src/components/panels/TilemapEditorPanel.tsx similarity index 99% rename from packages/tilemap/src/editor/components/panels/TilemapEditorPanel.tsx rename to packages/tilemap-editor/src/components/panels/TilemapEditorPanel.tsx index f690efd5..6898b761 100644 --- a/packages/tilemap/src/editor/components/panels/TilemapEditorPanel.tsx +++ b/packages/tilemap-editor/src/components/panels/TilemapEditorPanel.tsx @@ -36,7 +36,7 @@ import { } from 'lucide-react'; import { Core, Entity } from '@esengine/ecs-framework'; import { MessageHub, ProjectService, IFileSystemService, type IFileSystem, IDialogService, type IDialog } from '@esengine/editor-core'; -import { TilemapComponent, type ITilesetData, type ResizeAnchor } from '../../../TilemapComponent'; +import { TilemapComponent, type ITilesetData, type ResizeAnchor } from '@esengine/tilemap'; import { useTilemapEditorStore, type TilemapToolType, type LayerState } from '../../stores/TilemapEditorStore'; import { TilemapCanvas } from '../TilemapCanvas'; import { TileSetSelectorPanel } from './TileSetSelectorPanel'; diff --git a/packages/tilemap/src/editor/components/panels/TilesetPanel.tsx b/packages/tilemap-editor/src/components/panels/TilesetPanel.tsx similarity index 98% rename from packages/tilemap/src/editor/components/panels/TilesetPanel.tsx rename to packages/tilemap-editor/src/components/panels/TilesetPanel.tsx index db69e5a8..b515868f 100644 --- a/packages/tilemap/src/editor/components/panels/TilesetPanel.tsx +++ b/packages/tilemap-editor/src/components/panels/TilesetPanel.tsx @@ -5,7 +5,7 @@ import React, { useEffect, useCallback } from 'react'; import { Core } from '@esengine/ecs-framework'; import { MessageHub } from '@esengine/editor-core'; -import { TilemapComponent, type ITilesetData } from '../../../TilemapComponent'; +import { TilemapComponent, type ITilesetData } from '@esengine/tilemap'; import { useTilemapEditorStore } from '../../stores/TilemapEditorStore'; import { TilesetPreview } from '../TilesetPreview'; import '../../styles/TilemapEditor.css'; diff --git a/packages/tilemap/src/editor/gizmos/TilemapGizmo.ts b/packages/tilemap-editor/src/gizmos/TilemapGizmo.ts similarity index 98% rename from packages/tilemap/src/editor/gizmos/TilemapGizmo.ts rename to packages/tilemap-editor/src/gizmos/TilemapGizmo.ts index 2038578e..8f009aa8 100644 --- a/packages/tilemap/src/editor/gizmos/TilemapGizmo.ts +++ b/packages/tilemap-editor/src/gizmos/TilemapGizmo.ts @@ -11,9 +11,9 @@ import type { Entity } from '@esengine/ecs-framework'; import type { IGizmoRenderData, IRectGizmoData, IGridGizmoData, GizmoColor } from '@esengine/editor-core'; import { GizmoColors, GizmoRegistry } from '@esengine/editor-core'; -import { TransformComponent } from '@esengine/ecs-components'; -import { TilemapComponent } from '../../TilemapComponent'; -import { TilemapCollider2DComponent, TilemapColliderMode } from '../../physics/TilemapCollider2DComponent'; +import { TransformComponent } from '@esengine/engine-core'; +import { TilemapComponent } from '@esengine/tilemap'; +import { TilemapCollider2DComponent, TilemapColliderMode } from '@esengine/tilemap'; /** * Tilemap Collider Gizmo 颜色配置 diff --git a/packages/tilemap/src/editor/index.ts b/packages/tilemap-editor/src/index.ts similarity index 92% rename from packages/tilemap/src/editor/index.ts rename to packages/tilemap-editor/src/index.ts index aade1be3..c227dd47 100644 --- a/packages/tilemap/src/editor/index.ts +++ b/packages/tilemap-editor/src/index.ts @@ -28,11 +28,11 @@ import { FileActionRegistry } from '@esengine/editor-core'; import type { IDialog, IFileSystem } from '@esengine/editor-core'; -import { TransformComponent } from '@esengine/ecs-components'; +import { TransformComponent } from '@esengine/engine-core'; -// Local imports -import { TilemapComponent } from '../TilemapComponent'; -import { TilemapCollider2DComponent } from '../physics/TilemapCollider2DComponent'; +// Runtime imports from @esengine/tilemap +import { TilemapComponent, TilemapCollider2DComponent, TilemapRuntimeModule } from '@esengine/tilemap'; +import type { IPlugin, PluginDescriptor } from '@esengine/editor-core'; import { TilemapEditorPanel } from './components/panels/TilemapEditorPanel'; import { TilemapInspectorProvider } from './providers/TilemapInspectorProvider'; import { registerTilemapGizmo } from './gizmos/TilemapGizmo'; @@ -354,6 +354,33 @@ export class TilemapEditorModule implements IEditorModuleLoader { export const tilemapEditorModule = new TilemapEditorModule(); -// Plugin exports -export { TilemapPlugin } from './TilemapPlugin'; +/** + * Tilemap 插件描述符 + * Tilemap Plugin Descriptor + */ +const descriptor: PluginDescriptor = { + id: '@esengine/tilemap', + name: 'Tilemap', + version: '1.0.0', + description: 'Tilemap system with Tiled editor support', + category: 'tilemap', + enabledByDefault: false, + isEnginePlugin: true, + canContainContent: true, + modules: [ + { name: 'Runtime', type: 'runtime', loadingPhase: 'default' }, + { name: 'Editor', type: 'editor', loadingPhase: 'postDefault' } + ] +}; + +/** + * 完整的 Tilemap 插件(运行时 + 编辑器) + * Complete Tilemap Plugin (runtime + editor) + */ +export const TilemapPlugin: IPlugin = { + descriptor, + runtimeModule: new TilemapRuntimeModule(), + editorModule: tilemapEditorModule +}; + export default tilemapEditorModule; diff --git a/packages/tilemap/src/editor/providers/TilemapInspectorProvider.tsx b/packages/tilemap-editor/src/providers/TilemapInspectorProvider.tsx similarity index 98% rename from packages/tilemap/src/editor/providers/TilemapInspectorProvider.tsx rename to packages/tilemap-editor/src/providers/TilemapInspectorProvider.tsx index d1ff993c..2ea16c76 100644 --- a/packages/tilemap/src/editor/providers/TilemapInspectorProvider.tsx +++ b/packages/tilemap-editor/src/providers/TilemapInspectorProvider.tsx @@ -7,7 +7,7 @@ import { Edit3 } from 'lucide-react'; import type { IInspectorProvider, InspectorContext } from '@esengine/editor-core'; import { MessageHub } from '@esengine/editor-core'; import { Core } from '@esengine/ecs-framework'; -import type { TilemapComponent } from '../../TilemapComponent'; +import type { TilemapComponent } from '@esengine/tilemap'; interface TilemapInspectorData { entityId: string; diff --git a/packages/tilemap/src/editor/stores/TilemapEditorStore.ts b/packages/tilemap-editor/src/stores/TilemapEditorStore.ts similarity index 100% rename from packages/tilemap/src/editor/stores/TilemapEditorStore.ts rename to packages/tilemap-editor/src/stores/TilemapEditorStore.ts diff --git a/packages/tilemap/src/editor/styles/TileSetSelectorPanel.css b/packages/tilemap-editor/src/styles/TileSetSelectorPanel.css similarity index 100% rename from packages/tilemap/src/editor/styles/TileSetSelectorPanel.css rename to packages/tilemap-editor/src/styles/TileSetSelectorPanel.css diff --git a/packages/tilemap/src/editor/styles/TilemapDetailsPanel.css b/packages/tilemap-editor/src/styles/TilemapDetailsPanel.css similarity index 100% rename from packages/tilemap/src/editor/styles/TilemapDetailsPanel.css rename to packages/tilemap-editor/src/styles/TilemapDetailsPanel.css diff --git a/packages/tilemap/src/editor/styles/TilemapEditor.css b/packages/tilemap-editor/src/styles/TilemapEditor.css similarity index 100% rename from packages/tilemap/src/editor/styles/TilemapEditor.css rename to packages/tilemap-editor/src/styles/TilemapEditor.css diff --git a/packages/tilemap/src/editor/tools/BrushTool.ts b/packages/tilemap-editor/src/tools/BrushTool.ts similarity index 100% rename from packages/tilemap/src/editor/tools/BrushTool.ts rename to packages/tilemap-editor/src/tools/BrushTool.ts diff --git a/packages/tilemap/src/editor/tools/EraserTool.ts b/packages/tilemap-editor/src/tools/EraserTool.ts similarity index 100% rename from packages/tilemap/src/editor/tools/EraserTool.ts rename to packages/tilemap-editor/src/tools/EraserTool.ts diff --git a/packages/tilemap/src/editor/tools/FillTool.ts b/packages/tilemap-editor/src/tools/FillTool.ts similarity index 100% rename from packages/tilemap/src/editor/tools/FillTool.ts rename to packages/tilemap-editor/src/tools/FillTool.ts diff --git a/packages/tilemap/src/editor/tools/ITilemapTool.ts b/packages/tilemap-editor/src/tools/ITilemapTool.ts similarity index 93% rename from packages/tilemap/src/editor/tools/ITilemapTool.ts rename to packages/tilemap-editor/src/tools/ITilemapTool.ts index f628e8fe..2127aa04 100644 --- a/packages/tilemap/src/editor/tools/ITilemapTool.ts +++ b/packages/tilemap-editor/src/tools/ITilemapTool.ts @@ -2,7 +2,7 @@ * Tilemap Tool Interface */ -import type { TilemapComponent } from '../../TilemapComponent'; +import type { TilemapComponent } from '@esengine/tilemap'; import type { TileSelection } from '../stores/TilemapEditorStore'; export interface ToolContext { diff --git a/packages/tilemap-editor/tsconfig.build.json b/packages/tilemap-editor/tsconfig.build.json new file mode 100644 index 00000000..ba0684d9 --- /dev/null +++ b/packages/tilemap-editor/tsconfig.build.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ES2020", + "moduleResolution": "bundler", + "lib": ["ES2020", "DOM"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "declaration": true, + "declarationMap": true, + "jsx": "react-jsx", + "resolveJsonModule": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/tilemap-editor/tsconfig.json b/packages/tilemap-editor/tsconfig.json new file mode 100644 index 00000000..d099ddd8 --- /dev/null +++ b/packages/tilemap-editor/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src", + "declaration": true, + "jsx": "react-jsx" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/tilemap-editor/tsup.config.ts b/packages/tilemap-editor/tsup.config.ts new file mode 100644 index 00000000..b4f49f5d --- /dev/null +++ b/packages/tilemap-editor/tsup.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'tsup'; +import { editorOnlyPreset } from '../build-config/src/presets/plugin-tsup'; + +export default defineConfig({ + ...editorOnlyPreset(), + tsconfig: 'tsconfig.build.json' +}); diff --git a/packages/tilemap/package.json b/packages/tilemap/package.json index 3d17cafa..c574382b 100644 --- a/packages/tilemap/package.json +++ b/packages/tilemap/package.json @@ -1,85 +1,58 @@ { - "name": "@esengine/tilemap", - "version": "1.0.0", - "description": "Tilemap system for ECS Framework - supports Tiled editor import", - "main": "dist/index.js", - "module": "dist/index.js", - "types": "dist/index.d.ts", - "type": "module", - "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.js" + "name": "@esengine/tilemap", + "version": "1.0.0", + "description": "Tilemap system for ECS Framework - supports Tiled editor import", + "esengine": { + "plugin": true, + "pluginExport": "TilemapPlugin", + "category": "tilemap" }, - "./runtime": { - "types": "./dist/runtime.d.ts", - "import": "./dist/runtime.js" + "main": "dist/index.js", + "module": "dist/index.js", + "types": "dist/index.d.ts", + "type": "module", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } }, - "./editor": { - "types": "./dist/editor/index.d.ts", - "import": "./dist/editor/index.js" + "files": [ + "dist" + ], + "keywords": [ + "ecs", + "tilemap", + "tiled", + "game-engine", + "2d", + "typescript" + ], + "scripts": { + "clean": "rimraf dist tsconfig.tsbuildinfo", + "build": "tsup", + "build:watch": "tsup --watch", + "type-check": "tsc --noEmit", + "rebuild": "npm run clean && npm run build" }, - "./plugin.json": "./plugin.json" - }, - "files": [ - "dist", - "plugin.json" - ], - "keywords": [ - "ecs", - "tilemap", - "tiled", - "game-engine", - "2d", - "typescript" - ], - "scripts": { - "clean": "rimraf dist tsconfig.tsbuildinfo", - "build": "vite build", - "build:watch": "vite build --watch", - "type-check": "tsc --noEmit", - "rebuild": "npm run clean && npm run build" - }, - "author": "yhh", - "license": "MIT", - "devDependencies": { - "@types/react": "^18.3.12", - "@vitejs/plugin-react": "^4.7.0", - "rimraf": "^5.0.0", - "typescript": "^5.8.3", - "vite": "^6.0.7", - "vite-plugin-dts": "^3.7.0" - }, - "peerDependencies": { - "@esengine/ecs-framework": "^2.2.8", - "@esengine/asset-system": "workspace:*", - "@esengine/ecs-components": "workspace:*", - "@esengine/ecs-engine-bindgen": "workspace:*", - "@esengine/editor-core": "workspace:*", - "react": "^18.3.1", - "zustand": "^5.0.8", - "lucide-react": "^0.545.0" - }, - "peerDependenciesMeta": { - "@esengine/editor-core": { - "optional": true + "author": "yhh", + "license": "MIT", + "devDependencies": { + "@esengine/ecs-framework": "workspace:*", + "@esengine/asset-system": "workspace:*", + "@esengine/engine-core": "workspace:*", + "@esengine/ecs-engine-bindgen": "workspace:*", + "@esengine/build-config": "workspace:*", + "rimraf": "^5.0.0", + "tsup": "^8.0.0", + "typescript": "^5.8.3" }, - "react": { - "optional": true + "dependencies": { + "tslib": "^2.8.1" }, - "zustand": { - "optional": true - }, - "lucide-react": { - "optional": true + "repository": { + "type": "git", + "url": "https://github.com/esengine/ecs-framework.git", + "directory": "packages/tilemap" } - }, - "dependencies": { - "tslib": "^2.8.1" - }, - "repository": { - "type": "git", - "url": "https://github.com/esengine/ecs-framework.git", - "directory": "packages/tilemap" - } } diff --git a/packages/tilemap/src/TilemapRuntimeModule.ts b/packages/tilemap/src/TilemapRuntimeModule.ts index 4f6ff8bd..19790e3d 100644 --- a/packages/tilemap/src/TilemapRuntimeModule.ts +++ b/packages/tilemap/src/TilemapRuntimeModule.ts @@ -1,11 +1,6 @@ -/** - * Tilemap Runtime Module (Pure runtime, no editor dependencies) - * Tilemap 运行时模块(纯运行时,无编辑器依赖) - */ - import type { IScene } from '@esengine/ecs-framework'; import { ComponentRegistry } from '@esengine/ecs-framework'; -import type { IRuntimeModuleLoader, SystemContext } from '@esengine/ecs-components'; +import type { IRuntimeModule, IPlugin, PluginDescriptor, SystemContext } from '@esengine/engine-core'; import type { AssetManager } from '@esengine/asset-system'; import { TilemapComponent } from './TilemapComponent'; @@ -13,13 +8,17 @@ import { TilemapRenderingSystem } from './systems/TilemapRenderingSystem'; import { TilemapCollider2DComponent } from './physics/TilemapCollider2DComponent'; import { TilemapPhysicsSystem, type IPhysicsWorld } from './physics/TilemapPhysicsSystem'; import { TilemapLoader } from './loaders/TilemapLoader'; -import { TilemapAssetType } from './index'; +import { TilemapAssetType } from './constants'; -/** - * Tilemap Runtime Module - * Tilemap 运行时模块 - */ -export class TilemapRuntimeModule implements IRuntimeModuleLoader { +export interface TilemapSystemContext extends SystemContext { + tilemapSystem?: TilemapRenderingSystem; + tilemapPhysicsSystem?: TilemapPhysicsSystem; + physics2DWorld?: IPhysicsWorld; + assetManager?: AssetManager; + renderSystem?: any; +} + +class TilemapRuntimeModule implements IRuntimeModule { private _tilemapPhysicsSystem: TilemapPhysicsSystem | null = null; private _loaderRegistered = false; @@ -29,47 +28,54 @@ export class TilemapRuntimeModule implements IRuntimeModuleLoader { } createSystems(scene: IScene, context: SystemContext): void { - // 注册 Tilemap 加载器到 AssetManager - // Register tilemap loader to AssetManager - const assetManager = context.assetManager as AssetManager | undefined; - if (!this._loaderRegistered && assetManager) { - assetManager.registerLoader(TilemapAssetType, new TilemapLoader()); + const tilemapContext = context as TilemapSystemContext; + + if (!this._loaderRegistered && tilemapContext.assetManager) { + tilemapContext.assetManager.registerLoader(TilemapAssetType, new TilemapLoader()); this._loaderRegistered = true; } - // Tilemap rendering system const tilemapSystem = new TilemapRenderingSystem(); scene.addSystem(tilemapSystem); - if (context.renderSystem) { - context.renderSystem.addRenderDataProvider(tilemapSystem); + if (tilemapContext.renderSystem) { + tilemapContext.renderSystem.addRenderDataProvider(tilemapSystem); } - context.tilemapSystem = tilemapSystem; + tilemapContext.tilemapSystem = tilemapSystem; - // Tilemap physics system this._tilemapPhysicsSystem = new TilemapPhysicsSystem(); scene.addSystem(this._tilemapPhysicsSystem); - context.tilemapPhysicsSystem = this._tilemapPhysicsSystem; + tilemapContext.tilemapPhysicsSystem = this._tilemapPhysicsSystem; } - /** - * 所有系统创建完成后,连接跨插件依赖 - * Wire cross-plugin dependencies after all systems are created - */ onSystemsCreated(_scene: IScene, context: SystemContext): void { - // 连接物理世界(如果物理插件已加载) - // Connect physics world (if physics plugin is loaded) - if (this._tilemapPhysicsSystem && context.physics2DWorld) { - this._tilemapPhysicsSystem.setPhysicsWorld(context.physics2DWorld as IPhysicsWorld); + const tilemapContext = context as TilemapSystemContext; + + if (this._tilemapPhysicsSystem && tilemapContext.physics2DWorld) { + this._tilemapPhysicsSystem.setPhysicsWorld(tilemapContext.physics2DWorld); } } - /** - * 获取 Tilemap 物理系统 - */ get tilemapPhysicsSystem(): TilemapPhysicsSystem | null { return this._tilemapPhysicsSystem; } } + +const descriptor: PluginDescriptor = { + id: '@esengine/tilemap', + name: 'Tilemap', + version: '1.0.0', + description: 'Tilemap system with Tiled editor support', + category: 'tilemap', + enabledByDefault: false, + isEnginePlugin: true +}; + +export const TilemapPlugin: IPlugin = { + descriptor, + runtimeModule: new TilemapRuntimeModule() +}; + +export { TilemapRuntimeModule }; diff --git a/packages/tilemap/src/constants.ts b/packages/tilemap/src/constants.ts new file mode 100644 index 00000000..9bfce3c9 --- /dev/null +++ b/packages/tilemap/src/constants.ts @@ -0,0 +1,9 @@ +/** + * Tilemap Constants + * 瓦片地图常量 + */ + +// Asset type constants for tilemap +// 瓦片地图资产类型常量 +export const TilemapAssetType = 'tilemap' as const; +export const TilesetAssetType = 'tileset' as const; diff --git a/packages/tilemap/src/editor/TilemapPlugin.ts b/packages/tilemap/src/editor/TilemapPlugin.ts deleted file mode 100644 index bf0c514e..00000000 --- a/packages/tilemap/src/editor/TilemapPlugin.ts +++ /dev/null @@ -1,60 +0,0 @@ -/** - * Tilemap 统一插件 - * Tilemap Unified Plugin - * - * 整合运行时模块和编辑器模块 - * Integrates runtime and editor modules - */ - -import type { IPluginLoader, PluginDescriptor } from '@esengine/editor-core'; - -// Runtime module -import { TilemapRuntimeModule } from '../TilemapRuntimeModule'; - -// Editor imports -import { TilemapEditorModule } from './index'; - -/** - * 插件描述符 - */ -const descriptor: PluginDescriptor = { - id: '@esengine/tilemap', - name: 'Tilemap System', - version: '1.0.0', - description: '瓦片地图系统,支持 Tiled 格式导入和高效渲染', - category: 'rendering', - enabledByDefault: true, - canContainContent: true, - isEnginePlugin: true, - modules: [ - { - name: 'TilemapRuntime', - type: 'runtime', - loadingPhase: 'default', - entry: './src/index.ts' - }, - { - name: 'TilemapEditor', - type: 'editor', - loadingPhase: 'default', - entry: './src/editor/index.ts' - } - ], - dependencies: [ - { id: '@esengine/core', version: '^1.0.0' }, - { id: '@esengine/physics-rapier2d', version: '^1.0.0', optional: true } - ], - icon: 'Grid3X3' -}; - -/** - * Tilemap 插件加载器 - * Tilemap plugin loader - */ -export const TilemapPlugin: IPluginLoader = { - descriptor, - runtimeModule: new TilemapRuntimeModule(), - editorModule: new TilemapEditorModule(), -}; - -export default TilemapPlugin; diff --git a/packages/tilemap/src/index.ts b/packages/tilemap/src/index.ts index 713a99d4..49eee765 100644 --- a/packages/tilemap/src/index.ts +++ b/packages/tilemap/src/index.ts @@ -3,10 +3,8 @@ * ECS框架的瓦片地图系统 */ -// Asset type constants for tilemap -// 瓦片地图资产类型常量 -export const TilemapAssetType = 'tilemap' as const; -export const TilesetAssetType = 'tileset' as const; +// Constants +export { TilemapAssetType, TilesetAssetType } from './constants'; // Component export { TilemapComponent } from './TilemapComponent'; @@ -33,8 +31,5 @@ export type { ITilesetAsset } from './loaders/TilesetLoader'; export { TiledConverter } from './loaders/TiledConverter'; export type { ITiledMap, ITiledConversionResult } from './loaders/TiledConverter'; -// Runtime module (no editor dependencies) -export { TilemapRuntimeModule } from './TilemapRuntimeModule'; - -// Plugin (for PluginManager - includes editor dependencies) -export { TilemapPlugin } from './editor/TilemapPlugin'; +// Runtime module and plugin +export { TilemapRuntimeModule, TilemapPlugin, type TilemapSystemContext } from './TilemapRuntimeModule'; diff --git a/packages/tilemap/src/loaders/TilemapLoader.ts b/packages/tilemap/src/loaders/TilemapLoader.ts index 8da3e314..44b9714b 100644 --- a/packages/tilemap/src/loaders/TilemapLoader.ts +++ b/packages/tilemap/src/loaders/TilemapLoader.ts @@ -10,7 +10,7 @@ import { AssetLoadError, IAssetLoader } from '@esengine/asset-system'; -import { TilemapAssetType } from '../index'; +import { TilemapAssetType } from '../constants'; /** * Tilemap data interface diff --git a/packages/tilemap/src/loaders/TilesetLoader.ts b/packages/tilemap/src/loaders/TilesetLoader.ts index 7f09d950..55e92c01 100644 --- a/packages/tilemap/src/loaders/TilesetLoader.ts +++ b/packages/tilemap/src/loaders/TilesetLoader.ts @@ -10,7 +10,7 @@ import { AssetLoadError, IAssetLoader } from '@esengine/asset-system'; -import { TilesetAssetType } from '../index'; +import { TilesetAssetType } from '../constants'; /** * Tileset data interface diff --git a/packages/tilemap/src/physics/TilemapPhysicsSystem.ts b/packages/tilemap/src/physics/TilemapPhysicsSystem.ts index 4b2a1404..ed502760 100644 --- a/packages/tilemap/src/physics/TilemapPhysicsSystem.ts +++ b/packages/tilemap/src/physics/TilemapPhysicsSystem.ts @@ -7,7 +7,7 @@ */ import { EntitySystem, Matcher, ECSSystem, type Entity, type Scene } from '@esengine/ecs-framework'; -import { TransformComponent } from '@esengine/ecs-components'; +import { TransformComponent } from '@esengine/engine-core'; import { TilemapComponent } from '../TilemapComponent'; import { TilemapCollider2DComponent, type CollisionRect } from './TilemapCollider2DComponent'; diff --git a/packages/tilemap/src/runtime.ts b/packages/tilemap/src/runtime.ts deleted file mode 100644 index 68adb124..00000000 --- a/packages/tilemap/src/runtime.ts +++ /dev/null @@ -1,37 +0,0 @@ -/** - * @esengine/tilemap Runtime Entry Point - * - * This entry point exports only runtime-related code without any editor dependencies. - * Use this for standalone game runtime builds. - * - * 此入口点仅导出运行时相关代码,不包含任何编辑器依赖。 - * 用于独立游戏运行时构建。 - */ - -// Components -export { TilemapComponent } from './TilemapComponent'; -export type { ITilemapData, ITilesetData } from './TilemapComponent'; -export type { ResizeAnchor } from './TilemapComponent'; - -export { TilemapCollider2DComponent, TilemapColliderMode } from './physics/TilemapCollider2DComponent'; -export type { CollisionRect } from './physics/TilemapCollider2DComponent'; - -// Systems -export { TilemapRenderingSystem } from './systems/TilemapRenderingSystem'; -export type { TilemapRenderData, ViewportBounds } from './systems/TilemapRenderingSystem'; - -export { TilemapPhysicsSystem } from './physics/TilemapPhysicsSystem'; -export type { IPhysicsWorld, IPhysics2DSystem } from './physics/TilemapPhysicsSystem'; - -// Loaders -export { TilemapLoader } from './loaders/TilemapLoader'; -export type { ITilemapAsset } from './loaders/TilemapLoader'; -export { TilesetLoader } from './loaders/TilesetLoader'; -export type { ITilesetAsset } from './loaders/TilesetLoader'; - -// Tiled converter -export { TiledConverter } from './loaders/TiledConverter'; -export type { ITiledMap, ITiledConversionResult } from './loaders/TiledConverter'; - -// Runtime module -export { TilemapRuntimeModule } from './TilemapRuntimeModule'; diff --git a/packages/tilemap/src/systems/TilemapRenderingSystem.ts b/packages/tilemap/src/systems/TilemapRenderingSystem.ts index 5caf4922..c2226c90 100644 --- a/packages/tilemap/src/systems/TilemapRenderingSystem.ts +++ b/packages/tilemap/src/systems/TilemapRenderingSystem.ts @@ -1,5 +1,5 @@ import { EntitySystem, Matcher, ECSSystem, Entity } from '@esengine/ecs-framework'; -import { TransformComponent } from '@esengine/ecs-components'; +import { TransformComponent } from '@esengine/engine-core'; import type { IRenderDataProvider } from '@esengine/ecs-engine-bindgen'; import { TilemapComponent } from '../TilemapComponent'; diff --git a/packages/tilemap/tsconfig.build.json b/packages/tilemap/tsconfig.build.json new file mode 100644 index 00000000..ba0684d9 --- /dev/null +++ b/packages/tilemap/tsconfig.build.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ES2020", + "moduleResolution": "bundler", + "lib": ["ES2020", "DOM"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "declaration": true, + "declarationMap": true, + "jsx": "react-jsx", + "resolveJsonModule": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/tilemap/tsconfig.json b/packages/tilemap/tsconfig.json index 98f43266..73b318fa 100644 --- a/packages/tilemap/tsconfig.json +++ b/packages/tilemap/tsconfig.json @@ -50,7 +50,7 @@ "path": "../core" }, { - "path": "../components" + "path": "../engine-core" }, { "path": "../ecs-engine-bindgen" diff --git a/packages/tilemap/tsup.config.ts b/packages/tilemap/tsup.config.ts new file mode 100644 index 00000000..f704a430 --- /dev/null +++ b/packages/tilemap/tsup.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'tsup'; +import { runtimeOnlyPreset } from '../build-config/src/presets/plugin-tsup'; + +export default defineConfig({ + ...runtimeOnlyPreset(), + tsconfig: 'tsconfig.build.json' +}); diff --git a/packages/tilemap/vite.config.ts b/packages/tilemap/vite.config.ts deleted file mode 100644 index 71c14427..00000000 --- a/packages/tilemap/vite.config.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { defineConfig } from 'vite'; -import { resolve } from 'path'; -import dts from 'vite-plugin-dts'; -import react from '@vitejs/plugin-react'; - -/** - * 自定义插件:将 CSS 转换为自执行的样式注入代码 - * Custom plugin: Convert CSS to self-executing style injection code - * - * 当用户写 `import './styles.css'` 时,这个插件会: - * 1. 在构建时将 CSS 内容转换为 JS 代码 - * 2. JS 代码在模块导入时自动执行,将样式注入到 DOM - * 3. 使用唯一 ID 防止重复注入 - * - * When user writes `import './styles.css'`, this plugin will: - * 1. Convert CSS content to JS code during build - * 2. JS code auto-executes when module is imported, injecting styles to DOM - * 3. Uses unique ID to prevent duplicate injection - */ -function injectCSSPlugin(): any { - const cssIdMap = new Map(); - let cssCounter = 0; - - return { - name: 'inject-css-plugin', - enforce: 'post' as const, - generateBundle(_options: any, bundle: any) { - const bundleKeys = Object.keys(bundle); - - // 找到所有 CSS 文件 - const cssFiles = bundleKeys.filter(key => key.endsWith('.css')); - - for (const cssFile of cssFiles) { - const cssChunk = bundle[cssFile]; - if (!cssChunk || !cssChunk.source) continue; - - const cssContent = cssChunk.source; - const styleId = `esengine-tilemap-style-${cssCounter++}`; - cssIdMap.set(cssFile, styleId); - - // 生成样式注入代码 - const injectCode = `(function(){if(typeof document!=='undefined'){var s=document.createElement('style');s.id='${styleId}';if(!document.getElementById(s.id)){s.textContent=${JSON.stringify(cssContent)};document.head.appendChild(s);}}})();`; - - // 找到引用此 CSS 的 JS chunk 并注入代码 - for (const jsKey of bundleKeys) { - if (!jsKey.endsWith('.js')) continue; - const jsChunk = bundle[jsKey]; - if (!jsChunk || jsChunk.type !== 'chunk' || !jsChunk.code) continue; - - // 检查是否引用了这个 CSS(通过检查是否有相关的 import) - // 对于 vite 生成的代码,CSS 导入会被转换,所以我们直接注入到 editor/index.js - if (jsKey === 'editor/index.js' || jsKey.match(/^index-[^/]+\.js$/)) { - jsChunk.code = injectCode + '\n' + jsChunk.code; - } - } - - // 删除独立的 CSS 文件 - delete bundle[cssFile]; - } - } - }; -} - -export default defineConfig({ - plugins: [ - react(), - dts({ - include: ['src'], - outDir: 'dist', - rollupTypes: false - }), - injectCSSPlugin() - ], - esbuild: { - jsx: 'automatic', - }, - build: { - lib: { - entry: { - index: resolve(__dirname, 'src/index.ts'), - runtime: resolve(__dirname, 'src/runtime.ts'), - 'editor/index': resolve(__dirname, 'src/editor/index.ts') - }, - formats: ['es'], - fileName: (format, entryName) => `${entryName}.js` - }, - rollupOptions: { - external: [ - '@esengine/ecs-framework', - '@esengine/ecs-components', - '@esengine/ecs-engine-bindgen', - '@esengine/asset-system', - '@esengine/editor-core', - 'react', - 'react/jsx-runtime', - 'lucide-react', - 'zustand', - /^@esengine\//, - /^@tauri-apps\// - ], - output: { - exports: 'named', - preserveModules: false - } - }, - target: 'es2020', - minify: false, - sourcemap: true - } -}); diff --git a/packages/ui-editor/package.json b/packages/ui-editor/package.json new file mode 100644 index 00000000..d123d385 --- /dev/null +++ b/packages/ui-editor/package.json @@ -0,0 +1,47 @@ +{ + "name": "@esengine/ui-editor", + "version": "1.0.0", + "description": "Editor support for @esengine/ui - inspectors, gizmos, and entity templates", + "main": "dist/index.js", + "module": "dist/index.js", + "types": "dist/index.d.ts", + "type": "module", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + }, + "./plugin.json": "./plugin.json" + }, + "files": [ + "dist", + "plugin.json" + ], + "scripts": { + "build": "tsup", + "build:watch": "tsup --watch", + "type-check": "tsc --noEmit", + "clean": "rimraf dist" + }, + "dependencies": { + "@esengine/ui": "workspace:*" + }, + "devDependencies": { + "@esengine/ecs-framework": "workspace:*", + "@esengine/editor-core": "workspace:*", + "@esengine/build-config": "workspace:*", + "lucide-react": "^0.545.0", + "react": "^18.3.1", + "@types/react": "^18.3.12", + "rimraf": "^5.0.5", + "tsup": "^8.0.0", + "typescript": "^5.3.3" + }, + "keywords": [ + "ecs", + "ui", + "editor" + ], + "author": "", + "license": "MIT" +} diff --git a/packages/ui/src/editor/gizmos/UITransformGizmo.ts b/packages/ui-editor/src/gizmos/UITransformGizmo.ts similarity index 59% rename from packages/ui/src/editor/gizmos/UITransformGizmo.ts rename to packages/ui-editor/src/gizmos/UITransformGizmo.ts index 20be2b2c..73a66643 100644 --- a/packages/ui/src/editor/gizmos/UITransformGizmo.ts +++ b/packages/ui-editor/src/gizmos/UITransformGizmo.ts @@ -1,7 +1,7 @@ import type { Entity } from '@esengine/ecs-framework'; import type { IGizmoRenderData, IRectGizmoData, GizmoColor } from '@esengine/editor-core'; import { GizmoRegistry } from '@esengine/editor-core'; -import { UITransformComponent } from '../../components'; +import { UITransformComponent } from '@esengine/ui'; const UI_GIZMO_COLOR: GizmoColor = { r: 0.2, g: 0.6, b: 1, a: 0.8 }; const UI_GIZMO_COLOR_UNSELECTED: GizmoColor = { r: 0.2, g: 0.6, b: 1, a: 0.3 }; @@ -21,24 +21,33 @@ function uiTransformGizmoProvider( // 否则回退到本地坐标 const x = transform.worldX ?? transform.x; const y = transform.worldY ?? transform.y; - const width = (transform.computedWidth ?? transform.width) * transform.scaleX; - const height = (transform.computedHeight ?? transform.height) * transform.scaleY; + // Use world scale for proper hierarchical transform inheritance + // 使用世界缩放以正确继承层级变换 + const scaleX = transform.worldScaleX ?? transform.scaleX; + const scaleY = transform.worldScaleY ?? transform.scaleY; + const width = (transform.computedWidth ?? transform.width) * scaleX; + const height = (transform.computedHeight ?? transform.height) * scaleY; + // Use world rotation for proper hierarchical transform inheritance + // 使用世界旋转以正确继承层级变换 + const rotation = transform.worldRotation ?? transform.rotation; + // 使用 transform 的 pivot 作为旋转/缩放中心 + const pivotX = transform.pivotX; + const pivotY = transform.pivotY; + // 渲染位置 = 左下角 + pivot 偏移 + const renderX = x + width * pivotX; + const renderY = y + height * pivotY; - // Use bottom-left position with origin at (0, 0) - // x, y is bottom-left corner in UITransform coordinate system (Y-up) - // This matches Gizmo origin=(0,0) which means reference point is at bottom-left - // 使用左下角位置,原点在 (0, 0) - // UITransform 坐标系中 x, y 是左下角(Y 向上) - // 这与 Gizmo origin=(0,0) 匹配,表示参考点在左下角 + // Use pivot position with transform's pivot values as origin + // 使用 transform 的 pivot 值作为 gizmo 的原点 const gizmo: IRectGizmoData = { type: 'rect', - x, - y, + x: renderX, + y: renderY, width, height, - rotation: transform.rotation, - originX: 0, - originY: 0, + rotation, + originX: pivotX, + originY: pivotY, color: isSelected ? UI_GIZMO_COLOR : UI_GIZMO_COLOR_UNSELECTED, showHandles: isSelected }; diff --git a/packages/ui/src/editor/gizmos/index.ts b/packages/ui-editor/src/gizmos/index.ts similarity index 100% rename from packages/ui/src/editor/gizmos/index.ts rename to packages/ui-editor/src/gizmos/index.ts diff --git a/packages/ui/src/editor/index.ts b/packages/ui-editor/src/index.ts similarity index 94% rename from packages/ui/src/editor/index.ts rename to packages/ui-editor/src/index.ts index c63faf57..d1869b85 100644 --- a/packages/ui/src/editor/index.ts +++ b/packages/ui-editor/src/index.ts @@ -1,18 +1,15 @@ /** - * UI 编辑器模块入口 - * UI Editor Module Entry + * @esengine/ui-editor + * + * Editor support for @esengine/ui - inspectors, gizmos, and entity templates + * UI 编辑器支持 - 检视器、Gizmo 和实体模板 */ -import React from 'react'; -import { LayoutGrid, Square, Type, MousePointer2, Sliders, BarChart3, ScrollText, PanelTop } from 'lucide-react'; import type { ServiceContainer, Entity } from '@esengine/ecs-framework'; import { Core } from '@esengine/ecs-framework'; import type { IEditorModuleLoader, - PanelDescriptor, - EntityCreationTemplate, - ComponentAction, - ComponentInspectorProviderDef + EntityCreationTemplate } from '@esengine/editor-core'; import { EntityStoreService, @@ -21,7 +18,7 @@ import { ComponentInspectorRegistry } from '@esengine/editor-core'; -// Local imports +// Runtime imports from @esengine/ui import { UITransformComponent, UIRenderComponent, @@ -35,7 +32,7 @@ import { UIProgressBarComponent, UISliderComponent, UIScrollViewComponent -} from '../components'; +} from '@esengine/ui'; import { UITransformInspector } from './inspectors'; import { registerUITransformGizmo, unregisterUITransformGizmo } from './gizmos'; @@ -405,6 +402,27 @@ export class UIEditorModule implements IEditorModuleLoader { export const uiEditorModule = new UIEditorModule(); -// Plugin exports -export { UIPlugin, UIRuntimeModule } from './UIPlugin'; +// 从 @esengine/ui 导入运行时模块 +import { UIRuntimeModule } from '@esengine/ui'; +import type { IPlugin, PluginDescriptor } from '@esengine/editor-core'; + +const descriptor: PluginDescriptor = { + id: '@esengine/ui', + name: 'UI', + version: '1.0.0', + description: 'ECS-based UI system with editor support', + category: 'ui', + enabledByDefault: true +}; + +/** + * 完整的 UI 插件(运行时 + 编辑器) + * Complete UI Plugin (runtime + editor) + */ +export const UIPlugin: IPlugin = { + descriptor, + runtimeModule: new UIRuntimeModule(), + editorModule: uiEditorModule +}; + export default uiEditorModule; diff --git a/packages/ui/src/editor/inspectors/UITransformInspector.tsx b/packages/ui-editor/src/inspectors/UITransformInspector.tsx similarity index 99% rename from packages/ui/src/editor/inspectors/UITransformInspector.tsx rename to packages/ui-editor/src/inspectors/UITransformInspector.tsx index ef20666f..6908a945 100644 --- a/packages/ui/src/editor/inspectors/UITransformInspector.tsx +++ b/packages/ui-editor/src/inspectors/UITransformInspector.tsx @@ -1,7 +1,7 @@ import React, { useState, useEffect, useRef } from 'react'; import { Component } from '@esengine/ecs-framework'; import type { IComponentInspector, ComponentInspectorContext } from '@esengine/editor-core'; -import { UITransformComponent, AnchorPreset } from '../../components'; +import { UITransformComponent, AnchorPreset } from '@esengine/ui'; const DraggableNumberInput: React.FC<{ axis?: 'x' | 'y' | 'z' | 'w'; diff --git a/packages/ui/src/editor/inspectors/index.ts b/packages/ui-editor/src/inspectors/index.ts similarity index 100% rename from packages/ui/src/editor/inspectors/index.ts rename to packages/ui-editor/src/inspectors/index.ts diff --git a/packages/ui-editor/tsconfig.build.json b/packages/ui-editor/tsconfig.build.json new file mode 100644 index 00000000..ba0684d9 --- /dev/null +++ b/packages/ui-editor/tsconfig.build.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ES2020", + "moduleResolution": "bundler", + "lib": ["ES2020", "DOM"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "declaration": true, + "declarationMap": true, + "jsx": "react-jsx", + "resolveJsonModule": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/ui-editor/tsconfig.json b/packages/ui-editor/tsconfig.json new file mode 100644 index 00000000..d099ddd8 --- /dev/null +++ b/packages/ui-editor/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src", + "declaration": true, + "jsx": "react-jsx" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/ui-editor/tsup.config.ts b/packages/ui-editor/tsup.config.ts new file mode 100644 index 00000000..b4f49f5d --- /dev/null +++ b/packages/ui-editor/tsup.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'tsup'; +import { editorOnlyPreset } from '../build-config/src/presets/plugin-tsup'; + +export default defineConfig({ + ...editorOnlyPreset(), + tsconfig: 'tsconfig.build.json' +}); diff --git a/packages/ui/package.json b/packages/ui/package.json index 31aa6f76..6437cf84 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -2,6 +2,11 @@ "name": "@esengine/ui", "version": "1.0.0", "description": "ECS-based UI system with WebGL rendering for games", + "esengine": { + "plugin": true, + "pluginExport": "UIPlugin", + "category": "ui" + }, "main": "dist/index.js", "module": "dist/index.js", "types": "dist/index.d.ts", @@ -10,52 +15,24 @@ ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" - }, - "./runtime": { - "types": "./dist/runtime.d.ts", - "import": "./dist/runtime.js" - }, - "./editor": { - "types": "./dist/editor/index.d.ts", - "import": "./dist/editor/index.js" - }, - "./plugin.json": "./plugin.json" + } }, "files": [ - "dist", - "plugin.json" + "dist" ], "scripts": { - "build": "vite build", - "build:watch": "vite build --watch", + "build": "tsup", + "build:watch": "tsup --watch", "type-check": "tsc --noEmit", "clean": "rimraf dist" }, - "peerDependencies": { - "@esengine/ecs-framework": ">=2.0.0", - "@esengine/ecs-components": "workspace:*", - "@esengine/editor-core": "workspace:*", - "lucide-react": "^0.545.0", - "react": "^18.3.1" - }, - "peerDependenciesMeta": { - "@esengine/editor-core": { - "optional": true - }, - "react": { - "optional": true - }, - "lucide-react": { - "optional": true - } - }, "devDependencies": { - "@types/react": "^18.3.12", - "@vitejs/plugin-react": "^4.7.0", + "@esengine/ecs-framework": "workspace:*", + "@esengine/engine-core": "workspace:*", + "@esengine/build-config": "workspace:*", "rimraf": "^5.0.5", - "typescript": "^5.3.3", - "vite": "^5.0.0", - "vite-plugin-dts": "^3.7.0" + "tsup": "^8.0.0", + "typescript": "^5.3.3" }, "keywords": [ "ecs", diff --git a/packages/ui/src/UIBuilder.ts b/packages/ui/src/UIBuilder.ts index ed2b735a..a45caa45 100644 --- a/packages/ui/src/UIBuilder.ts +++ b/packages/ui/src/UIBuilder.ts @@ -1,4 +1,4 @@ -import { Entity, Scene } from '@esengine/ecs-framework'; +import { Entity, Scene, HierarchySystem, HierarchyComponent } from '@esengine/ecs-framework'; import { UITransformComponent, AnchorPreset } from './components/UITransformComponent'; import { UIRenderComponent, UIRenderType } from './components/UIRenderComponent'; import { UIInteractableComponent } from './components/UIInteractableComponent'; @@ -145,6 +145,9 @@ export class UIBuilder { private createBase(config: UIBaseConfig, defaultName: string): Entity { const entity = this.scene.createEntity(config.name ?? `${defaultName}_${this.idCounter++}`); + // 添加 HierarchyComponent 支持层级结构 + entity.addComponent(new HierarchyComponent()); + const transform = entity.addComponent(new UITransformComponent()); transform.x = config.x ?? 0; transform.y = config.y ?? 0; @@ -419,7 +422,10 @@ export class UIBuilder { * Add child to parent */ public addChild(parent: Entity, child: Entity): Entity { - parent.addChild(child); + const hierarchySystem = this.scene.getSystem(HierarchySystem); + if (hierarchySystem) { + hierarchySystem.setParent(child, parent); + } return child; } @@ -428,8 +434,11 @@ export class UIBuilder { * Add multiple children to parent */ public addChildren(parent: Entity, children: Entity[]): Entity[] { - for (const child of children) { - parent.addChild(child); + const hierarchySystem = this.scene.getSystem(HierarchySystem); + if (hierarchySystem) { + for (const child of children) { + hierarchySystem.setParent(child, parent); + } } return children; } diff --git a/packages/ui/src/UIRuntimeModule.ts b/packages/ui/src/UIRuntimeModule.ts index 2aa07771..2d8bfba5 100644 --- a/packages/ui/src/UIRuntimeModule.ts +++ b/packages/ui/src/UIRuntimeModule.ts @@ -1,11 +1,6 @@ -/** - * UI Runtime Module (Pure runtime, no editor dependencies) - * UI 运行时模块(纯运行时,无编辑器依赖) - */ - import type { IScene } from '@esengine/ecs-framework'; import { ComponentRegistry } from '@esengine/ecs-framework'; -import type { IRuntimeModuleLoader, SystemContext } from '@esengine/ecs-components'; +import type { IRuntimeModule, IPlugin, PluginDescriptor, SystemContext } from '@esengine/engine-core'; import { UITransformComponent, @@ -32,11 +27,14 @@ import { UIScrollViewRenderSystem } from './systems/render'; -/** - * UI Runtime Module - * UI 运行时模块 - */ -export class UIRuntimeModule implements IRuntimeModuleLoader { +export interface UISystemContext extends SystemContext { + uiLayoutSystem?: UILayoutSystem; + uiRenderProvider?: UIRenderDataProvider; + uiInputSystem?: UIInputSystem; + uiTextRenderSystem?: UITextRenderSystem; +} + +class UIRuntimeModule implements IRuntimeModule { registerComponents(registry: typeof ComponentRegistry): void { registry.register(UITransformComponent); registry.register(UIRenderComponent); @@ -50,6 +48,8 @@ export class UIRuntimeModule implements IRuntimeModuleLoader { } createSystems(scene: IScene, context: SystemContext): void { + const uiContext = context as UISystemContext; + const layoutSystem = new UILayoutSystem(); scene.addSystem(layoutSystem); @@ -77,9 +77,9 @@ export class UIRuntimeModule implements IRuntimeModuleLoader { const textRenderSystem = new UITextRenderSystem(); scene.addSystem(textRenderSystem); - if (context.engineBridge) { + if (uiContext.engineBridge) { textRenderSystem.setTextureCallback((id: number, dataUrl: string) => { - context.engineBridge.loadTexture(id, dataUrl); + uiContext.engineBridge.loadTexture(id, dataUrl); }); } @@ -88,9 +88,26 @@ export class UIRuntimeModule implements IRuntimeModuleLoader { inputSystem.setLayoutSystem(layoutSystem); scene.addSystem(inputSystem); - context.uiLayoutSystem = layoutSystem; - context.uiRenderProvider = uiRenderProvider; - context.uiInputSystem = inputSystem; - context.uiTextRenderSystem = textRenderSystem; + uiContext.uiLayoutSystem = layoutSystem; + uiContext.uiRenderProvider = uiRenderProvider; + uiContext.uiInputSystem = inputSystem; + uiContext.uiTextRenderSystem = textRenderSystem; } } + +const descriptor: PluginDescriptor = { + id: '@esengine/ui', + name: 'UI', + version: '1.0.0', + description: 'ECS-based UI system', + category: 'ui', + enabledByDefault: true, + isEnginePlugin: true +}; + +export const UIPlugin: IPlugin = { + descriptor, + runtimeModule: new UIRuntimeModule() +}; + +export { UIRuntimeModule }; diff --git a/packages/ui/src/components/UITransformComponent.ts b/packages/ui/src/components/UITransformComponent.ts index 04cfedd0..90c15a8b 100644 --- a/packages/ui/src/components/UITransformComponent.ts +++ b/packages/ui/src/components/UITransformComponent.ts @@ -232,6 +232,32 @@ export class UITransformComponent extends Component { */ public worldAlpha: number = 1; + /** + * 计算后的世界旋转(弧度,考虑父元素旋转) + * Computed world rotation in radians (considering parent rotation) + */ + public worldRotation: number = 0; + + /** + * 计算后的世界 X 缩放(考虑父元素缩放) + * Computed world X scale (considering parent scale) + */ + public worldScaleX: number = 1; + + /** + * 计算后的世界 Y 缩放(考虑父元素缩放) + * Computed world Y scale (considering parent scale) + */ + public worldScaleY: number = 1; + + /** + * 本地到世界的 2D 变换矩阵(只读,由 UILayoutSystem 计算) + * Local to world 2D transformation matrix (readonly, computed by UILayoutSystem) + */ + public localToWorldMatrix: { a: number; b: number; c: number; d: number; tx: number; ty: number } = { + a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0 + }; + /** * 布局是否需要更新 * Flag indicating layout needs update diff --git a/packages/ui/src/editor/UIPlugin.ts b/packages/ui/src/editor/UIPlugin.ts deleted file mode 100644 index c98179e5..00000000 --- a/packages/ui/src/editor/UIPlugin.ts +++ /dev/null @@ -1,159 +0,0 @@ -/** - * UI 统一插件 - * UI Unified Plugin - */ - -import type { IScene, ServiceContainer } from '@esengine/ecs-framework'; -import { ComponentRegistry } from '@esengine/ecs-framework'; -import type { - IPluginLoader, - IRuntimeModuleLoader, - PluginDescriptor, - SystemContext -} from '@esengine/editor-core'; - -// Editor imports -import { UIEditorModule } from './index'; - -// Runtime imports -import { - UITransformComponent, - UIRenderComponent, - UIInteractableComponent, - UITextComponent, - UILayoutComponent, - UIButtonComponent, - UIProgressBarComponent, - UISliderComponent, - UIScrollViewComponent -} from '../components'; -import { UILayoutSystem } from '../systems/UILayoutSystem'; -import { UIInputSystem } from '../systems/UIInputSystem'; -import { UIRenderDataProvider } from '../systems/UIRenderDataProvider'; -// Render systems -import { - UIRenderBeginSystem, - UIRectRenderSystem, - UITextRenderSystem, - UIButtonRenderSystem, - UIProgressBarRenderSystem, - UISliderRenderSystem, - UIScrollViewRenderSystem -} from '../systems/render'; - -/** - * 插件描述符 - */ -const descriptor: PluginDescriptor = { - id: '@esengine/ui', - name: 'UI System', - version: '1.0.0', - description: '游戏 UI 系统,支持布局、交互、动画等', - category: 'ui', - enabledByDefault: true, - canContainContent: false, - isEnginePlugin: true, - modules: [ - { - name: 'UIRuntime', - type: 'runtime', - loadingPhase: 'default', - entry: './src/index.ts' - }, - { - name: 'UIEditor', - type: 'editor', - loadingPhase: 'default', - entry: './src/editor/index.ts' - } - ], - dependencies: [ - { id: '@esengine/core', version: '^1.0.0' } - ], - icon: 'LayoutGrid' -}; - -/** - * UI 运行时模块 - * UI runtime module - */ -export class UIRuntimeModule implements IRuntimeModuleLoader { - registerComponents(registry: typeof ComponentRegistry): void { - registry.register(UITransformComponent); - registry.register(UIRenderComponent); - registry.register(UIInteractableComponent); - registry.register(UITextComponent); - registry.register(UILayoutComponent); - registry.register(UIButtonComponent); - registry.register(UIProgressBarComponent); - registry.register(UISliderComponent); - registry.register(UIScrollViewComponent); - } - - createSystems(scene: IScene, context: SystemContext): void { - // UI Layout System (order: 50) - const layoutSystem = new UILayoutSystem(); - scene.addSystem(layoutSystem); - - // UI Render Begin System - clears collector at start of frame (order: 99) - const renderBeginSystem = new UIRenderBeginSystem(); - scene.addSystem(renderBeginSystem); - - // UI Render Systems - collect render data (order: 100-120) - const rectRenderSystem = new UIRectRenderSystem(); - scene.addSystem(rectRenderSystem); - - const progressBarRenderSystem = new UIProgressBarRenderSystem(); - scene.addSystem(progressBarRenderSystem); - - const sliderRenderSystem = new UISliderRenderSystem(); - scene.addSystem(sliderRenderSystem); - - const scrollViewRenderSystem = new UIScrollViewRenderSystem(); - scene.addSystem(scrollViewRenderSystem); - - const buttonRenderSystem = new UIButtonRenderSystem(); - scene.addSystem(buttonRenderSystem); - - const textRenderSystem = new UITextRenderSystem(); - scene.addSystem(textRenderSystem); - - // Set up text texture callback to register textures with engine - // 设置文本纹理回调以将纹理注册到引擎 - if (context.engineBridge) { - textRenderSystem.setTextureCallback((id: number, dataUrl: string) => { - // Load data URL as texture - context.engineBridge.loadTexture(id, dataUrl); - }); - } - - // UI Render Data Provider (not a system, just a provider) - // Note: Don't call addRenderDataProvider here - UI provider should be set via - // setUIRenderDataProvider for proper preview mode support - // 注意:不要在这里调用 addRenderDataProvider - UI 提供者应该通过 - // setUIRenderDataProvider 设置以支持预览模式 - const uiRenderProvider = new UIRenderDataProvider(); - - // UI Input System - const inputSystem = new UIInputSystem(); - scene.addSystem(inputSystem); - - // 保存引用 | Save references - context.uiLayoutSystem = layoutSystem; - context.uiRenderProvider = uiRenderProvider; - context.uiInputSystem = inputSystem; - context.uiTextRenderSystem = textRenderSystem; - } -} - -/** - * UI 插件加载器 - * UI plugin loader - */ -export const UIPlugin: IPluginLoader = { - descriptor, - runtimeModule: new UIRuntimeModule(), - editorModule: new UIEditorModule(), -}; - -export default UIPlugin; diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts index f568f87b..5cf2be07 100644 --- a/packages/ui/src/index.ts +++ b/packages/ui/src/index.ts @@ -161,8 +161,5 @@ export { type UIScrollViewConfig } from './UIBuilder'; -// Runtime module (no editor dependencies) -export { UIRuntimeModule } from './UIRuntimeModule'; - -// Plugin (for PluginManager - includes editor dependencies) -export { UIPlugin } from './editor/UIPlugin'; +// Runtime module and plugin +export { UIRuntimeModule, UIPlugin, type UISystemContext } from './UIRuntimeModule'; diff --git a/packages/ui/src/runtime.ts b/packages/ui/src/runtime.ts deleted file mode 100644 index 924846d5..00000000 --- a/packages/ui/src/runtime.ts +++ /dev/null @@ -1,111 +0,0 @@ -/** - * @esengine/ui Runtime Entry Point - * - * This entry point exports only runtime-related code without any editor dependencies. - * Use this for standalone game runtime builds. - * - * 此入口点仅导出运行时相关代码,不包含任何编辑器依赖。 - * 用于独立游戏运行时构建。 - */ - -// Components - Core -export { - UITransformComponent, - AnchorPreset -} from './components/UITransformComponent'; - -export { - UIRenderComponent, - UIRenderType, - type UIBorderStyle, - type UIShadowStyle -} from './components/UIRenderComponent'; - -export { - UIInteractableComponent, - type UICursorType -} from './components/UIInteractableComponent'; - -export { - UITextComponent, - type UITextAlign, - type UITextVerticalAlign, - type UITextOverflow, - type UIFontWeight -} from './components/UITextComponent'; - -export { - UILayoutComponent, - UILayoutType, - UIJustifyContent, - UIAlignItems, - type UIPadding -} from './components/UILayoutComponent'; - -// Components - Widgets -export { - UIButtonComponent, - type UIButtonStyle, - type UIButtonDisplayMode -} from './components/widgets/UIButtonComponent'; - -export { - UIProgressBarComponent, - UIProgressDirection, - UIProgressFillMode -} from './components/widgets/UIProgressBarComponent'; - -export { - UISliderComponent, - UISliderOrientation -} from './components/widgets/UISliderComponent'; - -export { - UIScrollViewComponent, - UIScrollbarVisibility -} from './components/widgets/UIScrollViewComponent'; - -// Systems - Core -export { UILayoutSystem } from './systems/UILayoutSystem'; -export { UIInputSystem, MouseButton, type UIInputEvent } from './systems/UIInputSystem'; -export { UIAnimationSystem, Easing, type EasingFunction, type EasingName } from './systems/UIAnimationSystem'; -export { UIRenderDataProvider, type IRenderDataProvider, type IUIRenderDataProvider } from './systems/UIRenderDataProvider'; - -// Systems - Render (ECS-compliant render systems) -export { - // Collector - UIRenderCollector, - getUIRenderCollector, - resetUIRenderCollector, - invalidateUIRenderCaches, - type UIRenderPrimitive, - type ProviderRenderData, - // Render systems - UIRenderBeginSystem, - UIRectRenderSystem, - UITextRenderSystem, - UIButtonRenderSystem, - UIProgressBarRenderSystem, - UISliderRenderSystem, - UIScrollViewRenderSystem -} from './systems/render'; - -// Rendering -export { WebGLUIRenderer } from './rendering/WebGLUIRenderer'; -export { TextRenderer, type TextMeasurement, type TextRenderOptions } from './rendering/TextRenderer'; - -// Builder API -export { - UIBuilder, - type UIBaseConfig, - type UIButtonConfig, - type UITextConfig, - type UIImageConfig, - type UIProgressBarConfig, - type UISliderConfig, - type UIPanelConfig, - type UIScrollViewConfig -} from './UIBuilder'; - -// Runtime module -export { UIRuntimeModule } from './UIRuntimeModule'; diff --git a/packages/ui/src/systems/UILayoutSystem.ts b/packages/ui/src/systems/UILayoutSystem.ts index 4c3c5b3c..8d81c2bd 100644 --- a/packages/ui/src/systems/UILayoutSystem.ts +++ b/packages/ui/src/systems/UILayoutSystem.ts @@ -1,7 +1,20 @@ -import { EntitySystem, Matcher, Entity, ECSSystem } from '@esengine/ecs-framework'; +import { EntitySystem, Matcher, Entity, ECSSystem, HierarchyComponent } from '@esengine/ecs-framework'; import { UITransformComponent } from '../components/UITransformComponent'; import { UILayoutComponent, UILayoutType, UIJustifyContent, UIAlignItems } from '../components/UILayoutComponent'; +/** + * 2D 变换矩阵类型 + * 2D transformation matrix type + */ +interface Matrix2D { + a: number; // scaleX * cos(rotation) + b: number; // scaleX * sin(rotation) + c: number; // scaleY * -sin(rotation) + d: number; // scaleY * cos(rotation) + tx: number; // translateX + ty: number; // translateY +} + /** * UI 布局系统 * UI Layout System - Computes layout for UI elements @@ -9,6 +22,9 @@ import { UILayoutComponent, UILayoutType, UIJustifyContent, UIAlignItems } from * 计算 UI 元素的世界坐标和尺寸 * Computes world coordinates and sizes for UI elements * + * 使用矩阵乘法计算世界变换:worldMatrix = parentMatrix * localMatrix + * Uses matrix multiplication for world transforms: worldMatrix = parentMatrix * localMatrix + * * 注意:canvasWidth/canvasHeight 是 UI 设计的参考尺寸,不是实际渲染视口大小 * Note: canvasWidth/canvasHeight is the UI design reference size, not the actual render viewport size */ @@ -60,7 +76,14 @@ export class UILayoutSystem extends EntitySystem { protected process(entities: readonly Entity[]): void { // 首先处理根元素(没有父元素的) - const rootEntities = entities.filter(e => !e.parent || !e.parent.hasComponent(UITransformComponent)); + const rootEntities = entities.filter(e => { + const hierarchy = e.getComponent(HierarchyComponent); + if (!hierarchy || hierarchy.parentId === null) { + return true; + } + const parent = this.scene?.findEntityById(hierarchy.parentId); + return !parent || !parent.hasComponent(UITransformComponent); + }); // 画布中心为原点,Y 轴向上为正 // Canvas center is origin, Y axis points up @@ -69,8 +92,11 @@ export class UILayoutSystem extends EntitySystem { const parentX = -this.canvasWidth / 2; const parentY = this.canvasHeight / 2; // Y 轴向上,所以顶部是正值 + // 根元素使用单位矩阵作为父矩阵 + const identityMatrix: Matrix2D = { a: 1, b: 0, c: 0, d: 1, tx: 0, ty: 0 }; + for (const entity of rootEntities) { - this.layoutEntity(entity, parentX, parentY, this.canvasWidth, this.canvasHeight, 1); + this.layoutEntity(entity, parentX, parentY, this.canvasWidth, this.canvasHeight, 1, identityMatrix); } } @@ -84,7 +110,8 @@ export class UILayoutSystem extends EntitySystem { parentY: number, parentWidth: number, parentHeight: number, - parentAlpha: number + parentAlpha: number, + parentMatrix: Matrix2D ): void { const transform = entity.getComponent(UITransformComponent); if (!transform) return; @@ -160,19 +187,23 @@ export class UILayoutSystem extends EntitySystem { worldY = anchorMaxY - transform.y; } - // 更新计算后的值 + // 更新布局计算的值 transform.worldX = worldX; transform.worldY = worldY; transform.computedWidth = width; transform.computedHeight = height; transform.worldAlpha = parentAlpha * transform.alpha; + + // 使用矩阵乘法计算世界变换 + this.updateWorldMatrix(transform, parentMatrix); + transform.layoutDirty = false; // 如果元素不可见,跳过子元素 if (!transform.visible) return; // 处理子元素布局 - const children = entity.children.filter(c => c.hasComponent(UITransformComponent)); + const children = this.getUIChildren(entity); if (children.length === 0) return; // 计算子元素的父容器边界 @@ -192,7 +223,8 @@ export class UILayoutSystem extends EntitySystem { childParentY, width, height, - transform.worldAlpha + transform.worldAlpha, + transform.localToWorldMatrix ); } } @@ -234,7 +266,8 @@ export class UILayoutSystem extends EntitySystem { parentTopY, parentTransform.computedWidth, parentTransform.computedHeight, - parentTransform.worldAlpha + parentTransform.worldAlpha, + parentTransform.localToWorldMatrix ); } } @@ -328,6 +361,8 @@ export class UILayoutSystem extends EntitySystem { childTransform.computedWidth = size.width; childTransform.computedHeight = childHeight; childTransform.worldAlpha = parentTransform.worldAlpha * childTransform.alpha; + // 使用矩阵乘法计算世界旋转和缩放 + this.updateWorldMatrix(childTransform, parentTransform.localToWorldMatrix); childTransform.layoutDirty = false; // 递归处理子元素的子元素 @@ -424,6 +459,8 @@ export class UILayoutSystem extends EntitySystem { childTransform.computedWidth = childWidth; childTransform.computedHeight = size.height; childTransform.worldAlpha = parentTransform.worldAlpha * childTransform.alpha; + // 使用矩阵乘法计算世界旋转和缩放 + this.updateWorldMatrix(childTransform, parentTransform.localToWorldMatrix); childTransform.layoutDirty = false; this.processChildrenRecursive(child, childTransform); @@ -478,18 +515,49 @@ export class UILayoutSystem extends EntitySystem { childTransform.computedWidth = cellWidth; childTransform.computedHeight = cellHeight; childTransform.worldAlpha = parentTransform.worldAlpha * childTransform.alpha; + // 使用矩阵乘法计算世界旋转和缩放 + this.updateWorldMatrix(childTransform, parentTransform.localToWorldMatrix); childTransform.layoutDirty = false; this.processChildrenRecursive(child, childTransform); } } + /** + * 获取具有 UITransformComponent 的子实体 + * Get child entities that have UITransformComponent + * + * 优先使用 HierarchyComponent,如果没有则返回空数组 + */ + private getUIChildren(entity: Entity): Entity[] { + const hierarchy = entity.getComponent(HierarchyComponent); + + // 如果没有 HierarchyComponent,返回空数组 + // UI 实体应该通过 UIBuilder 创建,会自动添加 HierarchyComponent + if (!hierarchy) { + return []; + } + + if (hierarchy.childIds.length === 0) { + return []; + } + + const children: Entity[] = []; + for (const childId of hierarchy.childIds) { + const child = this.scene?.findEntityById(childId); + if (child && child.hasComponent(UITransformComponent)) { + children.push(child); + } + } + return children; + } + /** * 递归处理子元素 * Recursively process children */ private processChildrenRecursive(entity: Entity, parentTransform: UITransformComponent): void { - const children = entity.children.filter(c => c.hasComponent(UITransformComponent)); + const children = this.getUIChildren(entity); if (children.length === 0) return; // 计算子元素的父容器顶部 Y(worldY 是底部,顶部 = 底部 + 高度) @@ -506,9 +574,129 @@ export class UILayoutSystem extends EntitySystem { parentTopY, parentTransform.computedWidth, parentTransform.computedHeight, - parentTransform.worldAlpha + parentTransform.worldAlpha, + parentTransform.localToWorldMatrix ); } } } + + // ===== 矩阵计算方法 Matrix calculation methods ===== + + /** + * 计算本地变换矩阵 + * Calculate local transformation matrix + * + * @param pivotX - 轴心点 X (0-1) + * @param pivotY - 轴心点 Y (0-1) + * @param width - 元素宽度 + * @param height - 元素高度 + * @param rotation - 旋转角度(弧度) + * @param scaleX - X 缩放 + * @param scaleY - Y 缩放 + * @param x - 元素世界 X 位置 + * @param y - 元素世界 Y 位置 + */ + private calculateLocalMatrix( + pivotX: number, + pivotY: number, + width: number, + height: number, + rotation: number, + scaleX: number, + scaleY: number, + x: number, + y: number + ): Matrix2D { + const cos = Math.cos(rotation); + const sin = Math.sin(rotation); + + // 轴心点相对于元素左下角的偏移 + const px = width * pivotX; + const py = height * pivotY; + + // 构建变换矩阵: Translate(-pivot) -> Scale -> Rotate -> Translate(position + pivot) + // 最终矩阵将轴心点作为旋转/缩放中心 + return { + a: scaleX * cos, + b: scaleX * sin, + c: scaleY * -sin, + d: scaleY * cos, + tx: x + px - (scaleX * cos * px - scaleY * sin * py), + ty: y + py - (scaleX * sin * px + scaleY * cos * py) + }; + } + + /** + * 矩阵乘法: result = a * b + * Matrix multiplication: result = a * b + */ + private multiplyMatrices(a: Matrix2D, b: Matrix2D): Matrix2D { + return { + a: a.a * b.a + a.c * b.b, + b: a.b * b.a + a.d * b.b, + c: a.a * b.c + a.c * b.d, + d: a.b * b.c + a.d * b.d, + tx: a.a * b.tx + a.c * b.ty + a.tx, + ty: a.b * b.tx + a.d * b.ty + a.ty + }; + } + + /** + * 从世界矩阵分解出旋转和缩放 + * Decompose rotation and scale from world matrix + */ + private decomposeMatrix(m: Matrix2D): { rotation: number; scaleX: number; scaleY: number } { + // 计算缩放 + const scaleX = Math.sqrt(m.a * m.a + m.b * m.b); + const scaleY = Math.sqrt(m.c * m.c + m.d * m.d); + + // 检测负缩放(通过行列式符号) + const det = m.a * m.d - m.b * m.c; + const sign = det < 0 ? -1 : 1; + + // 计算旋转(从归一化的矩阵) + let rotation = 0; + if (scaleX > 1e-10) { + rotation = Math.atan2(m.b / scaleX, m.a / scaleX); + } + + return { + rotation, + scaleX, + scaleY: scaleY * sign + }; + } + + /** + * 更新元素的世界变换矩阵 + * Update element's world transformation matrix + */ + private updateWorldMatrix(transform: UITransformComponent, parentMatrix: Matrix2D | null): void { + // 计算本地矩阵 + const localMatrix = this.calculateLocalMatrix( + transform.pivotX, + transform.pivotY, + transform.computedWidth, + transform.computedHeight, + transform.rotation, + transform.scaleX, + transform.scaleY, + transform.worldX, + transform.worldY + ); + + // 计算世界矩阵 + if (parentMatrix) { + transform.localToWorldMatrix = this.multiplyMatrices(parentMatrix, localMatrix); + } else { + transform.localToWorldMatrix = localMatrix; + } + + // 从世界矩阵分解出世界旋转和缩放 + const decomposed = this.decomposeMatrix(transform.localToWorldMatrix); + transform.worldRotation = decomposed.rotation; + transform.worldScaleX = decomposed.scaleX; + transform.worldScaleY = decomposed.scaleY; + } } diff --git a/packages/ui/src/systems/render/UIButtonRenderSystem.ts b/packages/ui/src/systems/render/UIButtonRenderSystem.ts index 1291e1f5..25ec2850 100644 --- a/packages/ui/src/systems/render/UIButtonRenderSystem.ts +++ b/packages/ui/src/systems/render/UIButtonRenderSystem.ts @@ -48,10 +48,20 @@ export class UIButtonRenderSystem extends EntitySystem { const x = transform.worldX ?? transform.x; const y = transform.worldY ?? transform.y; - const width = (transform.computedWidth ?? transform.width) * transform.scaleX; - const height = (transform.computedHeight ?? transform.height) * transform.scaleY; + // 使用世界缩放和旋转 + const scaleX = transform.worldScaleX ?? transform.scaleX; + const scaleY = transform.worldScaleY ?? transform.scaleY; + const rotation = transform.worldRotation ?? transform.rotation; + const width = (transform.computedWidth ?? transform.width) * scaleX; + const height = (transform.computedHeight ?? transform.height) * scaleY; const alpha = transform.worldAlpha ?? transform.alpha; const baseOrder = 100 + transform.zIndex; + // 使用 transform 的 pivot 作为旋转/缩放中心 + const pivotX = transform.pivotX; + const pivotY = transform.pivotY; + // 渲染位置 = 左下角 + pivot 偏移 + const renderX = x + width * pivotX; + const renderY = y + height * pivotY; // Render texture if in texture or both mode // 如果在纹理或两者模式下,渲染纹理 @@ -59,15 +69,15 @@ export class UIButtonRenderSystem extends EntitySystem { const texture = button.getStateTexture('normal'); if (texture) { collector.addRect( - x, y, + renderX, renderY, width, height, 0xFFFFFF, // White tint for texture alpha, baseOrder, { - rotation: transform.rotation, - pivotX: 0, - pivotY: 0, + rotation, + pivotX, + pivotY, texturePath: texture } ); @@ -80,15 +90,15 @@ export class UIButtonRenderSystem extends EntitySystem { const bgAlpha = render?.backgroundAlpha ?? 1; if (bgAlpha > 0) { collector.addRect( - x, y, + renderX, renderY, width, height, button.currentColor, bgAlpha * alpha, baseOrder + (button.useTexture() ? 0.05 : 0), { - rotation: transform.rotation, - pivotX: 0, - pivotY: 0 + rotation, + pivotX, + pivotY } ); } @@ -99,61 +109,72 @@ export class UIButtonRenderSystem extends EntitySystem { if (render && render.borderWidth > 0 && render.borderAlpha > 0) { this.renderBorder( collector, - x, y, width, height, + renderX, renderY, width, height, render.borderWidth, render.borderColor, render.borderAlpha * alpha, baseOrder + 0.1, - transform.rotation + rotation, + pivotX, + pivotY ); } } } /** - * Render border using top-left coordinates - * 使用左上角坐标渲染边框 + * Render border using pivot-based coordinates + * 使用基于 pivot 的坐标渲染边框 */ private renderBorder( collector: ReturnType, - x: number, y: number, + centerX: number, centerY: number, width: number, height: number, borderWidth: number, borderColor: number, alpha: number, sortOrder: number, - rotation: number + rotation: number, + pivotX: number, + pivotY: number ): void { + // 计算矩形的边界(相对于 pivot 中心) + const left = centerX - width * pivotX; + const bottom = centerY - height * pivotY; + const right = left + width; + const top = bottom + height; + // Top border collector.addRect( - x, y, + (left + right) / 2, top - borderWidth / 2, width, borderWidth, borderColor, alpha, sortOrder, - { rotation, pivotX: 0, pivotY: 0 } + { rotation, pivotX: 0.5, pivotY: 0.5 } ); // Bottom border collector.addRect( - x, y + height - borderWidth, + (left + right) / 2, bottom + borderWidth / 2, width, borderWidth, borderColor, alpha, sortOrder, - { rotation, pivotX: 0, pivotY: 0 } + { rotation, pivotX: 0.5, pivotY: 0.5 } ); // Left border (excluding corners) + const sideBorderHeight = height - borderWidth * 2; collector.addRect( - x, y + borderWidth, - borderWidth, height - borderWidth * 2, + left + borderWidth / 2, (top + bottom) / 2, + borderWidth, sideBorderHeight, borderColor, alpha, sortOrder, - { rotation, pivotX: 0, pivotY: 0 } + { rotation, pivotX: 0.5, pivotY: 0.5 } ); // Right border (excluding corners) collector.addRect( - x + width - borderWidth, y + borderWidth, - borderWidth, height - borderWidth * 2, + right - borderWidth / 2, (top + bottom) / 2, + borderWidth, sideBorderHeight, borderColor, alpha, sortOrder, - { rotation, pivotX: 0, pivotY: 0 } + { rotation, pivotX: 0.5, pivotY: 0.5 } ); } } diff --git a/packages/ui/src/systems/render/UIProgressBarRenderSystem.ts b/packages/ui/src/systems/render/UIProgressBarRenderSystem.ts index ab06c752..a8c56905 100644 --- a/packages/ui/src/systems/render/UIProgressBarRenderSystem.ts +++ b/packages/ui/src/systems/render/UIProgressBarRenderSystem.ts @@ -45,23 +45,33 @@ export class UIProgressBarRenderSystem extends EntitySystem { const x = transform.worldX ?? transform.x; const y = transform.worldY ?? transform.y; - const width = (transform.computedWidth ?? transform.width) * transform.scaleX; - const height = (transform.computedHeight ?? transform.height) * transform.scaleY; + // 使用世界缩放和旋转 + const scaleX = transform.worldScaleX ?? transform.scaleX; + const scaleY = transform.worldScaleY ?? transform.scaleY; + const rotation = transform.worldRotation ?? transform.rotation; + const width = (transform.computedWidth ?? transform.width) * scaleX; + const height = (transform.computedHeight ?? transform.height) * scaleY; const alpha = transform.worldAlpha ?? transform.alpha; const baseOrder = 100 + transform.zIndex; + // 使用 transform 的 pivot 作为旋转/缩放中心 + const pivotX = transform.pivotX; + const pivotY = transform.pivotY; + // 渲染位置 = 左下角 + pivot 偏移 + const renderX = x + width * pivotX; + const renderY = y + height * pivotY; - // Render background (x, y is top-left corner) - // 渲染背景(x, y 是左上角) + // Render background + // 渲染背景 if (progressBar.backgroundAlpha > 0) { collector.addRect( - x, y, width, height, + renderX, renderY, width, height, progressBar.backgroundColor, progressBar.backgroundAlpha * alpha, baseOrder, { - rotation: transform.rotation, - pivotX: 0, - pivotY: 0 + rotation, + pivotX, + pivotY } ); } @@ -70,12 +80,14 @@ export class UIProgressBarRenderSystem extends EntitySystem { // 渲染边框 if (progressBar.borderWidth > 0) { this.renderBorder( - collector, x, y, width, height, + collector, renderX, renderY, width, height, progressBar.borderWidth, progressBar.borderColor, alpha, baseOrder + 0.2, - transform + transform, + pivotX, + pivotY ); } @@ -85,13 +97,15 @@ export class UIProgressBarRenderSystem extends EntitySystem { if (progress > 0 && progressBar.fillAlpha > 0) { if (progressBar.showSegments) { this.renderSegmentedFill( - collector, x, y, width, height, - progress, progressBar, alpha, baseOrder + 0.1, transform + collector, renderX, renderY, width, height, + progress, progressBar, alpha, baseOrder + 0.1, transform, + pivotX, pivotY ); } else { this.renderSolidFill( - collector, x, y, width, height, - progress, progressBar, alpha, baseOrder + 0.1, transform + collector, renderX, renderY, width, height, + progress, progressBar, alpha, baseOrder + 0.1, transform, + pivotX, pivotY ); } } @@ -102,57 +116,67 @@ export class UIProgressBarRenderSystem extends EntitySystem { * Render solid fill rectangle * 渲染实心填充矩形 * - * Note: x, y is the top-left corner of the progress bar - * 注意:x, y 是进度条的左上角 + * Note: centerX, centerY is the pivot position of the progress bar + * 注意:centerX, centerY 是进度条的 pivot 位置 */ private renderSolidFill( collector: ReturnType, - x: number, y: number, width: number, height: number, + centerX: number, centerY: number, width: number, height: number, progress: number, progressBar: UIProgressBarComponent, alpha: number, sortOrder: number, - transform: UITransformComponent + transform: UITransformComponent, + pivotX: number, + pivotY: number ): void { - let fillX = x; - let fillY = y; + const rotation = transform.worldRotation ?? transform.rotation; + + // 计算进度条的边界(相对于 pivot 中心) + const left = centerX - width * pivotX; + const bottom = centerY - height * pivotY; + + let fillX: number; + let fillY: number; let fillWidth = width; let fillHeight = height; // Calculate fill dimensions based on direction - // x, y is top-left corner, so calculations are simpler // 根据方向计算填充尺寸 - // x, y 是左上角,所以计算更简单 switch (progressBar.direction) { case UIProgressDirection.LeftToRight: fillWidth = width * progress; - // Fill starts from left (fillX = x, no change) + fillX = left + fillWidth / 2; + fillY = bottom + height / 2; break; case UIProgressDirection.RightToLeft: fillWidth = width * progress; - // Fill starts from right - fillX = x + width - fillWidth; + fillX = left + width - fillWidth / 2; + fillY = bottom + height / 2; break; case UIProgressDirection.BottomToTop: fillHeight = height * progress; - // Fill starts from bottom - fillY = y + height - fillHeight; + fillX = left + width / 2; + fillY = bottom + fillHeight / 2; break; case UIProgressDirection.TopToBottom: fillHeight = height * progress; - // Fill starts from top (fillY = y, no change) + fillX = left + width / 2; + fillY = bottom + height - fillHeight / 2; break; + + default: + fillX = left + fillWidth / 2; + fillY = bottom + height / 2; } // Determine fill color (gradient or solid) // 确定填充颜色(渐变或实心) let fillColor = progressBar.fillColor; if (progressBar.useGradient) { - // Simple linear interpolation between start and end colors - // 简单的起始和结束颜色线性插值 fillColor = this.lerpColor( progressBar.gradientStartColor, progressBar.gradientEndColor, @@ -166,9 +190,9 @@ export class UIProgressBarRenderSystem extends EntitySystem { progressBar.fillAlpha * alpha, sortOrder, { - rotation: transform.rotation, - pivotX: 0, - pivotY: 0 + rotation, + pivotX: 0.5, + pivotY: 0.5 } ); } @@ -177,18 +201,21 @@ export class UIProgressBarRenderSystem extends EntitySystem { * Render segmented fill * 渲染分段填充 * - * Note: x, y is the top-left corner of the progress bar - * 注意:x, y 是进度条的左上角 + * Note: centerX, centerY is the pivot position of the progress bar + * 注意:centerX, centerY 是进度条的 pivot 位置 */ private renderSegmentedFill( collector: ReturnType, - x: number, y: number, width: number, height: number, + centerX: number, centerY: number, width: number, height: number, progress: number, progressBar: UIProgressBarComponent, alpha: number, sortOrder: number, - transform: UITransformComponent + transform: UITransformComponent, + pivotX: number, + pivotY: number ): void { + const rotation = transform.worldRotation ?? transform.rotation; const segments = progressBar.segments; const gap = progressBar.segmentGap; const filledSegments = Math.ceil(progress * segments); @@ -196,6 +223,10 @@ export class UIProgressBarRenderSystem extends EntitySystem { const isHorizontal = progressBar.direction === UIProgressDirection.LeftToRight || progressBar.direction === UIProgressDirection.RightToLeft; + // 计算进度条的边界(相对于 pivot 中心) + const left = centerX - width * pivotX; + const bottom = centerY - height * pivotY; + // Calculate segment dimensions // 计算段尺寸 let segmentWidth: number; @@ -209,41 +240,36 @@ export class UIProgressBarRenderSystem extends EntitySystem { segmentHeight = (height - gap * (segments - 1)) / segments; } - // x, y is already top-left corner - // x, y 已经是左上角 - const baseX = x; - const baseY = y; - for (let i = 0; i < filledSegments && i < segments; i++) { - let segX: number; - let segY: number; + let segCenterX: number; + let segCenterY: number; - // Calculate segment position based on direction (using top-left positions) - // 根据方向计算段位置(使用左上角位置) + // Calculate segment center position based on direction + // 根据方向计算段中心位置 switch (progressBar.direction) { case UIProgressDirection.LeftToRight: - segX = baseX + i * (segmentWidth + gap); - segY = baseY; + segCenterX = left + i * (segmentWidth + gap) + segmentWidth / 2; + segCenterY = bottom + height / 2; break; case UIProgressDirection.RightToLeft: - segX = baseX + width - (i + 1) * segmentWidth - i * gap; - segY = baseY; + segCenterX = left + width - i * (segmentWidth + gap) - segmentWidth / 2; + segCenterY = bottom + height / 2; break; case UIProgressDirection.TopToBottom: - segX = baseX; - segY = baseY + i * (segmentHeight + gap); + segCenterX = left + width / 2; + segCenterY = bottom + height - i * (segmentHeight + gap) - segmentHeight / 2; break; case UIProgressDirection.BottomToTop: - segX = baseX; - segY = baseY + height - (i + 1) * segmentHeight - i * gap; + segCenterX = left + width / 2; + segCenterY = bottom + i * (segmentHeight + gap) + segmentHeight / 2; break; default: - segX = baseX + i * (segmentWidth + gap); - segY = baseY; + segCenterX = left + i * (segmentWidth + gap) + segmentWidth / 2; + segCenterY = bottom + height / 2; } // Determine segment color @@ -258,19 +284,19 @@ export class UIProgressBarRenderSystem extends EntitySystem { ); } - // Use top-left position with pivot 0,0 - // 使用左上角位置,pivot 0,0 + // Use center position with pivot 0.5, 0.5 + // 使用中心位置,pivot 0.5, 0.5 collector.addRect( - segX, segY, + segCenterX, segCenterY, segmentWidth, segmentHeight, segmentColor, progressBar.fillAlpha * alpha, - sortOrder + i * 0.001, // Slight offset for each segment + sortOrder + i * 0.001, { - rotation: transform.rotation, - pivotX: 0, - pivotY: 0 + rotation, + pivotX: 0.5, + pivotY: 0.5 } ); } @@ -280,51 +306,59 @@ export class UIProgressBarRenderSystem extends EntitySystem { * Render border * 渲染边框 * - * Note: x, y is the top-left corner of the progress bar - * 注意:x, y 是进度条的左上角 + * Note: centerX, centerY is the pivot position of the progress bar + * 注意:centerX, centerY 是进度条的 pivot 位置 */ private renderBorder( collector: ReturnType, - x: number, y: number, width: number, height: number, + centerX: number, centerY: number, width: number, height: number, borderWidth: number, borderColor: number, alpha: number, sortOrder: number, - _transform: UITransformComponent + transform: UITransformComponent, + pivotX: number, + pivotY: number ): void { - // x, y is already top-left corner - // x, y 已经是左上角 + const rotation = transform.worldRotation ?? transform.rotation; + + // 计算边界(相对于 pivot 中心) + const left = centerX - width * pivotX; + const bottom = centerY - height * pivotY; + const right = left + width; + const top = bottom + height; // Top border collector.addRect( - x, y, + (left + right) / 2, top - borderWidth / 2, width, borderWidth, borderColor, alpha, sortOrder, - { pivotX: 0, pivotY: 0 } + { rotation, pivotX: 0.5, pivotY: 0.5 } ); // Bottom border collector.addRect( - x, y + height - borderWidth, + (left + right) / 2, bottom + borderWidth / 2, width, borderWidth, borderColor, alpha, sortOrder, - { pivotX: 0, pivotY: 0 } + { rotation, pivotX: 0.5, pivotY: 0.5 } ); // Left border (excluding corners) + const sideBorderHeight = height - borderWidth * 2; collector.addRect( - x, y + borderWidth, - borderWidth, height - borderWidth * 2, + left + borderWidth / 2, (top + bottom) / 2, + borderWidth, sideBorderHeight, borderColor, alpha, sortOrder, - { pivotX: 0, pivotY: 0 } + { rotation, pivotX: 0.5, pivotY: 0.5 } ); // Right border (excluding corners) collector.addRect( - x + width - borderWidth, y + borderWidth, - borderWidth, height - borderWidth * 2, + right - borderWidth / 2, (top + bottom) / 2, + borderWidth, sideBorderHeight, borderColor, alpha, sortOrder, - { pivotX: 0, pivotY: 0 } + { rotation, pivotX: 0.5, pivotY: 0.5 } ); } diff --git a/packages/ui/src/systems/render/UIRectRenderSystem.ts b/packages/ui/src/systems/render/UIRectRenderSystem.ts index 334c8941..63bc8cee 100644 --- a/packages/ui/src/systems/render/UIRectRenderSystem.ts +++ b/packages/ui/src/systems/render/UIRectRenderSystem.ts @@ -56,33 +56,40 @@ export class UIRectRenderSystem extends EntitySystem { const x = transform.worldX ?? transform.x; const y = transform.worldY ?? transform.y; - const width = (transform.computedWidth ?? transform.width) * transform.scaleX; - const height = (transform.computedHeight ?? transform.height) * transform.scaleY; + // 使用世界缩放(考虑父级缩放) + const scaleX = transform.worldScaleX ?? transform.scaleX; + const scaleY = transform.worldScaleY ?? transform.scaleY; + const width = (transform.computedWidth ?? transform.width) * scaleX; + const height = (transform.computedHeight ?? transform.height) * scaleY; const alpha = transform.worldAlpha ?? transform.alpha; + // 使用世界旋转(考虑父级旋转) + const rotation = transform.worldRotation ?? transform.rotation; const baseOrder = 100 + transform.zIndex; + // 使用 transform 的 pivot 作为旋转/缩放中心 + const pivotX = transform.pivotX; + const pivotY = transform.pivotY; - // Use top-left position with origin at (0, 0) - // Like Sprite: x,y is anchor position, origin determines where anchor is on the rect - // For UI: x,y is top-left corner, so origin should be (0, 0) - // 使用左上角位置,原点在 (0, 0) - // 类似 Sprite:x,y 是锚点位置,origin 决定锚点在矩形上的位置 - // 对于 UI:x,y 是左上角,所以 origin 应该是 (0, 0) + // worldX/worldY 是元素左下角位置,需要转换为以 pivot 为中心的位置 + // pivot 相对于元素的偏移:(width * pivotX, height * pivotY) + // 渲染位置 = 左下角 + pivot 偏移 + const renderX = x + width * pivotX; + const renderY = y + height * pivotY; // Render shadow if enabled // 如果启用,渲染阴影 if (render.shadowEnabled && render.shadowAlpha > 0) { collector.addRect( - x + render.shadowOffsetX - render.shadowBlur, - y + render.shadowOffsetY - render.shadowBlur, + renderX + render.shadowOffsetX, + renderY + render.shadowOffsetY, width + render.shadowBlur * 2, height + render.shadowBlur * 2, render.shadowColor, render.shadowAlpha * alpha, baseOrder - 0.1, { - rotation: transform.rotation, - pivotX: 0, - pivotY: 0 + rotation, + pivotX, + pivotY } ); } @@ -94,15 +101,15 @@ export class UIRectRenderSystem extends EntitySystem { const textureId = typeof render.texture === 'number' ? render.texture : undefined; collector.addRect( - x, y, + renderX, renderY, width, height, render.textureTint, alpha, baseOrder, { - rotation: transform.rotation, - pivotX: 0, - pivotY: 0, + rotation, + pivotX, + pivotY, textureId, texturePath, uv: render.textureUV @@ -115,15 +122,15 @@ export class UIRectRenderSystem extends EntitySystem { // 如果启用填充,渲染背景颜色 else if (render.fillBackground && render.backgroundAlpha > 0) { collector.addRect( - x, y, + renderX, renderY, width, height, render.backgroundColor, render.backgroundAlpha * alpha, baseOrder, { - rotation: transform.rotation, - pivotX: 0, - pivotY: 0 + rotation, + pivotX, + pivotY } ); } @@ -133,61 +140,78 @@ export class UIRectRenderSystem extends EntitySystem { if (render.borderWidth > 0 && render.borderAlpha > 0) { this.renderBorder( collector, - x, y, width, height, + renderX, renderY, width, height, render.borderWidth, render.borderColor, render.borderAlpha * alpha, baseOrder + 0.1, - transform.rotation + rotation, + pivotX, + pivotY ); } } } /** - * Render border using top-left coordinates - * 使用左上角坐标渲染边框 + * Render border using pivot-based coordinates + * 使用基于 pivot 的坐标渲染边框 */ private renderBorder( collector: ReturnType, - x: number, y: number, + centerX: number, centerY: number, width: number, height: number, borderWidth: number, borderColor: number, alpha: number, sortOrder: number, - rotation: number + rotation: number, + pivotX: number, + pivotY: number ): void { - // Top border (from top-left corner) + // 计算矩形的左下角位置(相对于 pivot 中心) + const left = centerX - width * pivotX; + const bottom = centerY - height * pivotY; + const right = left + width; + const top = bottom + height; + + // Top border + const topBorderCenterX = (left + right) / 2; + const topBorderCenterY = top - borderWidth / 2; collector.addRect( - x, y, + topBorderCenterX, topBorderCenterY, width, borderWidth, borderColor, alpha, sortOrder, - { rotation, pivotX: 0, pivotY: 0 } + { rotation, pivotX: 0.5, pivotY: 0.5 } ); // Bottom border + const bottomBorderCenterY = bottom + borderWidth / 2; collector.addRect( - x, y + height - borderWidth, + topBorderCenterX, bottomBorderCenterY, width, borderWidth, borderColor, alpha, sortOrder, - { rotation, pivotX: 0, pivotY: 0 } + { rotation, pivotX: 0.5, pivotY: 0.5 } ); // Left border (excluding corners) + const sideBorderHeight = height - borderWidth * 2; + const leftBorderCenterX = left + borderWidth / 2; + const sideBorderCenterY = (top + bottom) / 2; collector.addRect( - x, y + borderWidth, - borderWidth, height - borderWidth * 2, + leftBorderCenterX, sideBorderCenterY, + borderWidth, sideBorderHeight, borderColor, alpha, sortOrder, - { rotation, pivotX: 0, pivotY: 0 } + { rotation, pivotX: 0.5, pivotY: 0.5 } ); // Right border (excluding corners) + const rightBorderCenterX = right - borderWidth / 2; collector.addRect( - x + width - borderWidth, y + borderWidth, - borderWidth, height - borderWidth * 2, + rightBorderCenterX, sideBorderCenterY, + borderWidth, sideBorderHeight, borderColor, alpha, sortOrder, - { rotation, pivotX: 0, pivotY: 0 } + { rotation, pivotX: 0.5, pivotY: 0.5 } ); } } diff --git a/packages/ui/src/systems/render/UIScrollViewRenderSystem.ts b/packages/ui/src/systems/render/UIScrollViewRenderSystem.ts index 077f3e44..1a37f282 100644 --- a/packages/ui/src/systems/render/UIScrollViewRenderSystem.ts +++ b/packages/ui/src/systems/render/UIScrollViewRenderSystem.ts @@ -46,15 +46,24 @@ export class UIScrollViewRenderSystem extends EntitySystem { const x = transform.worldX ?? transform.x; const y = transform.worldY ?? transform.y; - const width = (transform.computedWidth ?? transform.width) * transform.scaleX; - const height = (transform.computedHeight ?? transform.height) * transform.scaleY; + // 使用世界缩放 + const scaleX = transform.worldScaleX ?? transform.scaleX; + const scaleY = transform.worldScaleY ?? transform.scaleY; + const rotation = transform.worldRotation ?? transform.rotation; + const width = (transform.computedWidth ?? transform.width) * scaleX; + const height = (transform.computedHeight ?? transform.height) * scaleY; const alpha = transform.worldAlpha ?? transform.alpha; const baseOrder = 100 + transform.zIndex; + // 使用 transform 的 pivot 计算位置 + const pivotX = transform.pivotX; + const pivotY = transform.pivotY; + // 渲染位置 = 左下角 + pivot 偏移 + const renderX = x + width * pivotX; + const renderY = y + height * pivotY; - // x, y is already top-left corner - // x, y 已经是左上角 - const baseX = x; - const baseY = y; + // 计算边界 + const baseX = renderX - width * pivotX; + const baseY = renderY - height * pivotY; // Render vertical scrollbar // 渲染垂直滚动条 @@ -62,7 +71,7 @@ export class UIScrollViewRenderSystem extends EntitySystem { this.renderVerticalScrollbar( collector, baseX, baseY, width, height, - scrollView, alpha, baseOrder + scrollView, alpha, baseOrder, rotation ); } @@ -72,7 +81,7 @@ export class UIScrollViewRenderSystem extends EntitySystem { this.renderHorizontalScrollbar( collector, baseX, baseY, width, height, - scrollView, alpha, baseOrder + scrollView, alpha, baseOrder, rotation ); } } @@ -88,7 +97,8 @@ export class UIScrollViewRenderSystem extends EntitySystem { viewWidth: number, viewHeight: number, scrollView: UIScrollViewComponent, alpha: number, - baseOrder: number + baseOrder: number, + rotation: number ): void { const scrollbarWidth = scrollView.scrollbarWidth; const hasHorizontal = scrollView.needsHorizontalScrollbar(viewWidth); @@ -108,7 +118,7 @@ export class UIScrollViewRenderSystem extends EntitySystem { scrollView.scrollbarTrackColor, scrollView.scrollbarTrackAlpha * alpha, baseOrder + 0.5, - { pivotX: 0.5, pivotY: 0.5 } + { rotation, pivotX: 0.5, pivotY: 0.5 } ); } @@ -131,7 +141,7 @@ export class UIScrollViewRenderSystem extends EntitySystem { scrollView.scrollbarColor, handleAlpha * alpha, baseOrder + 0.6, - { pivotX: 0.5, pivotY: 0.5 } + { rotation, pivotX: 0.5, pivotY: 0.5 } ); } @@ -145,7 +155,8 @@ export class UIScrollViewRenderSystem extends EntitySystem { viewWidth: number, viewHeight: number, scrollView: UIScrollViewComponent, alpha: number, - baseOrder: number + baseOrder: number, + rotation: number ): void { const scrollbarWidth = scrollView.scrollbarWidth; const hasVertical = scrollView.needsVerticalScrollbar(viewHeight); @@ -165,7 +176,7 @@ export class UIScrollViewRenderSystem extends EntitySystem { scrollView.scrollbarTrackColor, scrollView.scrollbarTrackAlpha * alpha, baseOrder + 0.5, - { pivotX: 0.5, pivotY: 0.5 } + { rotation, pivotX: 0.5, pivotY: 0.5 } ); } @@ -188,7 +199,7 @@ export class UIScrollViewRenderSystem extends EntitySystem { scrollView.scrollbarColor, handleAlpha * alpha, baseOrder + 0.6, - { pivotX: 0.5, pivotY: 0.5 } + { rotation, pivotX: 0.5, pivotY: 0.5 } ); } } diff --git a/packages/ui/src/systems/render/UISliderRenderSystem.ts b/packages/ui/src/systems/render/UISliderRenderSystem.ts index df0be72a..9f1927f0 100644 --- a/packages/ui/src/systems/render/UISliderRenderSystem.ts +++ b/packages/ui/src/systems/render/UISliderRenderSystem.ts @@ -45,10 +45,20 @@ export class UISliderRenderSystem extends EntitySystem { const x = transform.worldX ?? transform.x; const y = transform.worldY ?? transform.y; - const width = (transform.computedWidth ?? transform.width) * transform.scaleX; - const height = (transform.computedHeight ?? transform.height) * transform.scaleY; + // 使用世界缩放 + const scaleX = transform.worldScaleX ?? transform.scaleX; + const scaleY = transform.worldScaleY ?? transform.scaleY; + const rotation = transform.worldRotation ?? transform.rotation; + const width = (transform.computedWidth ?? transform.width) * scaleX; + const height = (transform.computedHeight ?? transform.height) * scaleY; const alpha = transform.worldAlpha ?? transform.alpha; const baseOrder = 100 + transform.zIndex; + // 使用 transform 的 pivot 计算中心位置 + const pivotX = transform.pivotX; + const pivotY = transform.pivotY; + // 渲染位置 = 左下角 + pivot 偏移 + const renderX = x + width * pivotX; + const renderY = y + height * pivotY; const isHorizontal = slider.orientation === UISliderOrientation.Horizontal; const progress = slider.getProgress(); @@ -58,10 +68,10 @@ export class UISliderRenderSystem extends EntitySystem { const trackLength = isHorizontal ? width : height; const trackThickness = slider.trackThickness; - // Calculate center position (x, y is top-left corner) - // 计算中心位置(x, y 是左上角) - const centerX = x + width / 2; - const centerY = y + height / 2; + // Calculate center position based on pivot + // 基于 pivot 计算中心位置 + const centerX = renderX; + const centerY = renderY; // Render track (using center position with pivot 0.5) // 渲染轨道(使用中心位置,pivot 0.5) @@ -73,7 +83,7 @@ export class UISliderRenderSystem extends EntitySystem { slider.trackColor, slider.trackAlpha * alpha, baseOrder, - { pivotX: 0.5, pivotY: 0.5 } + { rotation, pivotX: 0.5, pivotY: 0.5 } ); } else { collector.addRect( @@ -82,7 +92,7 @@ export class UISliderRenderSystem extends EntitySystem { slider.trackColor, slider.trackAlpha * alpha, baseOrder, - { pivotX: 0.5, pivotY: 0.5 } + { rotation, pivotX: 0.5, pivotY: 0.5 } ); } } @@ -101,7 +111,7 @@ export class UISliderRenderSystem extends EntitySystem { slider.fillColor, slider.fillAlpha * alpha, baseOrder + 0.1, - { pivotX: 0.5, pivotY: 0.5 } + { rotation, pivotX: 0.5, pivotY: 0.5 } ); } else { // Fill from bottom @@ -112,7 +122,7 @@ export class UISliderRenderSystem extends EntitySystem { slider.fillColor, slider.fillAlpha * alpha, baseOrder + 0.1, - { pivotX: 0.5, pivotY: 0.5 } + { rotation, pivotX: 0.5, pivotY: 0.5 } ); } } @@ -124,7 +134,7 @@ export class UISliderRenderSystem extends EntitySystem { collector, centerX, centerY, trackLength, trackThickness, slider, alpha, baseOrder + 0.05, - isHorizontal + isHorizontal, rotation ); } @@ -147,7 +157,7 @@ export class UISliderRenderSystem extends EntitySystem { 0x000000, 0.3 * alpha, baseOrder + 0.15, - { pivotX: 0.5, pivotY: 0.5 } + { rotation, pivotX: 0.5, pivotY: 0.5 } ); } @@ -159,7 +169,7 @@ export class UISliderRenderSystem extends EntitySystem { handleColor, alpha, baseOrder + 0.2, - { pivotX: 0.5, pivotY: 0.5 } + { rotation, pivotX: 0.5, pivotY: 0.5 } ); // Handle border (if any) @@ -172,7 +182,8 @@ export class UISliderRenderSystem extends EntitySystem { slider.handleBorderWidth, slider.handleBorderColor, alpha, - baseOrder + 0.25 + baseOrder + 0.25, + rotation ); } } @@ -189,7 +200,8 @@ export class UISliderRenderSystem extends EntitySystem { slider: UISliderComponent, alpha: number, sortOrder: number, - isHorizontal: boolean + isHorizontal: boolean, + rotation: number ): void { const tickCount = slider.tickCount + 2; // Include start and end ticks const tickSize = slider.tickSize; @@ -220,7 +232,7 @@ export class UISliderRenderSystem extends EntitySystem { slider.tickColor, alpha, sortOrder, - { pivotX: 0.5, pivotY: 0.5 } + { rotation, pivotX: 0.5, pivotY: 0.5 } ); } } @@ -236,7 +248,8 @@ export class UISliderRenderSystem extends EntitySystem { borderWidth: number, borderColor: number, alpha: number, - sortOrder: number + sortOrder: number, + rotation: number ): void { const halfW = width / 2; const halfH = height / 2; @@ -247,7 +260,7 @@ export class UISliderRenderSystem extends EntitySystem { x, y - halfH + halfB, width, borderWidth, borderColor, alpha, sortOrder, - { pivotX: 0.5, pivotY: 0.5 } + { rotation, pivotX: 0.5, pivotY: 0.5 } ); // Bottom @@ -255,7 +268,7 @@ export class UISliderRenderSystem extends EntitySystem { x, y + halfH - halfB, width, borderWidth, borderColor, alpha, sortOrder, - { pivotX: 0.5, pivotY: 0.5 } + { rotation, pivotX: 0.5, pivotY: 0.5 } ); // Left @@ -263,7 +276,7 @@ export class UISliderRenderSystem extends EntitySystem { x - halfW + halfB, y, borderWidth, height - borderWidth * 2, borderColor, alpha, sortOrder, - { pivotX: 0.5, pivotY: 0.5 } + { rotation, pivotX: 0.5, pivotY: 0.5 } ); // Right @@ -271,7 +284,7 @@ export class UISliderRenderSystem extends EntitySystem { x + halfW - halfB, y, borderWidth, height - borderWidth * 2, borderColor, alpha, sortOrder, - { pivotX: 0.5, pivotY: 0.5 } + { rotation, pivotX: 0.5, pivotY: 0.5 } ); } } diff --git a/packages/ui/src/systems/render/UITextRenderSystem.ts b/packages/ui/src/systems/render/UITextRenderSystem.ts index 91e4ec3a..5c118902 100644 --- a/packages/ui/src/systems/render/UITextRenderSystem.ts +++ b/packages/ui/src/systems/render/UITextRenderSystem.ts @@ -101,10 +101,20 @@ export class UITextRenderSystem extends EntitySystem { const x = transform.worldX ?? transform.x; const y = transform.worldY ?? transform.y; - const width = (transform.computedWidth ?? transform.width) * transform.scaleX; - const height = (transform.computedHeight ?? transform.height) * transform.scaleY; + // 使用世界缩放和旋转 + const scaleX = transform.worldScaleX ?? transform.scaleX; + const scaleY = transform.worldScaleY ?? transform.scaleY; + const rotation = transform.worldRotation ?? transform.rotation; + const width = (transform.computedWidth ?? transform.width) * scaleX; + const height = (transform.computedHeight ?? transform.height) * scaleY; const alpha = transform.worldAlpha ?? transform.alpha; const baseOrder = 100 + transform.zIndex; + // 使用 transform 的 pivot 作为旋转/缩放中心 + const pivotX = transform.pivotX; + const pivotY = transform.pivotY; + // 渲染位置 = 左下角 + pivot 偏移 + const renderX = x + width * pivotX; + const renderY = y + height * pivotY; // Generate or retrieve cached texture // 生成或获取缓存的纹理 @@ -114,18 +124,18 @@ export class UITextRenderSystem extends EntitySystem { if (textureId === null) continue; - // Use top-left position with origin at (0, 0) - // 使用左上角位置,原点在 (0, 0) + // Use pivot position with transform's pivot values + // 使用 transform 的 pivot 值作为旋转中心 collector.addRect( - x, y, + renderX, renderY, width, height, 0xFFFFFF, // White tint (color is baked into texture) alpha, baseOrder + 1, // Text renders above background { - rotation: transform.rotation, - pivotX: 0, - pivotY: 0, + rotation, + pivotX, + pivotY, textureId } ); diff --git a/packages/ui/tsconfig.build.json b/packages/ui/tsconfig.build.json new file mode 100644 index 00000000..ba0684d9 --- /dev/null +++ b/packages/ui/tsconfig.build.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ES2020", + "moduleResolution": "bundler", + "lib": ["ES2020", "DOM"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "declaration": true, + "declarationMap": true, + "jsx": "react-jsx", + "resolveJsonModule": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/ui/tsconfig.json b/packages/ui/tsconfig.json index a2c2555e..e3f78665 100644 --- a/packages/ui/tsconfig.json +++ b/packages/ui/tsconfig.json @@ -25,7 +25,6 @@ "exclude": ["node_modules", "dist"], "references": [ { "path": "../core" }, - { "path": "../components" }, { "path": "../editor-core" } ] } diff --git a/packages/ui/tsup.config.ts b/packages/ui/tsup.config.ts new file mode 100644 index 00000000..f704a430 --- /dev/null +++ b/packages/ui/tsup.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'tsup'; +import { runtimeOnlyPreset } from '../build-config/src/presets/plugin-tsup'; + +export default defineConfig({ + ...runtimeOnlyPreset(), + tsconfig: 'tsconfig.build.json' +}); diff --git a/packages/ui/vite.config.ts b/packages/ui/vite.config.ts deleted file mode 100644 index 8fc3c196..00000000 --- a/packages/ui/vite.config.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { defineConfig } from 'vite'; -import { resolve } from 'path'; -import dts from 'vite-plugin-dts'; -import react from '@vitejs/plugin-react'; - -export default defineConfig({ - plugins: [ - react(), - dts({ - include: ['src'], - outDir: 'dist', - rollupTypes: false - }) - ], - esbuild: { - jsx: 'automatic', - }, - build: { - lib: { - entry: { - index: resolve(__dirname, 'src/index.ts'), - runtime: resolve(__dirname, 'src/runtime.ts'), - 'editor/index': resolve(__dirname, 'src/editor/index.ts') - }, - formats: ['es'], - fileName: (format, entryName) => `${entryName}.js` - }, - rollupOptions: { - external: [ - '@esengine/ecs-framework', - '@esengine/editor-core', - 'react', - 'react/jsx-runtime', - 'lucide-react', - /^@esengine\// - ], - output: { - exports: 'named', - preserveModules: false - } - }, - target: 'es2020', - minify: false, - sourcemap: true - } -}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9088347c..bf0a7e10 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -126,6 +126,9 @@ importers: ts-jest: specifier: ^29.4.0 version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.25))(typescript@5.9.3) + turbo: + specifier: ^2.6.1 + version: 2.6.1 typedoc: specifier: ^0.28.13 version: 0.28.14(typescript@5.9.3) @@ -146,75 +149,68 @@ importers: version: 1.6.4(@algolia/client-search@5.44.0)(@types/node@20.19.25)(@types/react@18.3.27)(axios@1.13.2)(postcss@8.5.6)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(search-insights@2.17.3)(terser@5.44.1)(typescript@5.9.3) packages/asset-system: - dependencies: - '@esengine/ecs-framework': - specifier: ^2.0.0 - version: link:../core devDependencies: - '@rollup/plugin-commonjs': - specifier: ^28.0.3 - version: 28.0.9(rollup@4.53.3) - '@rollup/plugin-node-resolve': - specifier: ^16.0.1 - version: 16.0.3(rollup@4.53.3) - '@rollup/plugin-typescript': - specifier: ^11.1.6 - version: 11.1.6(rollup@4.53.3)(tslib@2.8.1)(typescript@5.9.3) + '@esengine/build-config': + specifier: workspace:* + version: link:../build-config + '@esengine/ecs-framework': + specifier: workspace:* + version: link:../core rimraf: specifier: ^5.0.0 version: 5.0.10 - rollup: - specifier: ^4.42.0 - version: 4.53.3 - rollup-plugin-dts: - specifier: ^6.2.1 - version: 6.2.3(rollup@4.53.3)(typescript@5.9.3) + tsup: + specifier: ^8.0.0 + version: 8.5.1(@microsoft/api-extractor@7.55.1(@types/node@20.19.25))(@swc/core@1.15.3)(jiti@2.6.1)(postcss@8.5.6)(typescript@5.9.3)(yaml@2.8.1) typescript: specifier: ^5.8.3 version: 5.9.3 + packages/audio: + devDependencies: + '@esengine/build-config': + specifier: workspace:* + version: link:../build-config + '@esengine/ecs-framework': + specifier: workspace:* + version: link:../core + '@esengine/engine-core': + specifier: workspace:* + version: link:../engine-core + rimraf: + specifier: ^5.0.5 + version: 5.0.10 + tsup: + specifier: ^8.0.0 + version: 8.5.1(@microsoft/api-extractor@7.55.1(@types/node@20.19.25))(@swc/core@1.15.3)(jiti@2.6.1)(postcss@8.5.6)(typescript@5.9.3)(yaml@2.8.1) + typescript: + specifier: ^5.3.3 + version: 5.9.3 + packages/behavior-tree: dependencies: - '@esengine/asset-system': - specifier: workspace:* - version: link:../asset-system - '@esengine/ecs-components': - specifier: workspace:* - version: link:../components - '@esengine/ecs-framework': - specifier: '>=2.0.0' - version: link:../core - '@esengine/editor-runtime': - specifier: workspace:* - version: link:../editor-runtime - lucide-react: - specifier: ^0.545.0 - version: 0.545.0(react@18.3.1) - react: - specifier: ^18.3.1 - version: 18.3.1 tslib: specifier: ^2.8.1 version: 2.8.1 - zustand: - specifier: ^4.5.2 - version: 4.5.7(@types/react@18.3.27)(react@18.3.1) devDependencies: - '@tauri-apps/plugin-fs': - specifier: ^2.4.2 - version: 2.4.4 + '@esengine/asset-system': + specifier: workspace:* + version: link:../asset-system + '@esengine/build-config': + specifier: workspace:* + version: link:../build-config + '@esengine/ecs-framework': + specifier: workspace:* + version: link:../core + '@esengine/engine-core': + specifier: workspace:* + version: link:../engine-core '@types/jest': specifier: ^29.5.14 version: 29.5.14 '@types/node': specifier: ^20.19.17 version: 20.19.25 - '@types/react': - specifier: ^18.3.12 - version: 18.3.27 - '@vitejs/plugin-react': - specifier: ^4.7.0 - version: 4.7.0(vite@6.4.1(@types/node@20.19.25)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.1)) jest: specifier: ^29.7.0 version: 29.7.0(@types/node@20.19.25) @@ -224,76 +220,197 @@ importers: ts-jest: specifier: ^29.4.0 version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.25))(typescript@5.9.3) + tsup: + specifier: ^8.0.0 + version: 8.5.1(@microsoft/api-extractor@7.55.1(@types/node@20.19.25))(@swc/core@1.15.3)(jiti@2.6.1)(postcss@8.5.6)(typescript@5.9.3)(yaml@2.8.1) typescript: specifier: ^5.8.3 version: 5.9.3 - vite: - specifier: ^6.0.7 - version: 6.4.1(@types/node@20.19.25)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.1) - vite-plugin-dts: - specifier: ^3.7.0 - version: 3.9.1(@types/node@20.19.25)(rollup@4.53.3)(typescript@5.9.3)(vite@6.4.1(@types/node@20.19.25)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.1)) - packages/blueprint: + packages/behavior-tree-editor: dependencies: + '@esengine/behavior-tree': + specifier: workspace:* + version: link:../behavior-tree + devDependencies: + '@esengine/build-config': + specifier: workspace:* + version: link:../build-config '@esengine/ecs-framework': - specifier: '>=2.0.0' + specifier: workspace:* version: link:../core + '@esengine/editor-core': + specifier: workspace:* + version: link:../editor-core '@esengine/editor-runtime': specifier: workspace:* version: link:../editor-runtime + '@esengine/engine-core': + specifier: workspace:* + version: link:../engine-core '@esengine/node-editor': specifier: workspace:* version: link:../node-editor - react: - specifier: ^18.3.1 - version: 18.3.1 - tslib: - specifier: ^2.8.1 - version: 2.8.1 - zustand: - specifier: ^4.5.2 - version: 4.5.7(@types/react@18.3.27)(react@18.3.1) - devDependencies: - '@types/node': - specifier: ^20.19.17 - version: 20.19.25 '@types/react': specifier: ^18.3.12 version: 18.3.27 - '@vitejs/plugin-react': - specifier: ^4.7.0 - version: 4.7.0(vite@6.4.1(@types/node@20.19.25)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.1)) + lucide-react: + specifier: ^0.545.0 + version: 0.545.0(react@18.3.1) + react: + specifier: ^18.3.1 + version: 18.3.1 + rimraf: + specifier: ^5.0.5 + version: 5.0.10 + tsup: + specifier: ^8.0.0 + version: 8.5.1(@microsoft/api-extractor@7.55.1(@types/node@20.19.25))(@swc/core@1.15.3)(jiti@2.6.1)(postcss@8.5.6)(typescript@5.9.3)(yaml@2.8.1) + typescript: + specifier: ^5.3.3 + version: 5.9.3 + zustand: + specifier: ^5.0.8 + version: 5.0.8(@types/react@18.3.27)(react@18.3.1)(use-sync-external-store@1.6.0(react@18.3.1)) + + packages/blueprint: + dependencies: + tslib: + specifier: ^2.8.1 + version: 2.8.1 + devDependencies: + '@esengine/build-config': + specifier: workspace:* + version: link:../build-config + '@esengine/ecs-framework': + specifier: workspace:* + version: link:../core + '@types/node': + specifier: ^20.19.17 + version: 20.19.25 rimraf: specifier: ^5.0.0 version: 5.0.10 + tsup: + specifier: ^8.0.0 + version: 8.5.1(@microsoft/api-extractor@7.55.1(@types/node@20.19.25))(@swc/core@1.15.3)(jiti@2.6.1)(postcss@8.5.6)(typescript@5.9.3)(yaml@2.8.1) + typescript: + specifier: ^5.8.3 + version: 5.9.3 + + packages/blueprint-editor: + dependencies: + '@esengine/blueprint': + specifier: workspace:* + version: link:../blueprint + devDependencies: + '@esengine/build-config': + specifier: workspace:* + version: link:../build-config + '@esengine/ecs-framework': + specifier: workspace:* + version: link:../core + '@esengine/editor-core': + specifier: workspace:* + version: link:../editor-core + '@esengine/engine-core': + specifier: workspace:* + version: link:../engine-core + '@esengine/node-editor': + specifier: workspace:* + version: link:../node-editor + '@types/react': + specifier: ^18.3.12 + version: 18.3.27 + lucide-react: + specifier: ^0.545.0 + version: 0.545.0(react@18.3.1) + react: + specifier: ^18.3.1 + version: 18.3.1 + rimraf: + specifier: ^5.0.5 + version: 5.0.10 + tsup: + specifier: ^8.0.0 + version: 8.5.1(@microsoft/api-extractor@7.55.1(@types/node@20.19.25))(@swc/core@1.15.3)(jiti@2.6.1)(postcss@8.5.6)(typescript@5.9.3)(yaml@2.8.1) + typescript: + specifier: ^5.3.3 + version: 5.9.3 + zustand: + specifier: ^5.0.8 + version: 5.0.8(@types/react@18.3.27)(react@18.3.1)(use-sync-external-store@1.6.0(react@18.3.1)) + + packages/build-config: + devDependencies: + '@vitejs/plugin-react': + specifier: ^4.3.4 + version: 4.7.0(vite@6.4.1(@types/node@20.19.25)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.1)) + tsup: + specifier: ^8.0.0 + version: 8.5.1(@microsoft/api-extractor@7.55.1(@types/node@20.19.25))(@swc/core@1.15.3)(jiti@2.6.1)(postcss@8.5.6)(typescript@5.9.3)(yaml@2.8.1) typescript: specifier: ^5.8.3 version: 5.9.3 vite: - specifier: ^6.0.7 + specifier: ^6.3.5 version: 6.4.1(@types/node@20.19.25)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.1) vite-plugin-dts: - specifier: ^3.7.0 - version: 3.9.1(@types/node@20.19.25)(rollup@4.53.3)(typescript@5.9.3)(vite@6.4.1(@types/node@20.19.25)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.1)) + specifier: ^4.5.4 + version: 4.5.4(@types/node@20.19.25)(rollup@4.53.3)(typescript@5.9.3)(vite@6.4.1(@types/node@20.19.25)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.1)) - packages/components: - dependencies: - '@esengine/asset-system': - specifier: workspace:* - version: link:../asset-system - '@esengine/ecs-framework': - specifier: ^2.2.8 - version: link:../core - tslib: - specifier: ^2.8.1 - version: 2.8.1 + packages/camera: devDependencies: + '@esengine/build-config': + specifier: workspace:* + version: link:../build-config + '@esengine/ecs-framework': + specifier: workspace:* + version: link:../core + '@esengine/engine-core': + specifier: workspace:* + version: link:../engine-core rimraf: - specifier: ^5.0.0 + specifier: ^5.0.5 version: 5.0.10 + tsup: + specifier: ^8.0.0 + version: 8.5.1(@microsoft/api-extractor@7.55.1(@types/node@20.19.25))(@swc/core@1.15.3)(jiti@2.6.1)(postcss@8.5.6)(typescript@5.9.3)(yaml@2.8.1) typescript: - specifier: ^5.8.3 + specifier: ^5.3.3 + version: 5.9.3 + + packages/camera-editor: + devDependencies: + '@esengine/build-config': + specifier: workspace:* + version: link:../build-config + '@esengine/camera': + specifier: workspace:* + version: link:../camera + '@esengine/ecs-framework': + specifier: workspace:* + version: link:../core + '@esengine/editor-core': + specifier: workspace:* + version: link:../editor-core + '@esengine/engine-core': + specifier: workspace:* + version: link:../engine-core + '@types/react': + specifier: ^18.2.0 + version: 18.3.27 + react: + specifier: ^18.3.1 + version: 18.3.1 + rimraf: + specifier: ^5.0.5 + version: 5.0.10 + tsup: + specifier: ^8.0.0 + version: 8.5.1(@microsoft/api-extractor@7.55.1(@types/node@20.19.25))(@swc/core@1.15.3)(jiti@2.6.1)(postcss@8.5.6)(typescript@5.9.3)(yaml@2.8.1) + typescript: + specifier: ^5.3.3 version: 5.9.3 packages/core: @@ -367,20 +484,28 @@ importers: version: 8.47.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) packages/ecs-engine-bindgen: - dependencies: + devDependencies: '@esengine/asset-system': specifier: workspace:* version: link:../asset-system - '@esengine/ecs-components': + '@esengine/camera': specifier: workspace:* - version: link:../components + version: link:../camera '@esengine/ecs-framework': specifier: workspace:* version: link:../core - devDependencies: + '@esengine/engine-core': + specifier: workspace:* + version: link:../engine-core + '@esengine/sprite': + specifier: workspace:* + version: link:../sprite rimraf: specifier: ^5.0.0 version: 5.0.10 + tsup: + specifier: ^8.5.1 + version: 8.5.1(@microsoft/api-extractor@7.55.1(@types/node@20.19.25))(@swc/core@1.15.3)(jiti@2.6.1)(postcss@8.5.6)(typescript@5.9.3)(yaml@2.8.1) typescript: specifier: ^5.8.0 version: 5.9.3 @@ -394,15 +519,24 @@ importers: '@esengine/asset-system': specifier: workspace:* version: link:../asset-system + '@esengine/audio': + specifier: workspace:* + version: link:../audio '@esengine/behavior-tree': specifier: workspace:* version: link:../behavior-tree + '@esengine/behavior-tree-editor': + specifier: workspace:* + version: link:../behavior-tree-editor '@esengine/blueprint': specifier: workspace:* version: link:../blueprint - '@esengine/ecs-components': + '@esengine/blueprint-editor': specifier: workspace:* - version: link:../components + version: link:../blueprint-editor + '@esengine/camera': + specifier: workspace:* + version: link:../camera '@esengine/ecs-engine-bindgen': specifier: workspace:* version: link:../ecs-engine-bindgen @@ -418,15 +552,33 @@ importers: '@esengine/engine': specifier: workspace:* version: link:../engine + '@esengine/engine-core': + specifier: workspace:* + version: link:../engine-core '@esengine/physics-rapier2d': specifier: workspace:* version: link:../physics-rapier2d + '@esengine/physics-rapier2d-editor': + specifier: workspace:* + version: link:../physics-rapier2d-editor + '@esengine/runtime-core': + specifier: workspace:* + version: link:../runtime-core + '@esengine/sprite': + specifier: workspace:* + version: link:../sprite '@esengine/tilemap': specifier: workspace:* version: link:../tilemap + '@esengine/tilemap-editor': + specifier: workspace:* + version: link:../tilemap-editor '@esengine/ui': specifier: workspace:* version: link:../ui + '@esengine/ui-editor': + specifier: workspace:* + version: link:../ui-editor '@monaco-editor/react': specifier: ^4.7.0 version: 4.7.0(monaco-editor@0.55.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -536,24 +688,9 @@ importers: packages/editor-core: dependencies: - '@esengine/asset-system': - specifier: '*' - version: link:../asset-system - '@esengine/ecs-framework': - specifier: ^2.2.8 - version: link:../core - react: - specifier: ^18.2.0 - version: 18.3.1 - rxjs: - specifier: ^7.8.0 - version: 7.8.2 tslib: specifier: ^2.8.1 version: 2.8.1 - tsyringe: - specifier: ^4.8.0 - version: 4.10.0 devDependencies: '@babel/core': specifier: ^7.28.3 @@ -567,6 +704,15 @@ importers: '@babel/preset-env': specifier: ^7.28.3 version: 7.28.5(@babel/core@7.28.5) + '@esengine/asset-system': + specifier: workspace:* + version: link:../asset-system + '@esengine/ecs-framework': + specifier: workspace:* + version: link:../core + '@esengine/engine-core': + specifier: workspace:* + version: link:../engine-core '@eslint/js': specifier: ^9.37.0 version: 9.39.1 @@ -597,6 +743,9 @@ importers: jest: specifier: ^29.7.0 version: 29.7.0(@types/node@20.19.25) + react: + specifier: ^18.2.0 + version: 18.3.1 rimraf: specifier: ^5.0.0 version: 5.0.10 @@ -606,9 +755,15 @@ importers: rollup-plugin-dts: specifier: ^6.2.1 version: 6.2.3(rollup@4.53.3)(typescript@5.9.3) + rxjs: + specifier: ^7.8.0 + version: 7.8.2 ts-jest: specifier: ^29.4.0 version: 29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.25))(typescript@5.9.3) + tsyringe: + specifier: ^4.8.0 + version: 4.10.0 typescript: specifier: ^5.8.3 version: 5.9.3 @@ -618,15 +773,6 @@ importers: packages/editor-runtime: dependencies: - '@esengine/ecs-framework': - specifier: workspace:* - version: link:../core - '@esengine/editor-core': - specifier: workspace:* - version: link:../editor-core - '@esengine/ui': - specifier: workspace:* - version: link:../ui '@tauri-apps/api': specifier: ^2.2.0 version: 2.9.0 @@ -636,6 +782,28 @@ importers: '@tauri-apps/plugin-fs': specifier: ^2.4.2 version: 2.4.4 + reflect-metadata: + specifier: ^0.2.2 + version: 0.2.2 + tsyringe: + specifier: ^4.10.0 + version: 4.10.0 + devDependencies: + '@esengine/ecs-framework': + specifier: workspace:* + version: link:../core + '@esengine/editor-core': + specifier: workspace:* + version: link:../editor-core + '@esengine/ui': + specifier: workspace:* + version: link:../ui + '@types/react': + specifier: ^18.3.12 + version: 18.3.27 + '@types/react-dom': + specifier: ^18.3.1 + version: 18.3.7(@types/react@18.3.27) lucide-react: specifier: ^0.545.0 version: 0.545.0(react@18.3.1) @@ -645,22 +813,6 @@ importers: react-dom: specifier: ^18.3.1 version: 18.3.1(react@18.3.1) - reflect-metadata: - specifier: ^0.2.2 - version: 0.2.2 - tsyringe: - specifier: ^4.10.0 - version: 4.10.0 - zustand: - specifier: ^5.0.8 - version: 5.0.8(@types/react@18.3.27)(react@18.3.1)(use-sync-external-store@1.6.0(react@18.3.1)) - devDependencies: - '@types/react': - specifier: ^18.3.12 - version: 18.3.27 - '@types/react-dom': - specifier: ^18.3.1 - version: 18.3.7(@types/react@18.3.27) rimraf: specifier: ^5.0.0 version: 5.0.10 @@ -673,6 +825,9 @@ importers: vite-plugin-dts: specifier: ^4.5.0 version: 4.5.4(@types/node@20.19.25)(rollup@4.53.3)(typescript@5.9.3)(vite@6.4.1(@types/node@20.19.25)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.1)) + zustand: + specifier: ^5.0.8 + version: 5.0.8(@types/react@18.3.27)(react@18.3.1)(use-sync-external-store@1.6.0(react@18.3.1)) packages/engine: devDependencies: @@ -680,6 +835,24 @@ importers: specifier: ^5.0.0 version: 5.0.10 + packages/engine-core: + devDependencies: + '@esengine/build-config': + specifier: workspace:* + version: link:../build-config + '@esengine/ecs-framework': + specifier: workspace:* + version: link:../core + rimraf: + specifier: ^5.0.5 + version: 5.0.10 + tsup: + specifier: ^8.0.0 + version: 8.5.1(@microsoft/api-extractor@7.55.1(@types/node@20.19.25))(@swc/core@1.15.3)(jiti@2.6.1)(postcss@8.5.6)(typescript@5.9.3)(yaml@2.8.1) + typescript: + specifier: ^5.3.3 + version: 5.9.3 + packages/math: devDependencies: '@rollup/plugin-commonjs': @@ -721,15 +894,9 @@ importers: packages/node-editor: dependencies: - react: - specifier: ^18.3.1 - version: 18.3.1 tslib: specifier: ^2.8.1 version: 2.8.1 - zustand: - specifier: ^4.5.2 - version: 4.5.7(@types/react@18.3.27)(react@18.3.1) devDependencies: '@types/node': specifier: ^20.19.17 @@ -740,6 +907,9 @@ importers: '@vitejs/plugin-react': specifier: ^4.7.0 version: 4.7.0(vite@6.4.1(@types/node@20.19.25)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.1)) + react: + specifier: ^18.3.1 + version: 18.3.1 rimraf: specifier: ^5.0.0 version: 5.0.10 @@ -752,43 +922,74 @@ importers: vite-plugin-dts: specifier: ^3.7.0 version: 3.9.1(@types/node@20.19.25)(rollup@4.53.3)(typescript@5.9.3)(vite@6.4.1(@types/node@20.19.25)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.1)) + zustand: + specifier: ^5.0.8 + version: 5.0.8(@types/react@18.3.27)(react@18.3.1)(use-sync-external-store@1.6.0(react@18.3.1)) packages/physics-rapier2d: dependencies: '@dimforge/rapier2d-compat': specifier: ^0.14.0 version: 0.14.0 - '@esengine/ecs-components': + devDependencies: + '@esengine/build-config': specifier: workspace:* - version: link:../components + version: link:../build-config '@esengine/ecs-framework': - specifier: '>=2.0.0' + specifier: workspace:* version: link:../core '@esengine/editor-core': specifier: workspace:* version: link:../editor-core - react: - specifier: ^18.3.1 - version: 18.3.1 - devDependencies: - '@types/react': - specifier: ^18.3.12 - version: 18.3.27 - '@vitejs/plugin-react': - specifier: ^4.7.0 - version: 4.7.0(vite@6.4.1(@types/node@20.19.25)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.1)) + '@esengine/engine-core': + specifier: workspace:* + version: link:../engine-core rimraf: specifier: ^5.0.0 version: 5.0.10 + tsup: + specifier: ^8.0.0 + version: 8.5.1(@microsoft/api-extractor@7.55.1(@types/node@20.19.25))(@swc/core@1.15.3)(jiti@2.6.1)(postcss@8.5.6)(typescript@5.9.3)(yaml@2.8.1) typescript: specifier: ^5.8.3 version: 5.9.3 - vite: - specifier: ^6.0.7 - version: 6.4.1(@types/node@20.19.25)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.1) - vite-plugin-dts: - specifier: ^4.5.0 - version: 4.5.4(@types/node@20.19.25)(rollup@4.53.3)(typescript@5.9.3)(vite@6.4.1(@types/node@20.19.25)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.1)) + + packages/physics-rapier2d-editor: + dependencies: + '@esengine/physics-rapier2d': + specifier: workspace:* + version: link:../physics-rapier2d + devDependencies: + '@esengine/build-config': + specifier: workspace:* + version: link:../build-config + '@esengine/ecs-framework': + specifier: workspace:* + version: link:../core + '@esengine/editor-core': + specifier: workspace:* + version: link:../editor-core + '@esengine/engine-core': + specifier: workspace:* + version: link:../engine-core + '@types/react': + specifier: ^18.3.12 + version: 18.3.27 + lucide-react: + specifier: ^0.545.0 + version: 0.545.0(react@18.3.1) + react: + specifier: ^18.3.1 + version: 18.3.1 + rimraf: + specifier: ^5.0.5 + version: 5.0.10 + tsup: + specifier: ^8.0.0 + version: 8.5.1(@microsoft/api-extractor@7.55.1(@types/node@20.19.25))(@swc/core@1.15.3)(jiti@2.6.1)(postcss@8.5.6)(typescript@5.9.3)(yaml@2.8.1) + typescript: + specifier: ^5.3.3 + version: 5.9.3 packages/platform-common: devDependencies: @@ -815,35 +1016,46 @@ importers: version: 5.9.3 packages/platform-web: - dependencies: + devDependencies: '@esengine/asset-system': specifier: workspace:* version: link:../asset-system + '@esengine/audio': + specifier: workspace:* + version: link:../audio '@esengine/behavior-tree': specifier: workspace:* version: link:../behavior-tree - '@esengine/ecs-components': + '@esengine/camera': specifier: workspace:* - version: link:../components + version: link:../camera '@esengine/ecs-engine-bindgen': specifier: workspace:* version: link:../ecs-engine-bindgen '@esengine/ecs-framework': specifier: workspace:* version: link:../core + '@esengine/engine-core': + specifier: workspace:* + version: link:../engine-core '@esengine/physics-rapier2d': specifier: workspace:* version: link:../physics-rapier2d '@esengine/platform-common': specifier: workspace:* version: link:../platform-common + '@esengine/runtime-core': + specifier: workspace:* + version: link:../runtime-core + '@esengine/sprite': + specifier: workspace:* + version: link:../sprite '@esengine/tilemap': specifier: workspace:* version: link:../tilemap '@esengine/ui': specifier: workspace:* version: link:../ui - devDependencies: '@rollup/plugin-alias': specifier: ^6.0.0 version: 6.0.0(rollup@4.53.3) @@ -906,91 +1118,215 @@ importers: specifier: ^5.8.3 version: 5.9.3 - packages/tilemap: - dependencies: + packages/runtime-core: + devDependencies: '@esengine/asset-system': specifier: workspace:* version: link:../asset-system - '@esengine/ecs-components': + '@esengine/build-config': specifier: workspace:* - version: link:../components + version: link:../build-config '@esengine/ecs-engine-bindgen': specifier: workspace:* version: link:../ecs-engine-bindgen '@esengine/ecs-framework': - specifier: ^2.2.8 + specifier: workspace:* version: link:../core - '@esengine/editor-core': + '@esengine/engine-core': specifier: workspace:* - version: link:../editor-core - lucide-react: - specifier: ^0.545.0 - version: 0.545.0(react@18.3.1) - react: - specifier: ^18.3.1 - version: 18.3.1 - tslib: - specifier: ^2.8.1 - version: 2.8.1 - zustand: - specifier: ^5.0.8 - version: 5.0.8(@types/react@18.3.27)(react@18.3.1)(use-sync-external-store@1.6.0(react@18.3.1)) - devDependencies: - '@types/react': - specifier: ^18.3.12 - version: 18.3.27 - '@vitejs/plugin-react': - specifier: ^4.7.0 - version: 4.7.0(vite@6.4.1(@types/node@20.19.25)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.1)) - rimraf: - specifier: ^5.0.0 - version: 5.0.10 - typescript: - specifier: ^5.8.3 - version: 5.9.3 - vite: - specifier: ^6.0.7 - version: 6.4.1(@types/node@20.19.25)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.1) - vite-plugin-dts: - specifier: ^3.7.0 - version: 3.9.1(@types/node@20.19.25)(rollup@4.53.3)(typescript@5.9.3)(vite@6.4.1(@types/node@20.19.25)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.1)) - - packages/ui: - dependencies: - '@esengine/ecs-components': - specifier: workspace:* - version: link:../components - '@esengine/ecs-framework': - specifier: '>=2.0.0' - version: link:../core - '@esengine/editor-core': - specifier: workspace:* - version: link:../editor-core - lucide-react: - specifier: ^0.545.0 - version: 0.545.0(react@18.3.1) - react: - specifier: ^18.3.1 - version: 18.3.1 - devDependencies: - '@types/react': - specifier: ^18.3.12 - version: 18.3.27 - '@vitejs/plugin-react': - specifier: ^4.7.0 - version: 4.7.0(vite@5.4.21(@types/node@20.19.25)(terser@5.44.1)) + version: link:../engine-core rimraf: specifier: ^5.0.5 version: 5.0.10 + tsup: + specifier: ^8.0.0 + version: 8.5.1(@microsoft/api-extractor@7.55.1(@types/node@20.19.25))(@swc/core@1.15.3)(jiti@2.6.1)(postcss@8.5.6)(typescript@5.9.3)(yaml@2.8.1) typescript: specifier: ^5.3.3 version: 5.9.3 - vite: + + packages/sprite: + devDependencies: + '@esengine/asset-system': + specifier: workspace:* + version: link:../asset-system + '@esengine/build-config': + specifier: workspace:* + version: link:../build-config + '@esengine/ecs-framework': + specifier: workspace:* + version: link:../core + '@esengine/engine-core': + specifier: workspace:* + version: link:../engine-core + rimraf: + specifier: ^5.0.5 + version: 5.0.10 + tsup: + specifier: ^8.0.0 + version: 8.5.1(@microsoft/api-extractor@7.55.1(@types/node@20.19.25))(@swc/core@1.15.3)(jiti@2.6.1)(postcss@8.5.6)(typescript@5.9.3)(yaml@2.8.1) + typescript: + specifier: ^5.3.3 + version: 5.9.3 + + packages/sprite-editor: + devDependencies: + '@esengine/build-config': + specifier: workspace:* + version: link:../build-config + '@esengine/ecs-framework': + specifier: workspace:* + version: link:../core + '@esengine/editor-core': + specifier: workspace:* + version: link:../editor-core + '@esengine/engine-core': + specifier: workspace:* + version: link:../engine-core + '@esengine/sprite': + specifier: workspace:* + version: link:../sprite + '@types/react': + specifier: ^18.2.0 + version: 18.3.27 + react: + specifier: ^18.3.1 + version: 18.3.1 + rimraf: + specifier: ^5.0.5 + version: 5.0.10 + tsup: + specifier: ^8.0.0 + version: 8.5.1(@microsoft/api-extractor@7.55.1(@types/node@20.19.25))(@swc/core@1.15.3)(jiti@2.6.1)(postcss@8.5.6)(typescript@5.9.3)(yaml@2.8.1) + typescript: + specifier: ^5.3.3 + version: 5.9.3 + + packages/tilemap: + dependencies: + tslib: + specifier: ^2.8.1 + version: 2.8.1 + devDependencies: + '@esengine/asset-system': + specifier: workspace:* + version: link:../asset-system + '@esengine/build-config': + specifier: workspace:* + version: link:../build-config + '@esengine/ecs-engine-bindgen': + specifier: workspace:* + version: link:../ecs-engine-bindgen + '@esengine/ecs-framework': + specifier: workspace:* + version: link:../core + '@esengine/engine-core': + specifier: workspace:* + version: link:../engine-core + rimraf: specifier: ^5.0.0 - version: 5.4.21(@types/node@20.19.25)(terser@5.44.1) - vite-plugin-dts: - specifier: ^3.7.0 - version: 3.9.1(@types/node@20.19.25)(rollup@4.53.3)(typescript@5.9.3)(vite@5.4.21(@types/node@20.19.25)(terser@5.44.1)) + version: 5.0.10 + tsup: + specifier: ^8.0.0 + version: 8.5.1(@microsoft/api-extractor@7.55.1(@types/node@20.19.25))(@swc/core@1.15.3)(jiti@2.6.1)(postcss@8.5.6)(typescript@5.9.3)(yaml@2.8.1) + typescript: + specifier: ^5.8.3 + version: 5.9.3 + + packages/tilemap-editor: + dependencies: + '@esengine/tilemap': + specifier: workspace:* + version: link:../tilemap + devDependencies: + '@esengine/build-config': + specifier: workspace:* + version: link:../build-config + '@esengine/ecs-framework': + specifier: workspace:* + version: link:../core + '@esengine/editor-core': + specifier: workspace:* + version: link:../editor-core + '@esengine/engine-core': + specifier: workspace:* + version: link:../engine-core + '@types/react': + specifier: ^18.3.12 + version: 18.3.27 + lucide-react: + specifier: ^0.545.0 + version: 0.545.0(react@18.3.1) + react: + specifier: ^18.3.1 + version: 18.3.1 + rimraf: + specifier: ^5.0.5 + version: 5.0.10 + tsup: + specifier: ^8.0.0 + version: 8.5.1(@microsoft/api-extractor@7.55.1(@types/node@20.19.25))(@swc/core@1.15.3)(jiti@2.6.1)(postcss@8.5.6)(typescript@5.9.3)(yaml@2.8.1) + typescript: + specifier: ^5.3.3 + version: 5.9.3 + zustand: + specifier: ^5.0.8 + version: 5.0.8(@types/react@18.3.27)(react@18.3.1)(use-sync-external-store@1.6.0(react@18.3.1)) + + packages/ui: + devDependencies: + '@esengine/build-config': + specifier: workspace:* + version: link:../build-config + '@esengine/ecs-framework': + specifier: workspace:* + version: link:../core + '@esengine/engine-core': + specifier: workspace:* + version: link:../engine-core + rimraf: + specifier: ^5.0.5 + version: 5.0.10 + tsup: + specifier: ^8.0.0 + version: 8.5.1(@microsoft/api-extractor@7.55.1(@types/node@20.19.25))(@swc/core@1.15.3)(jiti@2.6.1)(postcss@8.5.6)(typescript@5.9.3)(yaml@2.8.1) + typescript: + specifier: ^5.3.3 + version: 5.9.3 + + packages/ui-editor: + dependencies: + '@esengine/ui': + specifier: workspace:* + version: link:../ui + devDependencies: + '@esengine/build-config': + specifier: workspace:* + version: link:../build-config + '@esengine/ecs-framework': + specifier: workspace:* + version: link:../core + '@esengine/editor-core': + specifier: workspace:* + version: link:../editor-core + '@types/react': + specifier: ^18.3.12 + version: 18.3.27 + lucide-react: + specifier: ^0.545.0 + version: 0.545.0(react@18.3.1) + react: + specifier: ^18.3.1 + version: 18.3.1 + rimraf: + specifier: ^5.0.5 + version: 5.0.10 + tsup: + specifier: ^8.0.0 + version: 8.5.1(@microsoft/api-extractor@7.55.1(@types/node@20.19.25))(@swc/core@1.15.3)(jiti@2.6.1)(postcss@8.5.6)(typescript@5.9.3)(yaml@2.8.1) + typescript: + specifier: ^5.3.3 + version: 5.9.3 packages: @@ -1791,6 +2127,12 @@ packages: cpu: [ppc64] os: [aix] + '@esbuild/aix-ppc64@0.27.0': + resolution: {integrity: sha512-KuZrd2hRjz01y5JK9mEBSD3Vj3mbCvemhT466rSuJYeE/hjuBrHfjjcjMdTm/sz7au+++sdbJZJmuBwQLuw68A==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + '@esbuild/android-arm64@0.21.5': resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} engines: {node: '>=12'} @@ -1803,6 +2145,12 @@ packages: cpu: [arm64] os: [android] + '@esbuild/android-arm64@0.27.0': + resolution: {integrity: sha512-CC3vt4+1xZrs97/PKDkl0yN7w8edvU2vZvAFGD16n9F0Cvniy5qvzRXjfO1l94efczkkQE6g1x0i73Qf5uthOQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm@0.21.5': resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} engines: {node: '>=12'} @@ -1815,6 +2163,12 @@ packages: cpu: [arm] os: [android] + '@esbuild/android-arm@0.27.0': + resolution: {integrity: sha512-j67aezrPNYWJEOHUNLPj9maeJte7uSMM6gMoxfPC9hOg8N02JuQi/T7ewumf4tNvJadFkvLZMlAq73b9uwdMyQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + '@esbuild/android-x64@0.21.5': resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} engines: {node: '>=12'} @@ -1827,6 +2181,12 @@ packages: cpu: [x64] os: [android] + '@esbuild/android-x64@0.27.0': + resolution: {integrity: sha512-wurMkF1nmQajBO1+0CJmcN17U4BP6GqNSROP8t0X/Jiw2ltYGLHpEksp9MpoBqkrFR3kv2/te6Sha26k3+yZ9Q==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + '@esbuild/darwin-arm64@0.21.5': resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} engines: {node: '>=12'} @@ -1839,6 +2199,12 @@ packages: cpu: [arm64] os: [darwin] + '@esbuild/darwin-arm64@0.27.0': + resolution: {integrity: sha512-uJOQKYCcHhg07DL7i8MzjvS2LaP7W7Pn/7uA0B5S1EnqAirJtbyw4yC5jQ5qcFjHK9l6o/MX9QisBg12kNkdHg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-x64@0.21.5': resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} engines: {node: '>=12'} @@ -1851,6 +2217,12 @@ packages: cpu: [x64] os: [darwin] + '@esbuild/darwin-x64@0.27.0': + resolution: {integrity: sha512-8mG6arH3yB/4ZXiEnXof5MK72dE6zM9cDvUcPtxhUZsDjESl9JipZYW60C3JGreKCEP+p8P/72r69m4AZGJd5g==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + '@esbuild/freebsd-arm64@0.21.5': resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} engines: {node: '>=12'} @@ -1863,6 +2235,12 @@ packages: cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-arm64@0.27.0': + resolution: {integrity: sha512-9FHtyO988CwNMMOE3YIeci+UV+x5Zy8fI2qHNpsEtSF83YPBmE8UWmfYAQg6Ux7Gsmd4FejZqnEUZCMGaNQHQw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-x64@0.21.5': resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} engines: {node: '>=12'} @@ -1875,6 +2253,12 @@ packages: cpu: [x64] os: [freebsd] + '@esbuild/freebsd-x64@0.27.0': + resolution: {integrity: sha512-zCMeMXI4HS/tXvJz8vWGexpZj2YVtRAihHLk1imZj4efx1BQzN76YFeKqlDr3bUWI26wHwLWPd3rwh6pe4EV7g==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + '@esbuild/linux-arm64@0.21.5': resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} engines: {node: '>=12'} @@ -1887,6 +2271,12 @@ packages: cpu: [arm64] os: [linux] + '@esbuild/linux-arm64@0.27.0': + resolution: {integrity: sha512-AS18v0V+vZiLJyi/4LphvBE+OIX682Pu7ZYNsdUHyUKSoRwdnOsMf6FDekwoAFKej14WAkOef3zAORJgAtXnlQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm@0.21.5': resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} engines: {node: '>=12'} @@ -1899,6 +2289,12 @@ packages: cpu: [arm] os: [linux] + '@esbuild/linux-arm@0.27.0': + resolution: {integrity: sha512-t76XLQDpxgmq2cNXKTVEB7O7YMb42atj2Re2Haf45HkaUpjM2J0UuJZDuaGbPbamzZ7bawyGFUkodL+zcE+jvQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + '@esbuild/linux-ia32@0.21.5': resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} engines: {node: '>=12'} @@ -1911,6 +2307,12 @@ packages: cpu: [ia32] os: [linux] + '@esbuild/linux-ia32@0.27.0': + resolution: {integrity: sha512-Mz1jxqm/kfgKkc/KLHC5qIujMvnnarD9ra1cEcrs7qshTUSksPihGrWHVG5+osAIQ68577Zpww7SGapmzSt4Nw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-loong64@0.21.5': resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} engines: {node: '>=12'} @@ -1923,6 +2325,12 @@ packages: cpu: [loong64] os: [linux] + '@esbuild/linux-loong64@0.27.0': + resolution: {integrity: sha512-QbEREjdJeIreIAbdG2hLU1yXm1uu+LTdzoq1KCo4G4pFOLlvIspBm36QrQOar9LFduavoWX2msNFAAAY9j4BDg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-mips64el@0.21.5': resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} engines: {node: '>=12'} @@ -1935,6 +2343,12 @@ packages: cpu: [mips64el] os: [linux] + '@esbuild/linux-mips64el@0.27.0': + resolution: {integrity: sha512-sJz3zRNe4tO2wxvDpH/HYJilb6+2YJxo/ZNbVdtFiKDufzWq4JmKAiHy9iGoLjAV7r/W32VgaHGkk35cUXlNOg==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-ppc64@0.21.5': resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} engines: {node: '>=12'} @@ -1947,6 +2361,12 @@ packages: cpu: [ppc64] os: [linux] + '@esbuild/linux-ppc64@0.27.0': + resolution: {integrity: sha512-z9N10FBD0DCS2dmSABDBb5TLAyF1/ydVb+N4pi88T45efQ/w4ohr/F/QYCkxDPnkhkp6AIpIcQKQ8F0ANoA2JA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-riscv64@0.21.5': resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} engines: {node: '>=12'} @@ -1959,6 +2379,12 @@ packages: cpu: [riscv64] os: [linux] + '@esbuild/linux-riscv64@0.27.0': + resolution: {integrity: sha512-pQdyAIZ0BWIC5GyvVFn5awDiO14TkT/19FTmFcPdDec94KJ1uZcmFs21Fo8auMXzD4Tt+diXu1LW1gHus9fhFQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-s390x@0.21.5': resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} engines: {node: '>=12'} @@ -1971,6 +2397,12 @@ packages: cpu: [s390x] os: [linux] + '@esbuild/linux-s390x@0.27.0': + resolution: {integrity: sha512-hPlRWR4eIDDEci953RI1BLZitgi5uqcsjKMxwYfmi4LcwyWo2IcRP+lThVnKjNtk90pLS8nKdroXYOqW+QQH+w==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-x64@0.21.5': resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} engines: {node: '>=12'} @@ -1983,12 +2415,24 @@ packages: cpu: [x64] os: [linux] + '@esbuild/linux-x64@0.27.0': + resolution: {integrity: sha512-1hBWx4OUJE2cab++aVZ7pObD6s+DK4mPGpemtnAORBvb5l/g5xFGk0vc0PjSkrDs0XaXj9yyob3d14XqvnQ4gw==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + '@esbuild/netbsd-arm64@0.25.12': resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] + '@esbuild/netbsd-arm64@0.27.0': + resolution: {integrity: sha512-6m0sfQfxfQfy1qRuecMkJlf1cIzTOgyaeXaiVaaki8/v+WB+U4hc6ik15ZW6TAllRlg/WuQXxWj1jx6C+dfy3w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + '@esbuild/netbsd-x64@0.21.5': resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} engines: {node: '>=12'} @@ -2001,12 +2445,24 @@ packages: cpu: [x64] os: [netbsd] + '@esbuild/netbsd-x64@0.27.0': + resolution: {integrity: sha512-xbbOdfn06FtcJ9d0ShxxvSn2iUsGd/lgPIO2V3VZIPDbEaIj1/3nBBe1AwuEZKXVXkMmpr6LUAgMkLD/4D2PPA==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + '@esbuild/openbsd-arm64@0.25.12': resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] + '@esbuild/openbsd-arm64@0.27.0': + resolution: {integrity: sha512-fWgqR8uNbCQ/GGv0yhzttj6sU/9Z5/Sv/VGU3F5OuXK6J6SlriONKrQ7tNlwBrJZXRYk5jUhuWvF7GYzGguBZQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + '@esbuild/openbsd-x64@0.21.5': resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} engines: {node: '>=12'} @@ -2019,12 +2475,24 @@ packages: cpu: [x64] os: [openbsd] + '@esbuild/openbsd-x64@0.27.0': + resolution: {integrity: sha512-aCwlRdSNMNxkGGqQajMUza6uXzR/U0dIl1QmLjPtRbLOx3Gy3otfFu/VjATy4yQzo9yFDGTxYDo1FfAD9oRD2A==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + '@esbuild/openharmony-arm64@0.25.12': resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] + '@esbuild/openharmony-arm64@0.27.0': + resolution: {integrity: sha512-nyvsBccxNAsNYz2jVFYwEGuRRomqZ149A39SHWk4hV0jWxKM0hjBPm3AmdxcbHiFLbBSwG6SbpIcUbXjgyECfA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + '@esbuild/sunos-x64@0.21.5': resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} engines: {node: '>=12'} @@ -2037,6 +2505,12 @@ packages: cpu: [x64] os: [sunos] + '@esbuild/sunos-x64@0.27.0': + resolution: {integrity: sha512-Q1KY1iJafM+UX6CFEL+F4HRTgygmEW568YMqDA5UV97AuZSm21b7SXIrRJDwXWPzr8MGr75fUZPV67FdtMHlHA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + '@esbuild/win32-arm64@0.21.5': resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} engines: {node: '>=12'} @@ -2049,6 +2523,12 @@ packages: cpu: [arm64] os: [win32] + '@esbuild/win32-arm64@0.27.0': + resolution: {integrity: sha512-W1eyGNi6d+8kOmZIwi/EDjrL9nxQIQ0MiGqe/AWc6+IaHloxHSGoeRgDRKHFISThLmsewZ5nHFvGFWdBYlgKPg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-ia32@0.21.5': resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} engines: {node: '>=12'} @@ -2061,6 +2541,12 @@ packages: cpu: [ia32] os: [win32] + '@esbuild/win32-ia32@0.27.0': + resolution: {integrity: sha512-30z1aKL9h22kQhilnYkORFYt+3wp7yZsHWus+wSKAJR8JtdfI76LJ4SBdMsCopTR3z/ORqVu5L1vtnHZWVj4cQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-x64@0.21.5': resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} engines: {node: '>=12'} @@ -2073,6 +2559,12 @@ packages: cpu: [x64] os: [win32] + '@esbuild/win32-x64@0.27.0': + resolution: {integrity: sha512-aIitBcjQeyOhMTImhLZmtxfdOcuNRpwlPNmlFKPcHQYPhEssw75Cl1TSXJXpMkzaua9FUetx/4OQKq7eJul5Cg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@eslint-community/eslint-utils@4.9.0': resolution: {integrity: sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -4002,6 +4494,12 @@ packages: buffer@5.7.1: resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + bundle-require@5.1.0: + resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + peerDependencies: + esbuild: '>=0.18' + byte-size@8.1.1: resolution: {integrity: sha512-tUkzZWK0M/qdoLEqikxBWe4kumyuwjl3HO6zHTr4yEI23EojPtLYXdG1+AQY7MN0cGyNDvEaJ8wiYQm6P2bPxg==} engines: {node: '>=12.17'} @@ -4010,6 +4508,10 @@ packages: resolution: {integrity: sha512-fey6+4jDK7TFtFg/klGSvNKJctyU7n2aQdnM+CO0ruLPbqqMOM8Tio0Pc+deqUeVKX1tL5DQep1zQ7+37aTAsA==} engines: {node: '>= 0.8'} + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + cacache@18.0.4: resolution: {integrity: sha512-B+L5iIa9mgcjLbliir2th36yEwPftrzteHYujzsx3dFP/31GCHcIeS8f5MGd80odLOjaOvSpU3EEAmRQptkxLQ==} engines: {node: ^16.14.0 || >=18.0.0} @@ -4193,6 +4695,10 @@ packages: commander@2.20.3: resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + commander@4.1.1: + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} + commander@9.5.0: resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==} engines: {node: ^12.20.0 || >=14} @@ -4228,6 +4734,10 @@ packages: config-chain@1.1.13: resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==} + consola@3.4.2: + resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} + engines: {node: ^14.18.0 || >=16.10.0} + console-control-strings@1.1.0: resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==} @@ -4598,6 +5108,11 @@ packages: engines: {node: '>=18'} hasBin: true + esbuild@0.27.0: + resolution: {integrity: sha512-jd0f4NHbD6cALCyGElNpGAOtWxSq46l9X/sWB0Nzd5er4Kz2YTm+Vl0qKFT9KUJvD8+fiO8AvoHhFvEatfVixA==} + engines: {node: '>=18'} + hasBin: true + escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} @@ -4801,6 +5316,9 @@ packages: resolution: {integrity: sha512-2kCCtc+JvcZ86IGAz3Z2Y0A1baIz9fL31pH/0S1IqZr9Iwnjq8izfPtrCyQKO6TLMPELLsQMre7VDqeIKCsHkA==} engines: {node: '>=18'} + fix-dts-default-cjs-exports@1.0.1: + resolution: {integrity: sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==} + flat-cache@4.0.1: resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} engines: {node: '>=16'} @@ -5597,6 +6115,10 @@ packages: jju@1.4.0: resolution: {integrity: sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==} + joycon@3.1.1: + resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} + engines: {node: '>=10'} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -5745,6 +6267,10 @@ packages: resolution: {integrity: sha512-gUD/epcRms75Cw8RT1pUdHugZYM5ce64ucs2GEISABwkRsOQr0q2wm/MV2TKThycIe5e0ytRweW2RZxclogCdQ==} engines: {node: '>=8'} + load-tsconfig@0.2.5: + resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + local-pkg@1.1.2: resolution: {integrity: sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==} engines: {node: '>=14'} @@ -6735,6 +7261,24 @@ packages: resolution: {integrity: sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==} engines: {node: '>=8'} + postcss-load-config@6.0.1: + resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} + engines: {node: '>= 18'} + peerDependencies: + jiti: '>=1.21.0' + postcss: '>=8.0.9' + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + jiti: + optional: true + postcss: + optional: true + tsx: + optional: true + yaml: + optional: true + postcss-selector-parser@6.1.2: resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} engines: {node: '>=4'} @@ -7262,6 +7806,10 @@ packages: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} + source-map@0.7.6: + resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} + engines: {node: '>= 12'} + space-separated-tokens@2.0.2: resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} @@ -7385,6 +7933,11 @@ packages: style-to-object@1.0.14: resolution: {integrity: sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==} + sucrase@3.35.1: + resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + super-regex@1.1.0: resolution: {integrity: sha512-WHkws2ZflZe41zj6AolvvmaTrWds/VuyeYr9iPVv/oQeaIoVxMKaushfFWpOGDT+GuBrM/sVqF8KUCYQlSSTdQ==} engines: {node: '>=18'} @@ -7484,6 +8037,9 @@ packages: resolution: {integrity: sha512-75voc/9G4rDIJleOo4jPvN4/YC4GRZrY8yy1uU4lwrB3XEQbWve8zXoO5No4eFrGcTAMYyoY67p8jRQdtA1HbA==} engines: {node: '>=12'} + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + tinyexec@1.0.2: resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==} engines: {node: '>=18'} @@ -7526,6 +8082,10 @@ packages: resolution: {integrity: sha512-aXJDbk6SnumuaZSANd21XAo15ucCDE38H4fkqiGsc3MhCK+wOlZvLP9cB/TvpHT0mOyWgC4Z8EwRlzqYSUzdsA==} engines: {node: '>= 0.4'} + tree-kill@1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + hasBin: true + treeverse@3.0.0: resolution: {integrity: sha512-gcANaAnd2QDZFmHFEOF4k7uc1J/6a6z3DJMd/QwEyxLoKGiptJRwid582r7QIsFlFMIZ3SnxfS52S4hm2DHkuQ==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} @@ -7546,6 +8106,9 @@ packages: peerDependencies: typescript: '>=4.8.4' + ts-interface-checker@0.1.13: + resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + ts-jest@29.4.5: resolution: {integrity: sha512-HO3GyiWn2qvTQA4kTgjDcXiMwYQt68a1Y8+JuLRVpdIzm+UOLSHgl/XqR4c6nzJkq5rOkjc02O2I7P7l/Yof0Q==} engines: {node: ^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0} @@ -7583,6 +8146,25 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + tsup@8.5.1: + resolution: {integrity: sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + '@microsoft/api-extractor': ^7.36.0 + '@swc/core': ^1 + postcss: ^8.4.12 + typescript: '>=4.5.0' + peerDependenciesMeta: + '@microsoft/api-extractor': + optional: true + '@swc/core': + optional: true + postcss: + optional: true + typescript: + optional: true + tsyringe@4.10.0: resolution: {integrity: sha512-axr3IdNuVIxnaK5XGEUFTu3YmAQ6lllgrvqfEoR16g/HGnYY/6We4oWENtAnzK6/LpJ2ur9PAb80RBt7/U4ugw==} engines: {node: '>= 6.0.0'} @@ -7591,6 +8173,40 @@ packages: resolution: {integrity: sha512-GwIJau9XaA8nLVbUXsN3IlFi7WmQ48gBUrl3FTkkL/XLu/POhBzfmX9hd33FNMX1qAsfl6ozO1iMmW9NC8YniA==} engines: {node: ^16.14.0 || >=18.0.0} + turbo-darwin-64@2.6.1: + resolution: {integrity: sha512-Dm0HwhyZF4J0uLqkhUyCVJvKM9Rw7M03v3J9A7drHDQW0qAbIGBrUijQ8g4Q9Cciw/BXRRd8Uzkc3oue+qn+ZQ==} + cpu: [x64] + os: [darwin] + + turbo-darwin-arm64@2.6.1: + resolution: {integrity: sha512-U0PIPTPyxdLsrC3jN7jaJUwgzX5sVUBsKLO7+6AL+OASaa1NbT1pPdiZoTkblBAALLP76FM0LlnsVQOnmjYhyw==} + cpu: [arm64] + os: [darwin] + + turbo-linux-64@2.6.1: + resolution: {integrity: sha512-eM1uLWgzv89bxlK29qwQEr9xYWBhmO/EGiH22UGfq+uXr+QW1OvNKKMogSN65Ry8lElMH4LZh0aX2DEc7eC0Mw==} + cpu: [x64] + os: [linux] + + turbo-linux-arm64@2.6.1: + resolution: {integrity: sha512-MFFh7AxAQAycXKuZDrbeutfWM5Ep0CEZ9u7zs4Hn2FvOViTCzIfEhmuJou3/a5+q5VX1zTxQrKGy+4Lf5cdpsA==} + cpu: [arm64] + os: [linux] + + turbo-windows-64@2.6.1: + resolution: {integrity: sha512-buq7/VAN7KOjMYi4tSZT5m+jpqyhbRU2EUTTvp6V0Ii8dAkY2tAAjQN1q5q2ByflYWKecbQNTqxmVploE0LVwQ==} + cpu: [x64] + os: [win32] + + turbo-windows-arm64@2.6.1: + resolution: {integrity: sha512-7w+AD5vJp3R+FB0YOj1YJcNcOOvBior7bcHTodqp90S3x3bLgpr7tE6xOea1e8JkP7GK6ciKVUpQvV7psiwU5Q==} + cpu: [arm64] + os: [win32] + + turbo@2.6.1: + resolution: {integrity: sha512-qBwXXuDT3rA53kbNafGbT5r++BrhRgx3sAo0cHoDAeG9g1ItTmUMgltz3Hy7Hazy1ODqNpR+C7QwqL6DYB52yA==} + hasBin: true + type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} @@ -8171,21 +8787,6 @@ packages: engines: {node: '>=8.0.0'} hasBin: true - zustand@4.5.7: - resolution: {integrity: sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==} - engines: {node: '>=12.7.0'} - peerDependencies: - '@types/react': '>=16.8' - immer: '>=9.0.6' - react: '>=16.8' - peerDependenciesMeta: - '@types/react': - optional: true - immer: - optional: true - react: - optional: true - zustand@5.0.8: resolution: {integrity: sha512-gyPKpIaxY9XcO2vSMrLbiER7QMAMGOQZVRdJ6Zi782jkbzZygq5GI9nG8g+sMgitRtndwaBSl7uiqC49o1SSiw==} engines: {node: '>=12.20.0'} @@ -9238,147 +9839,225 @@ snapshots: '@esbuild/aix-ppc64@0.25.12': optional: true + '@esbuild/aix-ppc64@0.27.0': + optional: true + '@esbuild/android-arm64@0.21.5': optional: true '@esbuild/android-arm64@0.25.12': optional: true + '@esbuild/android-arm64@0.27.0': + optional: true + '@esbuild/android-arm@0.21.5': optional: true '@esbuild/android-arm@0.25.12': optional: true + '@esbuild/android-arm@0.27.0': + optional: true + '@esbuild/android-x64@0.21.5': optional: true '@esbuild/android-x64@0.25.12': optional: true + '@esbuild/android-x64@0.27.0': + optional: true + '@esbuild/darwin-arm64@0.21.5': optional: true '@esbuild/darwin-arm64@0.25.12': optional: true + '@esbuild/darwin-arm64@0.27.0': + optional: true + '@esbuild/darwin-x64@0.21.5': optional: true '@esbuild/darwin-x64@0.25.12': optional: true + '@esbuild/darwin-x64@0.27.0': + optional: true + '@esbuild/freebsd-arm64@0.21.5': optional: true '@esbuild/freebsd-arm64@0.25.12': optional: true + '@esbuild/freebsd-arm64@0.27.0': + optional: true + '@esbuild/freebsd-x64@0.21.5': optional: true '@esbuild/freebsd-x64@0.25.12': optional: true + '@esbuild/freebsd-x64@0.27.0': + optional: true + '@esbuild/linux-arm64@0.21.5': optional: true '@esbuild/linux-arm64@0.25.12': optional: true + '@esbuild/linux-arm64@0.27.0': + optional: true + '@esbuild/linux-arm@0.21.5': optional: true '@esbuild/linux-arm@0.25.12': optional: true + '@esbuild/linux-arm@0.27.0': + optional: true + '@esbuild/linux-ia32@0.21.5': optional: true '@esbuild/linux-ia32@0.25.12': optional: true + '@esbuild/linux-ia32@0.27.0': + optional: true + '@esbuild/linux-loong64@0.21.5': optional: true '@esbuild/linux-loong64@0.25.12': optional: true + '@esbuild/linux-loong64@0.27.0': + optional: true + '@esbuild/linux-mips64el@0.21.5': optional: true '@esbuild/linux-mips64el@0.25.12': optional: true + '@esbuild/linux-mips64el@0.27.0': + optional: true + '@esbuild/linux-ppc64@0.21.5': optional: true '@esbuild/linux-ppc64@0.25.12': optional: true + '@esbuild/linux-ppc64@0.27.0': + optional: true + '@esbuild/linux-riscv64@0.21.5': optional: true '@esbuild/linux-riscv64@0.25.12': optional: true + '@esbuild/linux-riscv64@0.27.0': + optional: true + '@esbuild/linux-s390x@0.21.5': optional: true '@esbuild/linux-s390x@0.25.12': optional: true + '@esbuild/linux-s390x@0.27.0': + optional: true + '@esbuild/linux-x64@0.21.5': optional: true '@esbuild/linux-x64@0.25.12': optional: true + '@esbuild/linux-x64@0.27.0': + optional: true + '@esbuild/netbsd-arm64@0.25.12': optional: true + '@esbuild/netbsd-arm64@0.27.0': + optional: true + '@esbuild/netbsd-x64@0.21.5': optional: true '@esbuild/netbsd-x64@0.25.12': optional: true + '@esbuild/netbsd-x64@0.27.0': + optional: true + '@esbuild/openbsd-arm64@0.25.12': optional: true + '@esbuild/openbsd-arm64@0.27.0': + optional: true + '@esbuild/openbsd-x64@0.21.5': optional: true '@esbuild/openbsd-x64@0.25.12': optional: true + '@esbuild/openbsd-x64@0.27.0': + optional: true + '@esbuild/openharmony-arm64@0.25.12': optional: true + '@esbuild/openharmony-arm64@0.27.0': + optional: true + '@esbuild/sunos-x64@0.21.5': optional: true '@esbuild/sunos-x64@0.25.12': optional: true + '@esbuild/sunos-x64@0.27.0': + optional: true + '@esbuild/win32-arm64@0.21.5': optional: true '@esbuild/win32-arm64@0.25.12': optional: true + '@esbuild/win32-arm64@0.27.0': + optional: true + '@esbuild/win32-ia32@0.21.5': optional: true '@esbuild/win32-ia32@0.25.12': optional: true + '@esbuild/win32-ia32@0.27.0': + optional: true + '@esbuild/win32-x64@0.21.5': optional: true '@esbuild/win32-x64@0.25.12': optional: true + '@esbuild/win32-x64@0.27.0': + optional: true + '@eslint-community/eslint-utils@4.9.0(eslint@9.39.1(jiti@2.6.1))': dependencies: eslint: 9.39.1(jiti@2.6.1) @@ -11243,18 +11922,6 @@ snapshots: transitivePeerDependencies: - '@swc/helpers' - '@vitejs/plugin-react@4.7.0(vite@5.4.21(@types/node@20.19.25)(terser@5.44.1))': - dependencies: - '@babel/core': 7.28.5 - '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.5) - '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.28.5) - '@rolldown/pluginutils': 1.0.0-beta.27 - '@types/babel__core': 7.20.5 - react-refresh: 0.17.0 - vite: 5.4.21(@types/node@20.19.25)(terser@5.44.1) - transitivePeerDependencies: - - supports-color - '@vitejs/plugin-react@4.7.0(vite@6.4.1(@types/node@20.19.25)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.1))': dependencies: '@babel/core': 7.28.5 @@ -11765,10 +12432,17 @@ snapshots: base64-js: 1.5.1 ieee754: 1.2.1 + bundle-require@5.1.0(esbuild@0.27.0): + dependencies: + esbuild: 0.27.0 + load-tsconfig: 0.2.5 + byte-size@8.1.1: {} bytes-iec@3.1.1: {} + cac@6.7.14: {} + cacache@18.0.4: dependencies: '@npmcli/fs': 3.1.1 @@ -11941,6 +12615,8 @@ snapshots: commander@2.20.3: {} + commander@4.1.1: {} + commander@9.5.0: optional: true @@ -11975,6 +12651,8 @@ snapshots: ini: 1.3.8 proto-list: 1.2.4 + consola@3.4.2: {} + console-control-strings@1.1.0: {} conventional-changelog-angular@7.0.0: @@ -12364,6 +13042,35 @@ snapshots: '@esbuild/win32-ia32': 0.25.12 '@esbuild/win32-x64': 0.25.12 + esbuild@0.27.0: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.0 + '@esbuild/android-arm': 0.27.0 + '@esbuild/android-arm64': 0.27.0 + '@esbuild/android-x64': 0.27.0 + '@esbuild/darwin-arm64': 0.27.0 + '@esbuild/darwin-x64': 0.27.0 + '@esbuild/freebsd-arm64': 0.27.0 + '@esbuild/freebsd-x64': 0.27.0 + '@esbuild/linux-arm': 0.27.0 + '@esbuild/linux-arm64': 0.27.0 + '@esbuild/linux-ia32': 0.27.0 + '@esbuild/linux-loong64': 0.27.0 + '@esbuild/linux-mips64el': 0.27.0 + '@esbuild/linux-ppc64': 0.27.0 + '@esbuild/linux-riscv64': 0.27.0 + '@esbuild/linux-s390x': 0.27.0 + '@esbuild/linux-x64': 0.27.0 + '@esbuild/netbsd-arm64': 0.27.0 + '@esbuild/netbsd-x64': 0.27.0 + '@esbuild/openbsd-arm64': 0.27.0 + '@esbuild/openbsd-x64': 0.27.0 + '@esbuild/openharmony-arm64': 0.27.0 + '@esbuild/sunos-x64': 0.27.0 + '@esbuild/win32-arm64': 0.27.0 + '@esbuild/win32-ia32': 0.27.0 + '@esbuild/win32-x64': 0.27.0 + escalade@3.2.0: {} escape-string-regexp@1.0.5: {} @@ -12612,6 +13319,12 @@ snapshots: semver-regex: 4.0.5 super-regex: 1.1.0 + fix-dts-default-cjs-exports@1.0.1: + dependencies: + magic-string: 0.30.21 + mlly: 1.8.0 + rollup: 4.53.3 + flat-cache@4.0.1: dependencies: flatted: 3.3.3 @@ -13664,6 +14377,8 @@ snapshots: jju@1.4.0: {} + joycon@3.1.1: {} + js-tokens@4.0.0: {} js-yaml@3.14.2: @@ -13921,6 +14636,8 @@ snapshots: strip-bom: 4.0.0 type-fest: 0.6.0 + load-tsconfig@0.2.5: {} + local-pkg@1.1.2: dependencies: mlly: 1.8.0 @@ -15091,6 +15808,14 @@ snapshots: dependencies: find-up: 3.0.0 + postcss-load-config@6.0.1(jiti@2.6.1)(postcss@8.5.6)(yaml@2.8.1): + dependencies: + lilconfig: 3.1.3 + optionalDependencies: + jiti: 2.6.1 + postcss: 8.5.6 + yaml: 2.8.1 + postcss-selector-parser@6.1.2: dependencies: cssesc: 3.0.0 @@ -15737,6 +16462,8 @@ snapshots: source-map@0.6.1: {} + source-map@0.7.6: {} + space-separated-tokens@2.0.2: {} spawn-error-forwarder@1.0.0: {} @@ -15854,6 +16581,16 @@ snapshots: dependencies: inline-style-parser: 0.2.7 + sucrase@3.35.1: + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + commander: 4.1.1 + lines-and-columns: 1.2.4 + mz: 2.7.0 + pirates: 4.0.7 + tinyglobby: 0.2.15 + ts-interface-checker: 0.1.13 + super-regex@1.1.0: dependencies: function-timeout: 1.0.2 @@ -15965,6 +16702,8 @@ snapshots: dependencies: convert-hrtime: 5.0.0 + tinyexec@0.3.2: {} + tinyexec@1.0.2: {} tinyglobby@0.2.12: @@ -16004,6 +16743,8 @@ snapshots: traverse@0.6.8: {} + tree-kill@1.2.2: {} + treeverse@3.0.0: {} trim-lines@3.0.1: {} @@ -16016,6 +16757,8 @@ snapshots: dependencies: typescript: 5.9.3 + ts-interface-checker@0.1.13: {} + ts-jest@29.4.5(@babel/core@7.28.5)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.28.5))(jest-util@29.7.0)(jest@29.7.0(@types/node@20.19.25))(typescript@5.9.3): dependencies: bs-logger: 0.2.6 @@ -16046,6 +16789,36 @@ snapshots: tslib@2.8.1: {} + tsup@8.5.1(@microsoft/api-extractor@7.55.1(@types/node@20.19.25))(@swc/core@1.15.3)(jiti@2.6.1)(postcss@8.5.6)(typescript@5.9.3)(yaml@2.8.1): + dependencies: + bundle-require: 5.1.0(esbuild@0.27.0) + cac: 6.7.14 + chokidar: 4.0.3 + consola: 3.4.2 + debug: 4.4.3 + esbuild: 0.27.0 + fix-dts-default-cjs-exports: 1.0.1 + joycon: 3.1.1 + picocolors: 1.1.1 + postcss-load-config: 6.0.1(jiti@2.6.1)(postcss@8.5.6)(yaml@2.8.1) + resolve-from: 5.0.0 + rollup: 4.53.3 + source-map: 0.7.6 + sucrase: 3.35.1 + tinyexec: 0.3.2 + tinyglobby: 0.2.15 + tree-kill: 1.2.2 + optionalDependencies: + '@microsoft/api-extractor': 7.55.1(@types/node@20.19.25) + '@swc/core': 1.15.3 + postcss: 8.5.6 + typescript: 5.9.3 + transitivePeerDependencies: + - jiti + - supports-color + - tsx + - yaml + tsyringe@4.10.0: dependencies: tslib: 1.14.1 @@ -16058,6 +16831,33 @@ snapshots: transitivePeerDependencies: - supports-color + turbo-darwin-64@2.6.1: + optional: true + + turbo-darwin-arm64@2.6.1: + optional: true + + turbo-linux-64@2.6.1: + optional: true + + turbo-linux-arm64@2.6.1: + optional: true + + turbo-windows-64@2.6.1: + optional: true + + turbo-windows-arm64@2.6.1: + optional: true + + turbo@2.6.1: + optionalDependencies: + turbo-darwin-64: 2.6.1 + turbo-darwin-arm64: 2.6.1 + turbo-linux-64: 2.6.1 + turbo-linux-arm64: 2.6.1 + turbo-windows-64: 2.6.1 + turbo-windows-arm64: 2.6.1 + type-check@0.4.0: dependencies: prelude-ls: 1.2.1 @@ -16276,23 +17076,6 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 - vite-plugin-dts@3.9.1(@types/node@20.19.25)(rollup@4.53.3)(typescript@5.9.3)(vite@5.4.21(@types/node@20.19.25)(terser@5.44.1)): - dependencies: - '@microsoft/api-extractor': 7.43.0(@types/node@20.19.25) - '@rollup/pluginutils': 5.3.0(rollup@4.53.3) - '@vue/language-core': 1.8.27(typescript@5.9.3) - debug: 4.4.3 - kolorist: 1.8.0 - magic-string: 0.30.21 - typescript: 5.9.3 - vue-tsc: 1.8.27(typescript@5.9.3) - optionalDependencies: - vite: 5.4.21(@types/node@20.19.25)(terser@5.44.1) - transitivePeerDependencies: - - '@types/node' - - rollup - - supports-color - vite-plugin-dts@3.9.1(@types/node@20.19.25)(rollup@4.53.3)(typescript@5.9.3)(vite@6.4.1(@types/node@20.19.25)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.1)): dependencies: '@microsoft/api-extractor': 7.43.0(@types/node@20.19.25) @@ -16635,13 +17418,6 @@ snapshots: optionalDependencies: commander: 9.5.0 - zustand@4.5.7(@types/react@18.3.27)(react@18.3.1): - dependencies: - use-sync-external-store: 1.6.0(react@18.3.1) - optionalDependencies: - '@types/react': 18.3.27 - react: 18.3.1 - zustand@5.0.8(@types/react@18.3.27)(react@18.3.1)(use-sync-external-store@1.6.0(react@18.3.1)): optionalDependencies: '@types/react': 18.3.27 diff --git a/screenshots/about.png b/screenshots/about.png index fc74be52..a15759e8 100644 Binary files a/screenshots/about.png and b/screenshots/about.png differ diff --git a/screenshots/main_screetshot.png b/screenshots/main_screetshot.png index a515957d..28ec866a 100644 Binary files a/screenshots/main_screetshot.png and b/screenshots/main_screetshot.png differ diff --git a/screenshots/performance_profiler.png b/screenshots/performance_profiler.png index 466f5483..ec498003 100644 Binary files a/screenshots/performance_profiler.png and b/screenshots/performance_profiler.png differ diff --git a/screenshots/plugin_manager.png b/screenshots/plugin_manager.png index c9ae9d12..131d9094 100644 Binary files a/screenshots/plugin_manager.png and b/screenshots/plugin_manager.png differ diff --git a/screenshots/settings.png b/screenshots/settings.png index 54f5f33d..2bcf1f95 100644 Binary files a/screenshots/settings.png and b/screenshots/settings.png differ diff --git a/scripts/create-package.mjs b/scripts/create-package.mjs new file mode 100644 index 00000000..eb3ffb71 --- /dev/null +++ b/scripts/create-package.mjs @@ -0,0 +1,307 @@ +#!/usr/bin/env node + +/** + * create-package - ES Engine 包脚手架工具 + * + * 使用方法: + * node scripts/create-package.mjs [options] + * + * 选项: + * --type, -t 包类型: runtime-only | plugin | editor-only + * --scope, -s npm scope (默认: @esengine) + * --external 外部模式,用于用户在自己项目中创建插件 + * + * 示例: + * # 内部开发 (monorepo 内,使用 workspace:*) + * node scripts/create-package.mjs my-plugin --type plugin + * + * # 外部用户 (独立项目,使用版本号) + * node scripts/create-package.mjs my-plugin --type plugin --external --scope @mycompany + */ + +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import readline from 'readline'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const ROOT_DIR = path.resolve(__dirname, '..'); +const PACKAGES_DIR = path.join(ROOT_DIR, 'packages'); +const TEMPLATES_DIR = path.join(PACKAGES_DIR, 'build-config', 'templates'); + +const PACKAGE_TYPES = { + 'runtime-only': { + description: '纯运行时库 (不含编辑器代码)', + examples: 'core, math, components' + }, + 'plugin': { + description: '插件包 (同时包含 runtime 和 editor 模块)', + examples: 'ui, tilemap, behavior-tree' + }, + 'editor-only': { + description: '纯编辑器包 (仅用于编辑器)', + examples: 'editor-core, node-editor' + } +}; + +const CATEGORIES = [ + 'core', + 'rendering', + 'physics', + 'ai', + 'ui', + 'audio', + 'input', + 'networking', + 'tools', + 'other' +]; + +// 默认 scope +const DEFAULT_SCOPE = '@esengine'; + +// ES Engine 核心包的最低版本要求 +const ESENGINE_VERSION = '^2.0.0'; + +function parseArgs(args) { + const result = { + name: null, + type: null, + scope: null, + bExternal: false // 外部模式标记 + }; + + for (let i = 0; i < args.length; i++) { + if (args[i] === '--type' || args[i] === '-t') { + result.type = args[i + 1]; + i++; + } else if (args[i] === '--scope' || args[i] === '-s') { + result.scope = args[i + 1]; + i++; + } else if (args[i] === '--external' || args[i] === '-e') { + result.bExternal = true; + } else if (!args[i].startsWith('-')) { + result.name = args[i]; + } + } + + return result; +} + +function createReadlineInterface() { + return readline.createInterface({ + input: process.stdin, + output: process.stdout + }); +} + +async function question(rl, prompt) { + return new Promise((resolve) => { + rl.question(prompt, (answer) => { + resolve(answer.trim()); + }); + }); +} + +async function selectOption(rl, prompt, options) { + console.log(prompt); + options.forEach((opt, i) => { + console.log(` ${i + 1}. ${opt.label}${opt.description ? ` - ${opt.description}` : ''}`); + }); + + while (true) { + const answer = await question(rl, '请选择 (输入数字): '); + const index = parseInt(answer, 10) - 1; + if (index >= 0 && index < options.length) { + return options[index].value; + } + console.log('无效选择,请重试'); + } +} + +function toPascalCase(str) { + return str + .split(/[-_]/) + .map(word => word.charAt(0).toUpperCase() + word.slice(1)) + .join(''); +} + +function processTemplate(content, variables) { + let result = content; + for (const [key, value] of Object.entries(variables)) { + result = result.replace(new RegExp(`\\{\\{${key}\\}\\}`, 'g'), value); + } + return result; +} + +function copyTemplateDir(srcDir, destDir, variables) { + if (!fs.existsSync(destDir)) { + fs.mkdirSync(destDir, { recursive: true }); + } + + const entries = fs.readdirSync(srcDir, { withFileTypes: true }); + + for (const entry of entries) { + const srcPath = path.join(srcDir, entry.name); + let destName = entry.name.replace('.template', ''); + + // 处理文件名中的模板变量 + destName = processTemplate(destName, variables); + + const destPath = path.join(destDir, destName); + + if (entry.isDirectory()) { + copyTemplateDir(srcPath, destPath, variables); + } else { + const content = fs.readFileSync(srcPath, 'utf-8'); + const processedContent = processTemplate(content, variables); + fs.writeFileSync(destPath, processedContent, 'utf-8'); + console.log(` 创建: ${path.relative(PACKAGES_DIR, destPath)}`); + } + } +} + +async function main() { + console.log('\n🚀 ES Engine 包创建工具\n'); + + const args = parseArgs(process.argv.slice(2)); + const rl = createReadlineInterface(); + + try { + // 0. 确定模式:内部 (monorepo) 还是外部 (独立项目) + let bExternal = args.bExternal; + if (!args.bExternal && !args.scope) { + // 自动检测:如果在 monorepo 根目录运行,默认内部模式 + const bIsMonorepo = fs.existsSync(path.join(ROOT_DIR, 'pnpm-workspace.yaml')); + if (!bIsMonorepo) { + bExternal = true; + } + } + + // 1. 获取 scope + let scope = args.scope; + if (!scope) { + if (bExternal) { + // 外部模式必须指定 scope + scope = await question(rl, 'npm scope (例如: @mycompany): '); + if (!scope) { + console.error('❌ 外部模式必须指定 scope'); + process.exit(1); + } + } else { + scope = DEFAULT_SCOPE; + } + } + // 确保 scope 以 @ 开头 + if (!scope.startsWith('@')) { + scope = '@' + scope; + } + + // 2. 获取包名 + let packageName = args.name; + if (!packageName) { + packageName = await question(rl, '包名 (例如: my-plugin): '); + } + + if (!packageName || !/^[a-z][a-z0-9-]*$/.test(packageName)) { + console.error('❌ 无效的包名,必须以小写字母开头,只能包含小写字母、数字和连字符'); + process.exit(1); + } + + // 检查包是否已存在 + const packageDir = path.join(PACKAGES_DIR, packageName); + if (fs.existsSync(packageDir)) { + console.error(`❌ 包 ${packageName} 已存在`); + process.exit(1); + } + + // 3. 获取包类型 + let packageType = args.type; + if (!packageType || !PACKAGE_TYPES[packageType]) { + packageType = await selectOption(rl, '\n选择包类型:', [ + { value: 'runtime-only', label: 'runtime-only', description: PACKAGE_TYPES['runtime-only'].description }, + { value: 'plugin', label: 'plugin', description: PACKAGE_TYPES['plugin'].description }, + { value: 'editor-only', label: 'editor-only', description: PACKAGE_TYPES['editor-only'].description } + ]); + } + + // 4. 获取描述 + const description = await question(rl, '\n包描述: ') || `${packageName} package`; + + // 5. 获取显示名称 + const displayName = await question(rl, `\n显示名称 (默认: ${toPascalCase(packageName)}): `) || toPascalCase(packageName); + + // 6. 插件类型需要选择分类 + let category = 'other'; + if (packageType === 'plugin') { + category = await selectOption(rl, '\n选择插件分类:', CATEGORIES.map(c => ({ value: c, label: c }))); + } + + const modeLabel = bExternal ? '外部 (npm install)' : '内部 (workspace:*)'; + console.log('\n📦 创建包...\n'); + console.log(` 模式: ${modeLabel}`); + console.log(` Scope: ${scope}`); + console.log(` Full name: ${scope}/${packageName}\n`); + + // 准备模板变量 + // 依赖版本:内部用 workspace:*,外部用具体版本 + const depVersion = bExternal ? ESENGINE_VERSION : 'workspace:*'; + + const variables = { + scope, + name: packageName, + fullName: `${scope}/${packageName}`, + pascalName: toPascalCase(packageName), + description, + displayName, + category, + depVersion // 用于 package.json 中的依赖版本 + }; + + // 复制模板 + const templateDir = path.join(TEMPLATES_DIR, packageType); + if (!fs.existsSync(templateDir)) { + console.error(`❌ 模板目录不存在: ${templateDir}`); + process.exit(1); + } + + copyTemplateDir(templateDir, packageDir, variables); + + // 重命名特殊文件 + if (packageType === 'plugin') { + // RuntimeModule.ts.template -> {name}RuntimeModule.ts + const runtimeModuleSrc = path.join(packageDir, 'src', 'RuntimeModule.ts'); + const runtimeModuleDest = path.join(packageDir, 'src', `${toPascalCase(packageName)}RuntimeModule.ts`); + if (fs.existsSync(runtimeModuleSrc)) { + fs.renameSync(runtimeModuleSrc, runtimeModuleDest); + } + + // Plugin.ts.template -> {name}Plugin.ts + const pluginSrc = path.join(packageDir, 'src', 'editor', 'Plugin.ts'); + const pluginDest = path.join(packageDir, 'src', 'editor', `${toPascalCase(packageName)}Plugin.ts`); + if (fs.existsSync(pluginSrc)) { + fs.renameSync(pluginSrc, pluginDest); + } + } + + console.log('\n✅ 包创建成功!\n'); + console.log('下一步:'); + console.log(` 1. cd packages/${packageName}`); + console.log(` 2. pnpm install`); + console.log(` 3. 开始编写代码`); + console.log(` 4. pnpm run build`); + + if (packageType === 'plugin') { + console.log('\n插件开发提示:'); + console.log(' - runtime.ts: 纯运行时代码 (不能导入 React!)'); + console.log(' - editor/: 编辑器模块 (可以使用 React)'); + console.log(' - plugin.json: 插件描述文件'); + } + + } finally { + rl.close(); + } +} + +main().catch(console.error); diff --git a/tsconfig.base.json b/tsconfig.base.json new file mode 100644 index 00000000..12fa8f10 --- /dev/null +++ b/tsconfig.base.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ES2020", + "moduleResolution": "bundler", + "lib": ["ES2020", "DOM"], + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noUnusedLocals": false, + "noUnusedParameters": false + } +} diff --git a/turbo.json b/turbo.json new file mode 100644 index 00000000..4757370d --- /dev/null +++ b/turbo.json @@ -0,0 +1,48 @@ +{ + "$schema": "https://turbo.build/schema.json", + "tasks": { + "build": { + "dependsOn": ["^build"], + "outputs": ["dist/**", "bin/**", "pkg/**"], + "env": ["NODE_ENV"] + }, + "build:watch": { + "dependsOn": ["^build"], + "persistent": true, + "cache": false + }, + "build:npm": { + "dependsOn": ["^build:npm", "build"], + "outputs": ["dist/**", "bin/**"] + }, + "build:release": { + "dependsOn": ["^build"], + "outputs": ["dist/**", "bin/**", "pkg/**"] + }, + "type-check": { + "dependsOn": ["^build"], + "outputs": [] + }, + "lint": { + "outputs": [] + }, + "lint:fix": { + "outputs": [] + }, + "test": { + "dependsOn": ["build"], + "outputs": [] + }, + "test:ci": { + "dependsOn": ["build"], + "outputs": ["coverage/**"] + }, + "test:coverage": { + "dependsOn": ["build"], + "outputs": ["coverage/**"] + }, + "clean": { + "cache": false + } + } +}