Compare commits

...

39 Commits

Author SHA1 Message Date
github-actions[bot]
27b9e174eb chore: release packages (#348)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-12-26 18:16:57 +08:00
YHH
ede440d277 chore: changeset for framework package configs (#347)
* chore: add changeset for framework package configs

* fix(docs): 修复 VitePress 配置中的包路径
2025-12-26 18:13:12 +08:00
YHH
5cb83f0743 fix(framework): 补充包配置 peerDeps/repository/keywords (#346)
* fix(framework): 补充 peerDependencies, repository 和 keywords 配置

- fsm: 添加 peerDeps, repository, keywords
- timer: 添加 peerDeps, repository, keywords
- spatial: 添加 peerDeps, repository, keywords
- procgen: 添加 peerDeps, repository, keywords
- pathfinding: 移除重复的 dependencies,添加 repository, keywords

* chore: update pnpm-lock.yaml
2025-12-26 18:05:37 +08:00
yhh
7cbf92b8c7 fix(docs): 修复 tsconfig.json 中的 references 路径 2025-12-26 17:49:40 +08:00
YHH
a049bbe2f5 fix: add private:true to packages not meant for npm (#345)
* fix(ci): run on all PRs with conditional skip

- Remove paths filter from pull_request trigger
- Add check-changes job to detect code changes
- Skip full CI if no code files changed
- Satisfies branch protection while avoiding unnecessary builds

* fix: add private:true to packages not meant for npm

Prevents changesets from trying to publish internal packages:
- engine/* (8 packages)
- rendering/* (8 packages)
- physics/* (2 packages)
- streaming/* (1 package)
- tools/sdk, tools/worker-generator
2025-12-26 17:45:00 +08:00
github-actions[bot]
ec72df7af5 chore: release packages (#343)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: YHH <359807859@qq.com>
2025-12-26 17:18:01 +08:00
YHH
9327c1cef5 fix(ci): run on all PRs with conditional skip (#344)
- Remove paths filter from pull_request trigger
- Add check-changes job to detect code changes
- Skip full CI if no code files changed
- Satisfies branch protection while avoiding unnecessary builds
2025-12-26 17:15:02 +08:00
YHH
da5bf2116a fix(changesets): remove network-protocols and build-config from ignore (#342)
* fix(changesets): remove network-protocols and build-config from ignore

These packages are dependencies of non-ignored packages:
- @esengine/network depends on @esengine/network-protocols
- Multiple packages depend on @esengine/build-config (private, won't publish)

* fix(ci): add .changeset to CI trigger paths
2025-12-26 17:05:41 +08:00
YHH
67e97f89c6 fix(ci): update typedoc path and add workflow_dispatch (#341)
* fix: update Laya examples, add CLI docs, fix changesets workflow

- Update Laya examples to use Laya 3.x Script pattern (@regClass)
- Add CLI tool quick start section to README (npx @esengine/cli init)
- Fix changesets workflow to only build framework packages
- Remove unnecessary Rust/WASM build steps from changesets workflow
- Remove redundant 'pnpm build' from changeset:publish script

* docs: add CLI documentation and update Laya examples

- Add CLI quick start section to getting-started.md (zh/en)
- Update Laya examples to use Laya 3.x Script pattern

* fix(ci): update typedoc path and add workflow_dispatch

- Fix typedoc entry point: packages/core -> packages/framework/core
- Add workflow_dispatch to release-changesets for manual triggering
- Add deeper package paths to trigger on nested packages
2025-12-26 16:52:29 +08:00
YHH
31fd34b221 fix: update Laya examples, add CLI docs, fix changesets workflow (#340)
* fix: update Laya examples, add CLI docs, fix changesets workflow

- Update Laya examples to use Laya 3.x Script pattern (@regClass)
- Add CLI tool quick start section to README (npx @esengine/cli init)
- Fix changesets workflow to only build framework packages
- Remove unnecessary Rust/WASM build steps from changesets workflow
- Remove redundant 'pnpm build' from changeset:publish script

* docs: add CLI documentation and update Laya examples

- Add CLI quick start section to getting-started.md (zh/en)
- Update Laya examples to use Laya 3.x Script pattern
2025-12-26 16:37:37 +08:00
YHH
c4f7a13b74 feat(cli): add CLI tool for adding ECS framework to existing projects (#339)
* feat(cli): add CLI tool for adding ECS framework to existing projects

- Support Cocos Creator 2.x/3.x, LayaAir 3.x, and Node.js platforms
- Auto-detect project type based on directory structure
- Generate ECSManager with full configuration (debug, remote debug, WebSocket URL)
- Auto-install dependencies with npm/yarn/pnpm detection
- Platform-specific decorators and lifecycle methods

* chore: add changeset for @esengine/cli

* fix(ci): fix YAML syntax error in ai-issue-helper workflow

* fix(cli): resolve file system race conditions (CodeQL)

* chore(ci): remove unused and broken workflows

* fix(ci): fix YAML encoding in release.yml
2025-12-26 16:18:59 +08:00
YHH
155411e743 refactor: reorganize package structure and decouple framework packages (#338)
* refactor: reorganize package structure and decouple framework packages

## Package Structure Reorganization
- Reorganized 55 packages into categorized subdirectories:
  - packages/framework/ - Generic framework (Laya/Cocos compatible)
  - packages/engine/ - ESEngine core modules
  - packages/rendering/ - Rendering modules (WASM dependent)
  - packages/physics/ - Physics modules
  - packages/streaming/ - World streaming
  - packages/network-ext/ - Network extensions
  - packages/editor/ - Editor framework and plugins
  - packages/rust/ - Rust WASM engine
  - packages/tools/ - Build tools and SDK

## Framework Package Decoupling
- Decoupled behavior-tree and blueprint packages from ESEngine dependencies
- Created abstracted interfaces (IBTAssetManager, IBehaviorTreeAssetContent)
- ESEngine-specific code moved to esengine/ subpath exports
- Framework packages now usable with Cocos/Laya without ESEngine

## CI Configuration
- Updated CI to only type-check and lint framework packages
- Added type-check:framework and lint:framework scripts

## Breaking Changes
- Package import paths changed due to directory reorganization
- ESEngine integrations now use subpath imports (e.g., '@esengine/behavior-tree/esengine')

* fix: update es-engine file path after directory reorganization

* docs: update README to focus on framework over engine

* ci: only build framework packages, remove Rust/WASM dependencies

* fix: remove esengine subpath from behavior-tree and blueprint builds

ESEngine integration code will only be available in full engine builds.
Framework packages are now purely engine-agnostic.

* fix: move network-protocols to framework, build both in CI

* fix: update workflow paths from packages/core to packages/framework/core

* fix: exclude esengine folder from type-check in behavior-tree and blueprint

* fix: update network tsconfig references to new paths

* fix: add test:ci:framework to only test framework packages in CI

* fix: only build core and math npm packages in CI

* fix: exclude test files from CodeQL and fix string escaping security issue
2025-12-26 14:50:35 +08:00
YHH
a84ff902e4 fix: 修复 changesets 验证错误 (#337)
- 简化 ignore 配置,只保留 editor-app
- 设置内部包为 private: true
  - build-config, engine, ecs-engine-bindgen
  - editor-core, editor-runtime
  - 所有 *-editor 插件包

这样 changesets 可以正常工作,private 包不会被发布
2025-12-26 09:06:52 +08:00
YHH
54038e3250 feat: 配置 changesets 版本管理和自动发布 (#336)
- 安装 @changesets/cli 和 @changesets/changelog-github
- 配置 .changeset/config.json
- 添加 npm scripts
- 创建 release-changesets.yml GitHub Action
2025-12-25 23:07:44 +08:00
yhh
5544fca002 docs: update changelog for v2.4.2 2025-12-25 20:42:15 +08:00
yhh
88b5ffc0a7 fix(ci): don't mark npm package releases as latest 2025-12-25 20:37:50 +08:00
yhh
0c0a5f10f7 chore(core): release v2.4.2 2025-12-25 20:31:51 +08:00
YHH
56e322de7f refactor(core): 优化 PlatformWorkerPool 和 IncrementalSerializer 代码规范 (#335)
PlatformWorkerPool:
- 添加 IWorkerPoolStatus 导出接口
- 添加便捷 getter: isDestroyed, workerCount, isReady, hasPendingTasks
- 提取 createTask/completeTask 方法减少重复代码
- 添加 ERROR_POOL_DESTROYED 常量
- 添加代码段分隔符和双语注释

IncrementalSerializer:
- 所有公共 API 添加 @zh/@en 双语注释
- 类型改用 interface 并添加 readonly
- SceneDataChange.value 从 any 改为 unknown
- 导出 SceneSnapshot, IIncrementalStats 接口
- 提取 DEFAULT_OPTIONS 常量
- 优化 getIncrementalStats 从 6 次 filter 改为 2 次循环
2025-12-25 18:39:15 +08:00
YHH
c2ebd387f2 refactor(core): 优化 WorkerEntitySystem 实现 (#334)
重构改进:
- 分离 PlatformWorkerPool 到独立文件
- 使用 Map 管理 Worker 任务状态,替代 (worker as any)._currentTask
- 提取通用批次分割逻辑 splitIntoBatches
- 添加 IWorkerMessageData 接口提高类型安全
- 添加 WorkerState 枚举提高可读性
- 添加规范的双语注释 (@zh/@en)
- 导出新类型 IWorkerSystemConfig 和 ProcessingMode
- 保留 WorkerSystemConfig 类型别名向后兼容

代码组织:
- 按功能分组方法 (初始化/处理/批次/结果应用等)
- 减少 any 类型使用
- 统一命名风格
2025-12-25 18:08:17 +08:00
YHH
e5e647f1a4 feat(pathfinding): 添加寻路系统模块 (#333)
* feat(pathfinding): 添加寻路系统模块

实现完整的寻路系统,支持 A* 算法、网格地图、导航网格和路径平滑:

A* 寻路算法:
- 高效的二叉堆优先队列
- 可配置的启发式权重
- 最大搜索节点限制
- 支持对角移动和穿角避免

网格地图 (GridMap):
- 基于二维数组的网格地图
- 支持 4 方向和 8 方向移动
- 可变移动代价
- 从数组或字符串加载地图

导航网格 (NavMesh):
- 凸多边形导航网格
- 自动检测相邻多边形
- 漏斗算法路径优化
- 适合复杂地形

路径平滑:
- Bresenham 视线检测
- 射线投射视线检测
- 视线简化器 (移除不必要的拐点)
- Catmull-Rom 曲线平滑
- 组合平滑器

启发式函数:
- 曼哈顿距离 (4方向)
- 欧几里得距离 (任意方向)
- 切比雪夫距离 (8方向)
- 八角距离 (8方向,对角线√2)

蓝图节点 (8个):
- FindPath: 基础寻路
- FindPathSmooth: 平滑寻路
- IsWalkable: 检查可通行性
- GetPathLength: 获取路径点数
- GetPathDistance: 获取路径距离
- GetPathPoint: 获取路径点
- MoveAlongPath: 沿路径移动
- HasLineOfSight: 视线检测

* chore: update pnpm-lock.yaml for pathfinding package
2025-12-25 17:24:24 +08:00
YHH
4d501ba448 feat(effect): 添加效果系统模块 (#332)
实现 Buff/Debuff 效果管理和属性修改器系统:

核心功能:
- 效果容器管理效果的应用、移除和更新
- 支持持续时间类型: 永久/计时/条件
- 支持叠加规则: 刷新/叠加/独立/替换/忽略
- 效果标签系统用于分类和查询
- 效果优先级和互斥标签支持
- 效果事件系统 (应用/移除/叠加/刷新/跳动/过期)

修改器系统:
- 属性修改器支持加法/乘法/覆盖/最小值/最大值操作
- 优先级分层计算 (基础/加法/乘法/最终)
- 数值计算器自动按优先级应用修改器

蓝图节点 (12个):
- ApplyEffect: 应用效果
- RemoveEffect/RemoveEffectByTag: 移除效果
- HasEffect/HasEffectTag: 检查效果
- GetEffectStacks/GetEffectRemainingTime/GetEffectCount: 查询效果
- ClearAllEffects: 清除所有效果
- OnEffectApplied/OnEffectRemoved/OnEffectTick: 效果事件
2025-12-25 15:17:06 +08:00
YHH
275124b66c feat(procgen): 添加程序化生成工具包 (#331)
- 添加噪声函数 (Perlin, Simplex, Worley, FBM)
- 添加种子随机数生成器 (SeededRandom)
- 添加加权随机选择和洗牌工具
- 添加蓝图节点 (SampleNoise2D, SeededRandom, WeightedPick 等)
2025-12-25 14:33:19 +08:00
YHH
25936c19e9 feat(network): 添加状态同步和客户端预测模块 (#330)
* feat(network): 添加状态同步和客户端预测模块

- 添加状态快照接口和快照缓冲区实现
- 实现线性插值和赫尔米特插值器
- 实现客户端预测和服务器校正系统
- 添加网络相关蓝图节点 (IsLocalPlayer, IsServer 等)

* chore: update pnpm-lock.yaml
2025-12-25 14:03:12 +08:00
YHH
f43631a1e1 feat(spatial): 添加 AOI 兴趣区域系统 (#329)
添加 AOI (Area of Interest) 兴趣区域管理功能:

核心接口:
- IAOIManager<T> - AOI 管理器接口
- IAOIEvent<T> - AOI 事件类型
- IAOIObserverConfig - 观察者配置

实现:
- GridAOI<T> - 基于网格的 AOI 实现
- 实体进入/离开视野事件
- 视野范围配置
- 订阅者模式

蓝图节点:
- GetEntitiesInView - 获取视野内实体
- GetObserversOf - 获取能看到目标的观察者
- CanSee - 检查是否可见
- EventEntityEnterView - 实体进入视野事件
- EventEntityExitView - 实体离开视野事件

服务令牌:
- AOIManagerToken - AOI 管理器服务令牌
2025-12-25 13:24:26 +08:00
YHH
f8c181836e refactor(blueprint): 使用 ecs-framework 的 Entity 和 IScene 类型 (#328)
- 从 @esengine/ecs-framework 导入 Entity 和 IScene 类型
- 移除 ExecutionContext.ts 中的自定义 IEntity 和 IScene 接口
- 更新 BlueprintComponent.ts、BlueprintVM.ts、BlueprintSystem.ts 使用正确的类型

这使得 blueprint 包与 ecs-framework 的类型保持一致,避免重复定义接口。
2025-12-25 13:07:09 +08:00
YHH
840eb3452e feat(fsm): 添加有限状态机模块 (#327)
- 添加 IStateMachine 接口,支持泛型状态和上下文
- 添加 StateMachine 实现
- 支持状态定义 (onEnter/onExit/onUpdate)
- 支持转换定义和条件
- 支持事件监听 (onChange/onEnter/onExit)
- 添加 8 个蓝图节点:
  - GetCurrentState, TransitionTo, CanTransition
  - IsInState, WasInState, GetStateDuration
  - EvaluateTransitions, ResetStateMachine
2025-12-25 12:51:52 +08:00
YHH
0bf849e193 feat(blueprint): 蓝图组合系统 (#326)
* feat(blueprint): 添加蓝图组合系统

- 添加 IBlueprintFragment 接口和实现,支持可重用蓝图片段
- 添加 IBlueprintComposer 接口和实现,支持片段组合
- 添加 FragmentRegistry 片段注册表
- 支持暴露引脚连接和编译成完整蓝图
- 支持片段资产序列化格式

* fix(blueprint): 移除未使用的 ExposedPin 导入
2025-12-25 12:42:57 +08:00
YHH
ebb984d354 feat(timer): 添加定时器和冷却系统 (#325)
- 添加 ITimerService 接口和 TimerService 实现
- 支持一次性定时器和重复定时器
- 支持冷却系统 (startCooldown/isCooldownReady/getCooldownProgress)
- 添加 8 个蓝图节点:
  - StartCooldown, IsCooldownReady, GetCooldownProgress
  - ResetCooldown, GetCooldownInfo
  - HasTimer, CancelTimer, GetTimerRemaining
2025-12-25 12:29:59 +08:00
YHH
068ca4bf69 feat(spatial): 空间查询和索引系统 (#324)
* feat(spatial): 添加空间查询包

- ISpatialQuery: 空间查询接口
  - findInRadius, findInRect, findNearest, findKNearest
  - raycast, raycastFirst
  - IBounds, IRaycastHit, SpatialFilter 类型

- ISpatialIndex: 空间索引接口
  - insert, remove, update, clear, getAll

- GridSpatialIndex: 网格空间索引实现
  - 基于均匀网格的空间划分
  - 支持所有 ISpatialQuery 操作

- 工具函数
  - createBounds, isPointInBounds, boundsIntersect
  - distanceSquared, distance

* feat(spatial): 添加空间查询蓝图节点

- 添加 FindInRadius/FindInRect/FindNearest/FindKNearest 节点
- 添加 Raycast/RaycastFirst 射线检测节点
- 每个节点包含模板和执行器
- 使用 menuPath: ['Spatial', ...] 组织节点菜单
2025-12-25 12:15:06 +08:00
YHH
4089051731 feat(blueprint): 添加蓝图触发器系统 (#323)
* feat(blueprint): 添加蓝图触发器系统

- TriggerTypes: 定义触发器类型和上下文接口
  - tick/input/collision/message/timer/stateEnter/stateExit/custom
  - 各类型的上下文接口和工厂函数

- TriggerCondition: 触发条件系统
  - ITriggerCondition 接口
  - 复合条件 (CompositeCondition, NotCondition)
  - 通用条件 (AlwaysTrue, AlwaysFalse, TriggerType, EntityId)
  - 特定类型条件 (InputAction, MessageName, StateName, TimerId, CollisionEntity, CustomEvent)
  - ConditionBuilder 链式 API

- BlueprintTrigger: 触发器核心实现
  - IBlueprintTrigger 接口
  - BlueprintTrigger 实现类
  - TriggerRegistry 触发器注册表
  - 各类型触发器工厂函数

- TriggerDispatcher: 触发器调度系统
  - ITriggerDispatcher 调度器接口
  - TriggerDispatcher 实现
  - EntityTriggerManager 实体触发器管理器

* feat(blueprint): 添加触发器相关事件节点

- EventInput: 输入事件节点 (action/value/pressed/released)
- EventCollisionEnter/Exit: 碰撞事件节点
- EventMessage: 消息事件节点
- EventTimer: 定时器事件节点
- EventStateEnter/Exit: 状态机事件节点
2025-12-25 11:35:29 +08:00
YHH
6b8b65ae16 feat(script-runtime): 服务器端蓝图执行模块 (#322)
* feat(script-runtime): 添加服务器端蓝图执行模块

- ServerBlueprintVM: 服务器端蓝图虚拟机
- CPULimiter: CPU 时间和步数限制
- IntentCollector: 意图收集系统
- FileMemoryStore: 文件系统持久化
- ServerExecutionContext: 服务器执行上下文

* refactor(script-runtime): 分离引擎接口与游戏逻辑

- 重构 IntentTypes.ts 只保留基础 IIntent 接口和通用常量
- IntentCollector 改为泛型类,支持任意意图类型
- ServerExecutionContext 改为泛型类,支持任意游戏状态类型
- ServerBlueprintVM 改为泛型类,使用 TGameState 和 TIntent 类型参数
- 移除游戏特定类型(IUnitState, ISpawnerState 等),由游戏项目定义
- 添加 IntentKeyExtractor 机制用于防止重复意图

* feat(script-runtime): 添加服务器端游戏循环框架

- PlayerSession: 封装单个玩家的 VM、蓝图和 Memory 状态
- TickScheduler: 管理所有玩家会话,调度每 tick 的蓝图执行
- IIntentProcessor: 意图处理器接口,由游戏项目实现
- IntentProcessorBase: 意图处理器基类,提供常用处理模式
- IntentProcessorRegistry: 按类型注册意图处理器
- GameLoop: 完整的游戏主循环,协调各组件工作

* feat(script-runtime): 添加通用蓝图节点

Memory 节点:
- GetMemory: 读取玩家 Memory
- SetMemory: 写入玩家 Memory
- HasMemoryKey: 检查键是否存在
- DeleteMemory: 删除 Memory 键

Log 节点:
- Log: 记录日志
- Warn: 记录警告
- Error: 记录错误

Game 信息节点:
- GetTick: 获取当前 tick
- GetPlayerId: 获取玩家 ID
- GetDeltaTime: 获取增量时间
- GetGameState: 获取游戏状态

提供 registerScriptRuntimeNodes() 用于批量注册节点

* fix(script-runtime): 修复 CI 构建错误

- 更新 tsconfig.json 继承 tsconfig.base.json
- 添加 references 到 core 和 blueprint 包
- 更新 pnpm-lock.yaml

* fix(script-runtime): 修复 DTS 构建错误

- 添加 tsconfig.build.json 用于 tsup 构建
- 更新 tsup.config.ts 使用 tsconfig.build.json
- 分离构建配置和类型检查配置
2025-12-25 11:00:43 +08:00
YHH
a75c61c049 docs: 添加 Discord 社区链接 (#321) 2025-12-24 23:35:43 +08:00
YHH
770c05402d docs: 优化 README 包列表展示 (#320) 2025-12-24 23:33:12 +08:00
YHH
9d581ccd8d ci: 集成 Turborepo Remote Cache (#319) 2025-12-24 23:06:38 +08:00
YHH
235c432edb feat(network): 基于 TSRPC 的网络同步模块 (#318)
- network-protocols: 共享协议包,使用 TSRPC CLI 生成完整类型验证
- network: 浏览器客户端,提供 NetworkPlugin、NetworkService 和同步系统
- network-server: Node.js 服务端,提供 GameServer 和房间管理
2025-12-24 22:49:29 +08:00
YHH
dbc6793dc4 refactor: 代码规范化与依赖清理 (#317)
* refactor(deps): 统一编辑器包依赖配置 & 优化分层架构

- 将 ecs-engine-bindgen 提升为 Layer 1 核心包
- 统一 9 个编辑器包的依赖声明模式
- 清理废弃的包目录 (ui, ui-editor, network-*)

* refactor(tokens): 修复 PrefabService 令牌冲突 & 补充 module.json

- 将 editor-core 的 PrefabServiceToken 改名为 EditorPrefabServiceToken
  避免与 asset-system 的 PrefabServiceToken 冲突 (Symbol.for 冲突)
- 为 mesh-3d 添加 module.json
- 为 world-streaming 添加 module.json

* refactor(editor-core): 整理导出结构 & 添加 blueprint tokens.ts

- 按功能分组整理 editor-core 的 65 行导出
- 添加清晰的分组注释 (中英双语)
- 为 blueprint 添加占位符 tokens.ts

* chore(editor): 为 14 个编辑器插件包添加 module.json

统一编辑器包的模块配置,包含:
- isEditorPlugin 标识
- runtimeModule 关联
- exports 导出清单 (inspectors, panels, gizmos)

* refactor(core): 改进类型安全 - 减少 as any 使用

- 添加 GlobalTypes.ts 定义小游戏平台和 Chrome API 类型
- SoAStorage 使用 IComponentTypeMetadata 替代 as any
- PlatformDetector 使用类型安全的平台检测
- 添加 ISoAStorageStats/ISoAFieldStats 接口

* feat(editor): 添加 EditorServicesContext 解决 prop drilling

- 新增 contexts/EditorServicesContext.tsx 提供统一服务访问
- App.tsx 包裹 EditorServicesProvider
- 提供 useEditorServices/useMessageHub 等便捷 hooks
- SceneHierarchy 添加迁移注释,后续可移除 props

* docs(editor): 澄清 inspector 目录架构关系

- inspector/ 标记为内部实现,添加 @deprecated 警告
- inspectors/ 标记为公共 API 入口点
- 添加架构说明文档

* refactor(editor): 添加全局类型声明消除 window as any

- 创建 editor-app/src/global.d.ts 声明 Window 接口扩展
- 创建 editor-core/src/global.d.ts 声明 Window 接口扩展
- 更新 App.tsx 使用类型安全的 window 属性访问
- 更新 PluginLoader.ts 使用 window.__ESENGINE_PLUGINS__
- 更新 PluginSDKRegistry.ts 使用 window.__ESENGINE_SDK__
- 更新 UserCodeService.ts 使用类型安全的全局变量访问

* refactor(editor): 提取项目和场景操作到独立 hooks

- 创建 useProjectActions hook 封装项目操作
- 创建 useSceneActions hook 封装场景操作
- 为渐进式重构 App.tsx 做准备

* refactor(editor): 清理冗余代码和未使用文件

删除的目录和文件:
- application/state/ - 重复的状态管理(与 stores/ 重复)
- 8 个孤立 CSS 文件(对应组件不存在)
- AssetBrowser.tsx - 仅为 ContentBrowser 的向后兼容包装
- AssetPicker.tsx - 未被使用
- AssetPickerDialog.tsx (顶级) - 已被 dialogs/ 版本取代
- EntityInspector.tsx (顶级) - 已被 inspectors/views/ 版本取代

修复:
- 移除 App.tsx 中未使用的导入
- 更新 application/index.ts 移除已删除模块
- 修复 useProjectActions.ts 的 MutableRefObject 类型

* refactor(editor): 统一 inspectors 模块导出结构

- 在 inspectors/index.ts 重新导出 PropertyInspector
- 创建 inspectors/fields/index.ts barrel export
- 导出 views、fields、common 子模块
- 更新 EntityInspector 使用统一入口导入

* refactor(editor): 删除废弃的 Profiler 组件

删除未使用的组件(共 1059 行):
- ProfilerPanel.tsx (229 行)
- ProfilerWindow.tsx (589 行)
- ProfilerDockPanel.tsx (241 行)
- ProfilerPanel.css
- ProfilerDockPanel.css

保留:AdvancedProfiler + AdvancedProfilerWindow(正在使用)

* refactor(runtime-core): 统一依赖处理与插件状态管理

- 新增 DependencyUtils 统一拓扑排序和依赖验证
- 新增 PluginState 定义插件生命周期状态机
- 合并 UnifiedPluginLoader 到 PluginLoader
- 清理 index.ts 移除不必要的 Token re-exports
- 新增 RuntimeMode/UserCodeRealm/ImportMapGenerator

* refactor(editor-core): 使用统一的 ImportMapGenerator

- WebBuildPipeline 使用 runtime-core 的 generateImportMap
- UserCodeService 添加 ImportMap 相关接口

* feat(compiler): 增强 esbuild 查找策略

- 支持本地 node_modules、pnpm exec、npx、全局多种来源
- EngineService 使用 RuntimeMode

* refactor(runtime-core): 简化 GameRuntime 代码

- 合并 _disableGameLogicSystems/_enableGameLogicSystems 为 _setGameLogicSystemsEnabled
- 精简本地 Token 定义的注释

* refactor(editor-core): 引入 BaseRegistry 基类消除代码重复

- 新增 BaseRegistry 和 PrioritizedRegistry 基类
- 重构 CompilerRegistry, InspectorRegistry, FieldEditorRegistry
- 统一注册表的日志记录和错误处理

* refactor(editor-core): 扩展 BaseRegistry 重构

- ComponentInspectorRegistry 继承 PrioritizedRegistry
- EditorComponentRegistry 继承 BaseRegistry
- EntityCreationRegistry 继承 BaseRegistry
- PropertyRendererRegistry 继承 PrioritizedRegistry
- 导出 BaseRegistry 基类供外部使用
- 统一双语注释格式

* refactor(editor-core): 代码优雅性优化

CommandManager:
- 提取 tryMergeWithLast() 和 pushToUndoStack() 消除重复代码
- 统一双语注释格式

FileActionRegistry:
- 提取 normalizeExtension() 消除扩展名规范化重复
- 统一私有属性命名风格(_前缀)
- 使用 createRegistryToken 统一 Token 创建

BaseRegistry:
- 添加 IOrdered 接口
- 添加 sortByOrder() 排序辅助方法

EntityCreationRegistry:
- 使用 sortByOrder() 简化排序逻辑

* refactor(editor-core): 统一日志系统 & 代码规范优化

- GizmoRegistry: 使用 createLogger 替代 console.warn
- VirtualNodeRegistry: 使用 createLogger 替代 console.warn
- WindowRegistry: 使用 logger、添加 _ 前缀、导出 IWindowRegistry token
- EditorViewportService: 使用 createLogger 替代 console.warn
- ComponentActionRegistry: 使用 logger、添加 _ 前缀、返回值改进
- SettingsRegistry: 使用 logger、提取 ensureCategory/ensureSection 方法
- 添加 WindowRegistry 到主导出

* refactor(editor-core): ModuleRegistry 使用 logger 替代 console

* refactor(editor-core): SerializerRegistry/UIRegistry 添加 token 和 _ 前缀

* refactor(editor-core): UIRegistry 代码优雅性 & Token 命名统一

- UIRegistry: 提取 _sortByOrder 消除 6 处重复排序逻辑
- UIRegistry: 添加分节注释和双语文档
- FieldEditorRegistry: Token 重命名为 FieldEditorRegistryToken
- PropertyRendererRegistry: Token 重命名为 PropertyRendererRegistryToken

* refactor(core): 统一日志系统 - console 替换为 logger

- ComponentSerializer: 使用 logger 替代 console.warn
- ComponentRegistry: console.warn → logger.warn (已有 logger)
- SceneSerializer: 添加 logger,替换 console.warn/error
- SystemScheduler: 添加 logger,替换 console.warn
- VersionMigration: 添加 logger,替换所有 console.warn
- RuntimeModeService: console.error → logger.error
- Core.ts: _logger 改为 readonly,双语错误消息
- SceneSerializer 修复:使用 getComponentTypeName 替代 constructor.name

* fix(core): 修复 constructor.name 压缩后失效问题

- Scene.ts: 使用 system.systemName 替代 system.constructor.name
- CommandBuffer.ts: 使用 getComponentTypeName() 替代 constructor.name

* refactor(editor-core): 代码规范优化 - 私有方法命名 & 日志统一

- BuildService: console → logger
- FileActionRegistry: 添加 logger, 私有方法 _ 前缀
- SettingsRegistry: 私有方法 _ 前缀 (ensureCategory → _ensureCategory)

* refactor(core): Scene.ts 私有变量命名规范化

- logger → _logger (遵循私有变量 _ 前缀规范)

* refactor(editor-core): 服务类私有成员命名规范化

- CommandManager: 私有变量/方法添加 _ 前缀
  - undoStack/redoStack/config/isExecuting
  - tryMergeWithLast/pushToUndoStack
- LocaleService: 私有变量/方法添加 _ 前缀
  - currentLocale/translations/changeListeners
  - deepMerge/getNestedValue/loadSavedLocale/saveLocale

* refactor(core): 私有成员命名规范化 & 单例模式优化

- Component.ts: _idGenerator 私有静态变量规范化
- PlatformManager.ts: _instance, _adapter, _logger 规范化
- AutoProfiler.ts: _instance, _config 及所有私有方法规范化
- ProfilerSDK.ts: _instance, _config 及所有私有方法规范化
- ComponentPoolManager: _instance, _pools, _usageTracker 规范化
- GlobalEventBus: _instance 规范化
- 添加中英双语 JSDoc 注释

* refactor(editor-app,behavior-tree-editor): 私有成员 & 单例模式命名规范化

editor-app:
- EngineService: private static instance → _instance
- EditorEngineSync: 所有私有成员添加 _ 前缀
- RuntimeResolver: 所有私有成员和方法添加 _ 前缀
- SettingsService: 所有私有成员和方法添加 _ 前缀

behavior-tree-editor:
- GlobalBlackboardService: 所有私有成员和方法添加 _ 前缀
- NotificationService: private static instance → _instance
- NodeRegistryService: 所有私有成员和方法添加 _ 前缀
- TreeStateAdapter: private static instance → _instance

* fix(editor-runtime): 添加 editor-core 到 external 避免传递依赖问题

将 @esengine/editor-core 添加到 vite external 配置,
避免 editor-core → runtime-core → ecs-engine-bindgen 的传递依赖
被错误地打包进 editor-runtime.js,导致 CI 构建失败。

* fix(core): 修复空接口 lint 错误

将 IByteDanceMiniGameAPI、IAlipayMiniGameAPI、IBaiduMiniGameAPI 从空接口改为类型别名,修复 no-empty-object-type 规则报错
2025-12-24 20:57:08 +08:00
YHH
58f70a5783 refactor(core): 代码结构优化 & 添加独立 ECS 框架文档 (#316)
Core 库优化:
- Scene.ts: 提取 _runSystemPhase() 消除 update/lateUpdate 重复代码
- World.ts: 可选链简化、提取 _isSceneCleanupCandidate() 消除重复条件
- Entity.ts: 移除过度设计的 EntityComparer
- 清理方法内部冗余注释,保持代码自解释

文档改进:
- 为 core 包创建独立 README (中英文)
- 明确 ECS 框架可独立用于 Cocos/Laya 等引擎
- 添加 sparse-checkout 命令说明,支持只克隆 core 源码
- 主 README 添加 ECS 独立使用提示
2025-12-23 18:00:21 +08:00
YHH
828ff969e1 feat(3d): FBX/GLTF/OBJ 加载器与骨骼动画支持 (#315)
* feat(3d): FBX/GLTF/OBJ 加载器与骨骼动画支持

* chore: 更新 pnpm-lock.yaml

* fix: 移除未使用的变量和方法

* fix: 修复 mesh-3d-editor tsconfig 引用路径

* fix: 修复正则表达式 ReDoS 漏洞
2025-12-23 15:34:01 +08:00
yhh
49dd6a91c6 docs: v2.4.1 changelog 2025-12-23 15:22:33 +08:00
2042 changed files with 59332 additions and 25365 deletions

8
.changeset/README.md Normal file
View File

@@ -0,0 +1,8 @@
# Changesets
Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
with multi-package repos, or single-package repos to help you version and publish your code. You can
find the full documentation for it [in our repository](https://github.com/changesets/changesets)
We have a quick list of common questions to get you started engaging with this project in
[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)

58
.changeset/config.json Normal file
View File

@@ -0,0 +1,58 @@
{
"$schema": "https://unpkg.com/@changesets/config@3.1.2/schema.json",
"changelog": [
"@changesets/changelog-github",
{ "repo": "esengine/esengine" }
],
"commit": false,
"fixed": [],
"linked": [
["@esengine/ecs-framework", "@esengine/ecs-framework-math"]
],
"access": "public",
"baseBranch": "master",
"updateInternalDependencies": "patch",
"ignore": [
"@esengine/engine-core",
"@esengine/runtime-core",
"@esengine/asset-system",
"@esengine/material-system",
"@esengine/ecs-engine-bindgen",
"@esengine/script-runtime",
"@esengine/platform-common",
"@esengine/platform-web",
"@esengine/platform-wechat",
"@esengine/sprite",
"@esengine/camera",
"@esengine/particle",
"@esengine/tilemap",
"@esengine/mesh-3d",
"@esengine/effect",
"@esengine/audio",
"@esengine/fairygui",
"@esengine/physics-rapier2d",
"@esengine/rapier2d",
"@esengine/world-streaming",
"@esengine/network-server",
"@esengine/editor-core",
"@esengine/editor-runtime",
"@esengine/editor-app",
"@esengine/sprite-editor",
"@esengine/camera-editor",
"@esengine/particle-editor",
"@esengine/tilemap-editor",
"@esengine/mesh-3d-editor",
"@esengine/fairygui-editor",
"@esengine/physics-rapier2d-editor",
"@esengine/behavior-tree-editor",
"@esengine/blueprint-editor",
"@esengine/asset-system-editor",
"@esengine/material-editor",
"@esengine/shader-editor",
"@esengine/world-streaming-editor",
"@esengine/node-editor",
"@esengine/sdk",
"@esengine/worker-generator",
"@esengine/engine"
]
}

View File

@@ -6,3 +6,8 @@ paths-ignore:
- "**/node_modules"
- "**/dist"
- "**/bin"
- "**/tests"
- "**/*.test.ts"
- "**/*.spec.ts"
- "**/test"
- "**/__tests__"

32
.github/labeler.yml vendored
View File

@@ -1,32 +0,0 @@
# 自动标签配置
# 根据 issue/PR 内容自动打标签
'bug':
- '/(bug|错误|崩溃|crash|error|exception|问题)/i'
'enhancement':
- '/(feature|功能|enhancement|improve|优化|建议)/i'
'documentation':
- '/(doc|文档|readme|guide|tutorial|教程)/i'
'question':
- '/(question|疑问|how to|如何|怎么)/i'
'performance':
- '/(performance|性能|slow|慢|lag|卡顿|optimize)/i'
'core':
- '/(@esengine\/ecs-framework|packages\/core|core package)/i'
'editor':
- '/(editor|编辑器|tauri)/i'
'network':
- '/(network|网络|multiplayer|多人)/i'
'help wanted':
- '/(help wanted|需要帮助|求助)/i'
'good first issue':
- '/(good first issue|新手友好|beginner)/i'

View File

@@ -1,73 +0,0 @@
name: AI Batch Analyze Issues
on:
workflow_dispatch:
inputs:
mode:
description: '分析模式'
required: true
type: choice
options:
- 'recent' # 最近 10 个 issue
- 'open' # 所有打开的 issue
- 'all' # 所有 issue慎用
default: 'recent'
permissions:
issues: write
contents: read
jobs:
analyze:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20.x'
- name: Install GitHub CLI
run: |
gh --version || (curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null
sudo apt update
sudo apt install gh)
- name: Batch Analyze Issues
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
MODE="${{ github.event.inputs.mode }}"
# 获取 issue 列表
if [ "$MODE" = "recent" ]; then
echo "📊 分析最近 10 个 issue..."
ISSUES=$(gh issue list --limit 10 --json number --jq '.[].number')
elif [ "$MODE" = "open" ]; then
echo "📊 分析所有打开的 issue..."
ISSUES=$(gh issue list --state open --json number --jq '.[].number')
else
echo "📊 分析所有 issue这可能需要很长时间..."
ISSUES=$(gh issue list --state all --limit 100 --json number --jq '.[].number')
fi
# 为每个 issue 添加 AI 分析评论
for issue_num in $ISSUES; do
echo "🤖 分析 Issue #$issue_num..."
# 获取 issue 内容
ISSUE_BODY=$(gh issue view $issue_num --json body --jq '.body')
ISSUE_TITLE=$(gh issue view $issue_num --json title --jq '.title')
# 添加触发评论
gh issue comment $issue_num --body "@ai-helper 请帮我分析这个 issue" || true
# 避免 API 限制
sleep 2
done
echo "✅ 批量分析完成!"
echo "查看结果https://github.com/${{ github.repository }}/issues"

View File

@@ -1,61 +0,0 @@
name: AI Helper Tip
# 对所有新创建的 issue 自动回复 AI 助手使用说明(新老用户都适用)
on:
issues:
types: [opened]
permissions:
issues: write
jobs:
tip:
runs-on: ubuntu-latest
steps:
- name: Post AI Helper Usage Tip
uses: actions/github-script@v7
with:
script: |
const message = [
"## 🤖 AI 助手可用 | AI Helper Available",
"",
"**中文说明:**",
"",
"本项目配备了 AI 智能助手,可以帮助你快速获得解答!",
"",
"**使用方法:** 在评论中提及 `@ai-helper`AI 会自动搜索项目代码并提供解决方案。",
"",
"**示例:**",
"```",
"@ai-helper 如何创建一个新的 System",
"@ai-helper 这个报错是什么原因?",
"```",
"",
"---",
"",
"**English:**",
"",
"This project has an AI assistant to help you get answers quickly!",
"",
"**How to use:** Mention `@ai-helper` in a comment, and AI will automatically search the codebase and provide solutions.",
"",
"**Examples:**",
"```",
"@ai-helper How do I create a new System?",
"@ai-helper What causes this error?",
"```",
"",
"---",
"",
"💡 *AI 助手基于代码库提供建议,复杂问题建议等待维护者回复*",
"💡 *AI suggestions are based on the codebase. For complex issues, please wait for maintainer responses*"
].join('\n');
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: message
});
console.log('✅ AI helper tip posted successfully');

View File

@@ -1,85 +0,0 @@
name: AI Issue Helper
on:
issue_comment:
types: [created]
permissions:
issues: write
contents: read
models: read
jobs:
ai-helper:
runs-on: ubuntu-latest
# 只在真实用户提到 @ai-helper 时触发,忽略机器人评论
if: |
contains(github.event.comment.body, '@ai-helper') &&
github.event.comment.user.type != 'Bot'
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Get Issue Details
id: issue
uses: actions/github-script@v7
with:
script: |
const issue = await github.rest.issues.get({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number
});
// 限制长度,避免超过 token 限制
const maxLength = 1000;
const truncate = (str, max) => {
if (!str) return '';
return str.length > max ? str.substring(0, max) + '...[内容过长已截断]' : str;
};
core.exportVariable('ISSUE_TITLE', truncate(issue.data.title || '', 200));
core.exportVariable('ISSUE_BODY', truncate(issue.data.body || '', maxLength));
core.exportVariable('COMMENT_BODY', truncate(context.payload.comment.body || '', 500));
core.exportVariable('ISSUE_NUMBER', context.issue.number);
- name: Create Prompt
id: prompt
run: |
cat > prompt.txt << 'PROMPT_EOF'
Issue #${{ env.ISSUE_NUMBER }}
标题: ${{ env.ISSUE_TITLE }}
内容: ${{ env.ISSUE_BODY }}
评论: ${{ env.COMMENT_BODY }}
请搜索项目代码并提供解决方案。
PROMPT_EOF
- name: AI Analysis
uses: actions/ai-inference@v1
id: ai
with:
model: 'gpt-4o'
enable-github-mcp: true
max-tokens: 1500
system-prompt: |
你是 ECS Framework (TypeScript ECS 框架) 的 AI 助手。
主要代码在 packages/core/src。
搜索相关代码后,用中文简洁回答问题,包含问题分析、解决方案和代码引用。
prompt-file: prompt.txt
- name: Post AI Response
env:
AI_RESPONSE: ${{ steps.ai.outputs.response }}
uses: actions/github-script@v7
with:
script: |
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: process.env.AI_RESPONSE
});

View File

@@ -1,56 +0,0 @@
name: AI Issue Moderator
on:
issues:
types: [opened]
issue_comment:
types: [created]
permissions:
issues: write
contents: read
models: read
jobs:
moderate:
runs-on: ubuntu-latest
steps:
- name: Check Content
uses: actions/ai-inference@v1
id: check
with:
model: 'gpt-4o-mini'
system-prompt: |
你是一个内容审查助手。
检查内容是否包含:
1. 垃圾信息或广告
2. 恶意或攻击性内容
3. 与项目完全无关的内容
只返回 "SPAM" 或 "OK",不要其他内容。
prompt: |
标题:${{ github.event.issue.title || github.event.comment.body }}
内容:
${{ github.event.issue.body || github.event.comment.body }}
- name: Mark as Spam
if: contains(steps.check.outputs.response, 'SPAM')
uses: actions/github-script@v7
with:
script: |
// 添加 spam 标签
github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
labels: ['spam']
});
// 添加评论
github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: '🤖 这个内容被 AI 检测为可能的垃圾内容。如果这是误判,请联系维护者。\n\n🤖 This content was detected as potential spam by AI. If this is a false positive, please contact the maintainers.'
});

View File

@@ -1,160 +0,0 @@
name: Batch Label Issues
on:
workflow_dispatch:
inputs:
mode:
description: '标签模式'
required: true
type: choice
options:
- 'recent' # 最近 20 个 issue
- 'open' # 所有打开的 issue
- 'unlabeled' # 只处理没有标签的 issue
- 'all' # 所有 issue慎用
default: 'recent'
skip_labeled:
description: '跳过已有标签的 issue'
required: false
type: boolean
default: true
permissions:
issues: write
contents: read
jobs:
batch-label:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20.x'
- name: Batch Label Issues
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
MODE="${{ github.event.inputs.mode }}"
SKIP_LABELED="${{ github.event.inputs.skip_labeled }}"
echo "📊 开始批量打标签..."
echo "模式: $MODE"
echo "跳过已标签: $SKIP_LABELED"
# 获取 issue 列表
if [ "$MODE" = "recent" ]; then
echo "📋 获取最近 20 个 issue..."
ISSUES=$(gh issue list --limit 20 --json number,labels,title,body --jq '.[] | {number, labels: [.labels[].name], title, body}')
elif [ "$MODE" = "open" ]; then
echo "📋 获取所有打开的 issue..."
ISSUES=$(gh issue list --state open --json number,labels,title,body --jq '.[] | {number, labels: [.labels[].name], title, body}')
elif [ "$MODE" = "unlabeled" ]; then
echo "📋 获取没有标签的 issue..."
ISSUES=$(gh issue list --state all --json number,labels,title,body --jq '.[] | select(.labels | length == 0) | {number, labels: [.labels[].name], title, body}')
else
echo "📋 获取所有 issue限制 100 个)..."
ISSUES=$(gh issue list --state all --limit 100 --json number,labels,title,body --jq '.[] | {number, labels: [.labels[].name], title, body}')
fi
# 临时文件
echo "$ISSUES" > /tmp/issues.json
# 处理每个 issue
cat /tmp/issues.json | jq -c '.' | while read -r issue; do
ISSUE_NUM=$(echo "$issue" | jq -r '.number')
EXISTING_LABELS=$(echo "$issue" | jq -r '.labels | join(",")')
TITLE=$(echo "$issue" | jq -r '.title')
BODY=$(echo "$issue" | jq -r '.body')
echo ""
echo "🔍 处理 Issue #$ISSUE_NUM: $TITLE"
echo " 现有标签: $EXISTING_LABELS"
# 跳过已有标签的 issue
if [ "$SKIP_LABELED" = "true" ] && [ ! -z "$EXISTING_LABELS" ]; then
echo " ⏭️ 跳过(已有标签)"
continue
fi
# 分析内容并打标签
LABELS_TO_ADD=""
# 检测 bug
if echo "$TITLE $BODY" | grep -iE "(bug|错误|崩溃|crash|error|exception|问题|fix)" > /dev/null; then
LABELS_TO_ADD="$LABELS_TO_ADD bug"
echo " 🐛 检测到: bug"
fi
# 检测 feature request
if echo "$TITLE $BODY" | grep -iE "(feature|功能|enhancement|improve|优化|建议|新增|添加|add)" > /dev/null; then
LABELS_TO_ADD="$LABELS_TO_ADD enhancement"
echo " ✨ 检测到: enhancement"
fi
# 检测 question
if echo "$TITLE $BODY" | grep -iE "(question|疑问|how to|如何|怎么|为什么|why|咨询|\?|)" > /dev/null; then
LABELS_TO_ADD="$LABELS_TO_ADD question"
echo " ❓ 检测到: question"
fi
# 检测 documentation
if echo "$TITLE $BODY" | grep -iE "(doc|文档|readme|guide|tutorial|教程|说明)" > /dev/null; then
LABELS_TO_ADD="$LABELS_TO_ADD documentation"
echo " 📖 检测到: documentation"
fi
# 检测 performance
if echo "$TITLE $BODY" | grep -iE "(performance|性能|slow|慢|lag|卡顿|optimize|优化)" > /dev/null; then
LABELS_TO_ADD="$LABELS_TO_ADD performance"
echo " ⚡ 检测到: performance"
fi
# 检测 core
if echo "$TITLE $BODY" | grep -iE "(@esengine/ecs-framework|packages/core|core package|核心包)" > /dev/null; then
LABELS_TO_ADD="$LABELS_TO_ADD core"
echo " 🎯 检测到: core"
fi
# 检测 editor
if echo "$TITLE $BODY" | grep -iE "(editor|编辑器|tauri)" > /dev/null; then
LABELS_TO_ADD="$LABELS_TO_ADD editor"
echo " 🎨 检测到: editor"
fi
# 检测 network
if echo "$TITLE $BODY" | grep -iE "(network|网络|multiplayer|多人|同步)" > /dev/null; then
LABELS_TO_ADD="$LABELS_TO_ADD network"
echo " 🌐 检测到: network"
fi
# 检测 help wanted
if echo "$TITLE $BODY" | grep -iE "(help wanted|需要帮助|求助)" > /dev/null; then
LABELS_TO_ADD="$LABELS_TO_ADD help wanted"
echo " 🆘 检测到: help wanted"
fi
# 添加标签
if [ ! -z "$LABELS_TO_ADD" ]; then
echo " ✅ 添加标签: $LABELS_TO_ADD"
for label in $LABELS_TO_ADD; do
gh issue edit $ISSUE_NUM --add-label "$label" 2>&1 | grep -v "already exists" || true
done
echo " 💬 添加说明评论..."
gh issue comment $ISSUE_NUM --body $'🤖 自动标签系统检测到此 issue 并添加了相关标签。如有误判,请告知维护者。\n\n🤖 Auto-labeling system detected and labeled this issue. Please let maintainers know if this is incorrect.' || true
else
echo " 未检测到明确类型"
fi
# 避免 API 限制
sleep 1
done
echo ""
echo "✅ 批量标签完成!"
echo "查看结果: https://github.com/${{ github.repository }}/issues"

View File

@@ -13,18 +13,35 @@ on:
- '.github/workflows/ci.yml'
pull_request:
branches: [ master, main, develop ]
paths:
- 'packages/**'
- 'package.json'
- 'pnpm-lock.yaml'
- 'tsconfig.json'
- 'turbo.json'
- 'jest.config.*'
- '.github/workflows/ci.yml'
# Run on all PRs to satisfy branch protection, but skip build if no code changes
jobs:
ci:
# Check if we need to run the full CI
check-changes:
runs-on: ubuntu-latest
outputs:
should-run: ${{ steps.filter.outputs.code }}
steps:
- uses: actions/checkout@v4
- uses: dorny/paths-filter@v3
id: filter
with:
filters: |
code:
- 'packages/**'
- 'package.json'
- 'pnpm-lock.yaml'
- 'tsconfig.json'
- 'turbo.json'
- 'jest.config.*'
ci:
needs: check-changes
if: needs.check-changes.outputs.should-run == 'true'
runs-on: ubuntu-latest
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
steps:
- name: Checkout code
@@ -39,67 +56,35 @@ 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 --no-frozen-lockfile
# 缓存 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: Build all packages
run: pnpm run build
- name: Copy WASM files to ecs-engine-bindgen
# 构建 framework 包 (可独立发布的通用库,无外部依赖)
- name: Build framework packages
run: |
mkdir -p packages/ecs-engine-bindgen/src/wasm
cp packages/engine/pkg/es_engine.js packages/ecs-engine-bindgen/src/wasm/
cp packages/engine/pkg/es_engine.d.ts packages/ecs-engine-bindgen/src/wasm/
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/
pnpm --filter @esengine/ecs-framework build
pnpm --filter @esengine/ecs-framework-math build
pnpm --filter @esengine/behavior-tree build
pnpm --filter @esengine/blueprint build
pnpm --filter @esengine/fsm build
pnpm --filter @esengine/timer build
pnpm --filter @esengine/spatial build
pnpm --filter @esengine/procgen build
pnpm --filter @esengine/pathfinding build
pnpm --filter @esengine/network-protocols build
pnpm --filter @esengine/network build
# 类型检查
- name: Type check
run: pnpm run type-check
# 类型检查 (仅 framework 包)
- name: Type check (framework packages)
run: pnpm run type-check:framework
# Lint 检查
- name: Lint check
run: pnpm run lint
# Lint 检查 (仅 framework 包)
- name: Lint check (framework packages)
run: pnpm run lint:framework
# 测试
# 测试 (仅 framework 包)
- name: Run tests with coverage
run: pnpm run test:ci
run: pnpm run test:ci:framework
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
@@ -110,9 +95,11 @@ jobs:
name: codecov-umbrella
fail_ci_if_error: false
# 构建 npm 包
# 构建 npm 包 (core 和 math)
- name: Build npm packages
run: pnpm run build:npm
run: |
pnpm run build:npm:core
pnpm run build:npm:math
# 上传构建产物
- name: Upload build artifacts
@@ -120,6 +107,6 @@ jobs:
with:
name: build-artifacts
path: |
packages/*/dist/
packages/*/bin/
packages/framework/**/dist/
packages/framework/**/bin/
retention-days: 7

View File

@@ -1,146 +0,0 @@
name: Cleanup Old Dependabot PRs
# 手动触发的 workflow用于清理堆积的 Dependabot PR
on:
workflow_dispatch:
inputs:
days_old:
description: '关闭多少天前创建的 PR默认 7 天)'
required: false
default: '7'
dry_run:
description: '试运行模式true=仅显示,不关闭)'
required: false
default: 'true'
type: choice
options:
- 'true'
- 'false'
jobs:
cleanup:
runs-on: ubuntu-latest
permissions:
pull-requests: write
issues: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: List and Close Old Dependabot PRs
uses: actions/github-script@v7
with:
script: |
const daysOld = parseInt('${{ github.event.inputs.days_old }}') || 7;
const dryRun = '${{ github.event.inputs.dry_run }}' === 'true';
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - daysOld);
console.log(`🔍 查找超过 ${daysOld} 天的 Dependabot PR...`);
console.log(`📅 截止日期: ${cutoffDate.toISOString()}`);
console.log(`🏃 模式: ${dryRun ? '试运行(不会实际关闭)' : '实际执行'}`);
console.log('---');
// 获取所有 Dependabot PR
const { data: pulls } = await github.rest.pulls.list({
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
per_page: 100
});
const dependabotPRs = pulls.filter(pr =>
pr.user.login === 'dependabot[bot]' &&
new Date(pr.created_at) < cutoffDate
);
console.log(`📊 找到 ${dependabotPRs.length} 个符合条件的 Dependabot PR`);
console.log('');
if (dependabotPRs.length === 0) {
console.log('✅ 没有需要清理的 PR');
return;
}
// 按类型分组
const byType = {
dev: [],
prod: [],
actions: [],
other: []
};
for (const pr of dependabotPRs) {
const title = pr.title.toLowerCase();
const labels = pr.labels.map(l => l.name);
let type = 'other';
if (title.includes('dev-dependencies') || title.includes('development')) {
type = 'dev';
} else if (title.includes('production-dependencies')) {
type = 'prod';
} else if (labels.includes('github-actions')) {
type = 'actions';
}
byType[type].push(pr);
}
console.log('📋 PR 分类统计:');
console.log(` 🔧 开发依赖: ${byType.dev.length} 个`);
console.log(` 📦 生产依赖: ${byType.prod.length} 个`);
console.log(` ⚙️ GitHub Actions: ${byType.actions.length} 个`);
console.log(` ❓ 其他: ${byType.other.length} 个`);
console.log('');
// 处理每个 PR
for (const pr of dependabotPRs) {
const age = Math.floor((Date.now() - new Date(pr.created_at)) / (1000 * 60 * 60 * 24));
console.log(`${dryRun ? '🔍' : '🗑️ '} #${pr.number}: ${pr.title}`);
console.log(` 创建时间: ${pr.created_at} (${age} 天前)`);
console.log(` 链接: ${pr.html_url}`);
if (!dryRun) {
await github.rest.pulls.update({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: pr.number,
state: 'closed'
});
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr.number,
body: `🤖 **自动关闭旧的 Dependabot PR**
此 PR 已超过 ${daysOld} 天未合并,已被自动关闭以清理积压。
📌 **下一步:**
- Dependabot 已配置为月度运行,届时会创建新的分组更新
- 新的 Mergify 规则会智能处理不同类型的依赖更新
- 开发依赖和 GitHub Actions 会自动合并(即使 CI 失败)
- 生产依赖需要 CI 通过才会自动合并
如果需要立即应用此更新,请手动更新依赖。
---
*此操作由仓库维护者手动触发的清理工作流执行*`
});
console.log(' ✅ 已关闭并添加说明');
} else {
console.log(' 试运行模式 - 未执行操作');
}
console.log('');
}
console.log('---');
if (dryRun) {
console.log(`✨ 试运行完成!共发现 ${dependabotPRs.length} 个待清理的 PR`);
console.log('💡 要实际执行清理,请将 dry_run 参数设为 false 重新运行');
} else {
console.log(`✅ 清理完成!已关闭 ${dependabotPRs.length} 个 Dependabot PR`);
}

View File

@@ -28,7 +28,7 @@ jobs:
- name: Run tests with coverage
run: |
cd packages/core
cd packages/framework/core
pnpm run test:coverage
- name: Upload coverage to Codecov
@@ -36,7 +36,7 @@ jobs:
continue-on-error: true
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./packages/core/coverage/coverage-final.json
files: ./packages/framework/core/coverage/coverage-final.json
flags: core
name: core-coverage
fail_ci_if_error: false
@@ -46,4 +46,4 @@ jobs:
uses: actions/upload-artifact@v4
with:
name: coverage-report
path: packages/core/coverage/
path: packages/framework/core/coverage/

View File

@@ -1,23 +0,0 @@
name: Issue Labeler
on:
issues:
types: [opened, edited]
permissions:
issues: write
contents: read
jobs:
label:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Label Issues
uses: github/issue-labeler@v3.4
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
configuration-path: .github/labeler.yml
enable-versioned-regex: 1

View File

@@ -1,28 +0,0 @@
name: Issue Translator
on:
issue_comment:
types: [created]
issues:
types: [opened]
permissions:
issues: write
jobs:
translate:
runs-on: ubuntu-latest
steps:
- name: Translate Issues
uses: tomsun28/issues-translate-action@v2.7
with:
IS_MODIFY_TITLE: false
# 设置为 true 会修改标题false 只在评论中添加翻译
CUSTOM_BOT_NOTE: |
<details>
<summary>🌏 Translation / 翻译</summary>
Bot detected the issue body's language is not English, translate it automatically.
机器人检测到 issue 内容非英文,自动翻译。
</details>

View File

@@ -0,0 +1,70 @@
name: Release (Changesets)
on:
push:
branches:
- master
paths:
- '.changeset/**'
- 'packages/*/package.json'
- 'packages/*/*/package.json'
- 'packages/*/*/*/package.json'
workflow_dispatch:
concurrency: ${{ github.workflow }}-${{ github.ref }}
jobs:
release:
name: Release
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
id-token: write
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install pnpm
uses: pnpm/action-setup@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
registry-url: 'https://registry.npmjs.org'
cache: 'pnpm'
- name: Install dependencies
run: pnpm install
- name: Build framework packages
run: |
# Only build packages managed by Changesets (not in ignore list)
pnpm --filter "@esengine/ecs-framework" build
pnpm --filter "@esengine/ecs-framework-math" build
pnpm --filter "@esengine/behavior-tree" build
pnpm --filter "@esengine/blueprint" build
pnpm --filter "@esengine/fsm" build
pnpm --filter "@esengine/timer" build
pnpm --filter "@esengine/spatial" build
pnpm --filter "@esengine/procgen" build
pnpm --filter "@esengine/pathfinding" build
pnpm --filter "@esengine/network-protocols" build
pnpm --filter "@esengine/network" build
pnpm --filter "@esengine/cli" build
- name: Create Release Pull Request or Publish
id: changesets
uses: changesets/action@v1
with:
version: pnpm changeset:version
publish: pnpm changeset:publish
title: 'chore: release packages'
commit: 'chore: release packages'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

View File

@@ -50,7 +50,7 @@ jobs:
- name: Rust cache
uses: Swatinem/rust-cache@v2
with:
workspaces: packages/editor-app/src-tauri
workspaces: packages/editor/editor-app/src-tauri
cache-on-failure: true
- name: Install dependencies (Ubuntu)
@@ -65,7 +65,7 @@ jobs:
- name: Update version in config files (for manual trigger)
if: github.event_name == 'workflow_dispatch'
run: |
cd packages/editor-app
cd packages/editor/editor-app
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
@@ -80,15 +80,15 @@ jobs:
- name: Copy WASM files to ecs-engine-bindgen
shell: bash
run: |
mkdir -p packages/ecs-engine-bindgen/src/wasm
cp packages/engine/pkg/es_engine.js packages/ecs-engine-bindgen/src/wasm/
cp packages/engine/pkg/es_engine.d.ts packages/ecs-engine-bindgen/src/wasm/
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/
mkdir -p packages/engine/ecs-engine-bindgen/src/wasm
cp packages/rust/engine/pkg/es_engine.js packages/engine/ecs-engine-bindgen/src/wasm/
cp packages/rust/engine/pkg/es_engine.d.ts packages/engine/ecs-engine-bindgen/src/wasm/
cp packages/rust/engine/pkg/es_engine_bg.wasm packages/engine/ecs-engine-bindgen/src/wasm/
cp packages/rust/engine/pkg/es_engine_bg.wasm.d.ts packages/engine/ecs-engine-bindgen/src/wasm/
- name: Bundle runtime files for Tauri
run: |
cd packages/editor-app
cd packages/editor/editor-app
node scripts/bundle-runtime.mjs
- name: Build Tauri app
@@ -99,7 +99,7 @@ jobs:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
with:
projectPath: packages/editor-app
projectPath: packages/editor/editor-app
tagName: ${{ github.event_name == 'workflow_dispatch' && format('editor-v{0}', github.event.inputs.version) || github.ref_name }}
releaseName: 'ECS Editor v${{ github.event.inputs.version || github.ref_name }}'
releaseBody: 'See the assets to download this version and install.'
@@ -116,8 +116,8 @@ jobs:
with:
name: windows-unsigned
path: |
packages/editor-app/src-tauri/target/release/bundle/nsis/*.exe
packages/editor-app/src-tauri/target/release/bundle/msi/*.msi
packages/editor/editor-app/src-tauri/target/release/bundle/nsis/*.exe
packages/editor/editor-app/src-tauri/target/release/bundle/msi/*.msi
retention-days: 1
# SignPath 代码签名Windows
@@ -221,7 +221,7 @@ jobs:
- name: Update version files
run: |
cd packages/editor-app
cd packages/editor/editor-app
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
@@ -239,8 +239,8 @@ jobs:
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/editor-app/package.json` → `${{ github.event.inputs.version }}`
- Updated `packages/editor/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 }})

View File

@@ -1,7 +1,6 @@
name: Release NPM Packages
on:
# 标签触发:支持 v* 和 {package}-v* 格式
# Tag trigger: supports v* and {package}-v* formats
push:
tags:
@@ -15,12 +14,11 @@ on:
- 'physics-rapier2d-v*'
- 'worker-generator-v*'
# 保留手动触发选项
# Keep manual trigger option
# Manual trigger option
workflow_dispatch:
inputs:
package:
description: '选择要发布的包 | Select package to publish'
description: 'Select package to publish'
required: true
type: choice
options:
@@ -33,7 +31,7 @@ on:
- physics-rapier2d
- worker-generator
version_type:
description: '版本更新类型 | Version bump type'
description: 'Version bump type'
required: true
type: choice
options:
@@ -61,11 +59,10 @@ jobs:
id: parse
run: |
if [ "${{ github.event_name }}" = "push" ]; then
# 从标签解析包名和版本 | Parse package and version from tag
# Parse package and version from tag
TAG="${GITHUB_REF#refs/tags/}"
echo "tag=$TAG" >> $GITHUB_OUTPUT
# 解析格式v1.0.0 或 package-v1.0.0
# Parse format: v1.0.0 or package-v1.0.0
if [[ "$TAG" =~ ^v([0-9]+\.[0-9]+\.[0-9]+.*)$ ]]; then
PACKAGE="core"
@@ -82,10 +79,9 @@ jobs:
echo "package=$PACKAGE" >> $GITHUB_OUTPUT
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "mode=tag" >> $GITHUB_OUTPUT
echo "📦 Package: $PACKAGE"
echo "📌 Version: $VERSION"
echo "Package: $PACKAGE"
echo "Version: $VERSION"
else
# 手动触发:从 package.json 读取并 bump 版本
# Manual trigger: read from package.json and bump version
PACKAGE="${{ github.event.inputs.package }}"
echo "package=$PACKAGE" >> $GITHUB_OUTPUT
@@ -112,7 +108,6 @@ jobs:
PACKAGE="${{ steps.parse.outputs.package }}"
EXPECTED_VERSION="${{ steps.parse.outputs.version }}"
# 获取 package.json 中的版本
# Get version from package.json
ACTUAL_VERSION=$(node -p "require('./packages/$PACKAGE/package.json').version")
@@ -125,7 +120,7 @@ jobs:
exit 1
fi
echo "Version verified: $EXPECTED_VERSION"
echo "Version verified: $EXPECTED_VERSION"
- name: Bump version (manual mode)
if: steps.parse.outputs.mode == 'manual'
@@ -147,7 +142,7 @@ jobs:
node -e "const fs=require('fs'); const pkg=JSON.parse(fs.readFileSync('package.json')); pkg.version='$NEW_VERSION'; fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2)+'\n')"
echo "version=$NEW_VERSION" >> $GITHUB_OUTPUT
echo "📌 Bumped version: $CURRENT $NEW_VERSION"
echo "Bumped version: $CURRENT -> $NEW_VERSION"
- name: Set final version
id: version
@@ -161,7 +156,7 @@ jobs:
- name: Build core package (if needed)
if: ${{ steps.parse.outputs.package != 'core' && steps.parse.outputs.package != 'node-editor' && steps.parse.outputs.package != 'worker-generator' }}
run: |
cd packages/core
cd packages/framework/core
pnpm run build
- name: Build node-editor package (if needed for blueprint)
@@ -188,17 +183,18 @@ jobs:
with:
tag_name: ${{ steps.parse.outputs.tag }}
name: "${{ steps.parse.outputs.package }} v${{ steps.version.outputs.value }}"
make_latest: false
body: |
## 🚀 @esengine/${{ steps.parse.outputs.package }} v${{ steps.version.outputs.value }}
## @esengine/${{ steps.parse.outputs.package }} v${{ steps.version.outputs.value }}
📦 **NPM**: [@esengine/${{ steps.parse.outputs.package }}@${{ steps.version.outputs.value }}](https://www.npmjs.com/package/@esengine/${{ steps.parse.outputs.package }}/v/${{ steps.version.outputs.value }})
**NPM**: [@esengine/${{ steps.parse.outputs.package }}@${{ steps.version.outputs.value }}](https://www.npmjs.com/package/@esengine/${{ steps.parse.outputs.package }}/v/${{ steps.version.outputs.value }})
```bash
npm install @esengine/${{ steps.parse.outputs.package }}@${{ steps.version.outputs.value }}
```
---
*自动发布 | Auto-released by GitHub Actions*
*Auto-released by GitHub Actions*
generate_release_notes: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -213,16 +209,16 @@ jobs:
delete-branch: true
title: "chore(${{ steps.parse.outputs.package }}): Release v${{ steps.version.outputs.value }}"
body: |
## 🚀 Release v${{ steps.version.outputs.value }}
## Release v${{ steps.version.outputs.value }}
此 PR 更新 `@esengine/${{ steps.parse.outputs.package }}` 包的版本号
This PR updates `@esengine/${{ steps.parse.outputs.package }}` package version.
### 变更
- ✅ 已发布到 npm: [@esengine/${{ steps.parse.outputs.package }}@${{ steps.version.outputs.value }}](https://www.npmjs.com/package/@esengine/${{ steps.parse.outputs.package }}/v/${{ steps.version.outputs.value }})
- ✅ 更新 `packages/${{ steps.parse.outputs.package }}/package.json` `${{ steps.version.outputs.value }}`
### Changes
- Published to npm: [@esengine/${{ steps.parse.outputs.package }}@${{ steps.version.outputs.value }}](https://www.npmjs.com/package/@esengine/${{ steps.parse.outputs.package }}/v/${{ steps.version.outputs.value }})
- Updated `packages/${{ steps.parse.outputs.package }}/package.json` to `${{ steps.version.outputs.value }}`
---
*此 PR 由发布工作流自动创建*
*This PR was automatically created by the release workflow*
labels: |
release
${{ steps.parse.outputs.package }}

View File

@@ -1,46 +0,0 @@
name: Size Limit
on:
pull_request:
branches:
- master
- main
paths:
- 'packages/core/src/**'
- 'packages/core/package.json'
- '.size-limit.json'
permissions:
contents: read
pull-requests: write
issues: write
jobs:
size:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20.x'
cache: 'pnpm'
- name: Install dependencies
run: pnpm install
- name: Build core package
run: |
cd packages/core
pnpm run build:npm
- name: Check bundle size
uses: andresz1/size-limit-action@v1
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
skip_step: install

View File

@@ -1,58 +0,0 @@
name: Welcome
on:
issues:
types: [opened]
pull_request_target:
types: [opened]
permissions:
issues: write
pull-requests: write
jobs:
welcome:
runs-on: ubuntu-latest
steps:
- name: Welcome new contributors
uses: actions/first-interaction@v1.3.0
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
issue-message: |
👋 你好!感谢你提交第一个 issue
我们会尽快查看并回复。同时,建议你:
- 📚 查看[文档](https://esengine.github.io/ecs-framework/)
- 🤖 使用 [AI 文档助手](https://deepwiki.com/esengine/esengine)
- 💬 加入 [QQ 交流群](https://jq.qq.com/?_wv=1027&k=29w1Nud6)
---
👋 Hello! Thanks for opening your first issue!
We'll review it as soon as possible. Meanwhile, you might want to:
- 📚 Check the [documentation](https://esengine.github.io/ecs-framework/)
- 🤖 Use [AI documentation assistant](https://deepwiki.com/esengine/esengine)
pr-message: |
👋 你好!感谢你提交第一个 Pull Request
在我们 Review 之前,请确保:
- ✅ 代码遵循项目规范
- ✅ 通过所有测试
- ✅ 更新了相关文档
- ✅ Commit 遵循 [Conventional Commits](https://www.conventionalcommits.org/) 规范
查看完整的[贡献指南](https://github.com/esengine/esengine/blob/master/CONTRIBUTING.md)。
---
👋 Hello! Thanks for your first Pull Request!
Before we review, please ensure:
- ✅ Code follows project conventions
- ✅ All tests pass
- ✅ Documentation is updated
- ✅ Commits follow [Conventional Commits](https://www.conventionalcommits.org/)
See the full [Contributing Guide](https://github.com/esengine/esengine/blob/master/CONTRIBUTING.md).

289
README.md
View File

@@ -5,7 +5,7 @@
</h1>
<p align="center">
<strong>Cross-platform 2D Game Engine</strong>
<strong>Modular Game Framework for TypeScript</strong>
</p>
<p align="center">
@@ -23,62 +23,58 @@
<p align="center">
<a href="https://esengine.cn/">Documentation</a> ·
<a href="https://esengine.cn/api/README">API Reference</a> ·
<a href="https://github.com/esengine/esengine/releases">Download Editor</a> ·
<a href="./examples/">Examples</a>
</p>
---
## Overview
## What is ESEngine?
ESEngine is a cross-platform 2D game engine built from the ground up with modern web technologies. It provides a comprehensive toolset that enables developers to focus on creating games rather than building infrastructure.
ESEngine is a collection of **engine-agnostic game development modules** for TypeScript. Use them with Cocos Creator, Laya, Phaser, PixiJS, or any JavaScript game engine.
Export your games to multiple platforms including web browsers, WeChat Mini Games, and other mini-game platforms from a single codebase.
## Key Features
| Feature | Description |
|---------|-------------|
| **ECS Architecture** | Data-driven Entity-Component-System pattern for flexible and cache-friendly game logic |
| **High-Performance Rendering** | Rust/WebAssembly 2D renderer with automatic sprite batching and WebGL 2.0 backend |
| **Visual Editor** | Cross-platform desktop editor built with Tauri for scene management and asset workflows |
| **Modular Design** | Import only what you need - each feature is a standalone package |
| **Multi-Platform Export** | Deploy to Web, WeChat Mini Games, and more from one codebase |
| **Physics Integration** | 2D physics powered by Rapier with editor visualization |
| **Visual Scripting** | Behavior trees and blueprint system for designers |
## Tech Stack
- **Runtime**: TypeScript, Rust, WebAssembly
- **Renderer**: WebGL 2.0, WGPU (planned)
- **Editor**: Tauri, React, Zustand
- **Physics**: Rapier2D
- **Build**: pnpm, Turborepo, Rollup
## License
ESEngine is **free and open source** under the [MIT License](LICENSE). No royalties, no strings attached.
## Installation
### npm
The core is a high-performance **ECS (Entity-Component-System)** framework, accompanied by optional modules for AI, networking, physics, and more.
```bash
npm install @esengine/ecs-framework
```
### Editor
## Features
Download pre-built binaries from the [Releases](https://github.com/esengine/esengine/releases) page (Windows, macOS).
| Module | Description | Engine Required |
|--------|-------------|:---------------:|
| **ECS Core** | Entity-Component-System framework with reactive queries | No |
| **Behavior Tree** | AI behavior trees with visual editor support | No |
| **Blueprint** | Visual scripting system | No |
| **FSM** | Finite state machine | No |
| **Timer** | Timer and cooldown systems | No |
| **Spatial** | Spatial indexing and queries (QuadTree, Grid) | No |
| **Pathfinding** | A* and navigation mesh pathfinding | No |
| **Network** | Client/server networking with TSRPC | No |
> All framework modules can be used standalone with any rendering engine.
## Quick Start
### Using CLI (Recommended)
The easiest way to add ECS to your existing project:
```bash
# In your project directory
npx @esengine/cli init
```
The CLI automatically detects your project type (Cocos Creator 2.x/3.x, LayaAir 3.x, or Node.js) and generates the necessary integration code.
### Manual Setup
```typescript
import {
Core, Scene, Entity, Component, EntitySystem,
Matcher, Time, ECSComponent, ECSSystem
} from '@esengine/ecs-framework';
// Define components (data only)
@ECSComponent('Position')
class Position extends Component {
x = 0;
@@ -91,6 +87,7 @@ class Velocity extends Component {
dy = 0;
}
// Define system (logic)
@ECSSystem('Movement')
class MovementSystem extends EntitySystem {
constructor() {
@@ -118,7 +115,7 @@ player.addComponent(new Velocity());
Core.setScene(scene);
// Game loop
// Integrate with your game loop
function gameLoop(currentTime: number) {
Core.update(currentTime / 1000);
requestAnimationFrame(gameLoop);
@@ -126,96 +123,132 @@ function gameLoop(currentTime: number) {
requestAnimationFrame(gameLoop);
```
## Using with Other Engines
ESEngine's framework modules are designed to work alongside your preferred rendering engine:
### With Cocos Creator
```typescript
import { Component as CCComponent, _decorator } from 'cc';
import { Core, Scene, Matcher, EntitySystem } from '@esengine/ecs-framework';
import { BehaviorTreeExecutionSystem } from '@esengine/behavior-tree';
const { ccclass } = _decorator;
@ccclass('GameManager')
export class GameManager extends CCComponent {
private ecsScene!: Scene;
start() {
Core.create();
this.ecsScene = new Scene();
// Add ECS systems
this.ecsScene.addSystem(new BehaviorTreeExecutionSystem());
this.ecsScene.addSystem(new MyGameSystem());
Core.setScene(this.ecsScene);
}
update(dt: number) {
Core.update(dt);
}
}
```
### With Laya 3.x
```typescript
import { Core, Scene } from '@esengine/ecs-framework';
import { FSMSystem } from '@esengine/fsm';
const { regClass } = Laya;
@regClass()
export class ECSManager extends Laya.Script {
private ecsScene = new Scene();
onAwake(): void {
Core.create();
this.ecsScene.addSystem(new FSMSystem());
Core.setScene(this.ecsScene);
}
onUpdate(): void {
Core.update(Laya.timer.delta / 1000);
}
onDestroy(): void {
Core.destroy();
}
}
```
## Packages
ESEngine is organized as a monorepo with modular packages.
### Framework (Engine-Agnostic)
### Core
These packages have **zero rendering dependencies** and work with any engine:
| Package | Description |
|---------|-------------|
| `@esengine/ecs-framework` | Core ECS framework with entity management, component system, and queries |
| `@esengine/math` | Vector, matrix, and mathematical utilities |
| `@esengine/engine` | Rust/WASM 2D renderer |
| `@esengine/engine-core` | Engine module system and lifecycle management |
```bash
npm install @esengine/ecs-framework # Core ECS
npm install @esengine/behavior-tree # AI behavior trees
npm install @esengine/blueprint # Visual scripting
npm install @esengine/fsm # State machines
npm install @esengine/timer # Timers & cooldowns
npm install @esengine/spatial # Spatial indexing
npm install @esengine/pathfinding # Pathfinding
npm install @esengine/network # Networking
```
### Runtime
### ESEngine Runtime (Optional)
| Package | Description |
|---------|-------------|
| `@esengine/sprite` | 2D sprite rendering and animation |
| `@esengine/tilemap` | Tile-based map rendering |
| `@esengine/physics-rapier2d` | 2D physics simulation (Rapier) |
| `@esengine/behavior-tree` | Behavior tree AI system |
| `@esengine/blueprint` | Visual scripting runtime |
| `@esengine/camera` | Camera control and management |
| `@esengine/audio` | Audio playback |
| `@esengine/ui` | UI components |
| `@esengine/material-system` | Material and shader system |
| `@esengine/asset-system` | Asset loading and management |
If you want a complete engine solution with rendering:
### Editor Extensions
| Category | Packages |
|----------|----------|
| **Core** | `engine-core`, `asset-system`, `material-system` |
| **Rendering** | `sprite`, `tilemap`, `particle`, `camera`, `mesh-3d` |
| **Physics** | `physics-rapier2d` |
| **Platform** | `platform-web`, `platform-wechat` |
| Package | Description |
|---------|-------------|
| `@esengine/sprite-editor` | Sprite inspector and tools |
| `@esengine/tilemap-editor` | Visual tilemap editor |
| `@esengine/physics-rapier2d-editor` | Physics collider visualization |
| `@esengine/behavior-tree-editor` | Visual behavior tree editor |
| `@esengine/blueprint-editor` | Visual scripting editor |
| `@esengine/material-editor` | Material editor |
### Editor (Optional)
### Platform
A visual editor built with Tauri for scene management:
| Package | Description |
|---------|-------------|
| `@esengine/platform-common` | Platform abstraction interfaces |
| `@esengine/platform-web` | Web browser runtime |
| `@esengine/platform-wechat` | WeChat Mini Game runtime |
- Download from [Releases](https://github.com/esengine/esengine/releases)
- Supports behavior tree editing, tilemap painting, visual scripting
## Editor
## Project Structure
The ESEngine Editor is a cross-platform desktop application built with Tauri and React.
### Features
- Scene hierarchy and entity management
- Component inspector with custom property editors
- Asset browser with drag-and-drop
- Tilemap editor with paint and fill tools
- Behavior tree visual editor
- Blueprint visual scripting
- Material and shader editing
- Built-in performance profiler
- Localization (English, Chinese)
### Screenshot
![ESEngine Editor](screenshots/main_screetshot.png)
## Platform Support
| Platform | Runtime | Editor |
|----------|:-------:|:------:|
| Web Browser | ✓ | - |
| Windows | - | ✓ |
| macOS | - | ✓ |
| WeChat Mini Game | In Progress | - |
| Playable Ads | Planned | - |
| Android | Planned | - |
| iOS | Planned | - |
```
esengine/
├── packages/
│ ├── framework/ # Engine-agnostic modules (NPM publishable)
│ │ ├── core/ # ECS Framework
│ │ ├── math/ # Math utilities
│ │ ├── behavior-tree/ # AI behavior trees
│ │ ├── blueprint/ # Visual scripting
│ │ ├── fsm/ # Finite state machine
│ │ ├── timer/ # Timer system
│ │ ├── spatial/ # Spatial queries
│ │ ├── pathfinding/ # Pathfinding
│ │ ├── procgen/ # Procedural generation
│ │ └── network/ # Networking
│ │
│ ├── engine/ # ESEngine runtime
│ ├── rendering/ # Rendering modules
│ ├── physics/ # Physics modules
│ ├── editor/ # Visual editor
│ └── rust/ # WASM renderer
├── docs/ # Documentation
└── examples/ # Examples
```
## Building from Source
### Prerequisites
- Node.js 18+
- pnpm 10+
- Rust toolchain (for WASM renderer)
- wasm-pack
### Setup
```bash
git clone https://github.com/esengine/esengine.git
cd esengine
@@ -223,42 +256,28 @@ cd esengine
pnpm install
pnpm build
# Optional: Build WASM renderer
pnpm build:wasm
```
# Type check framework packages
pnpm type-check:framework
### Run Editor
```bash
cd packages/editor-app
pnpm tauri:dev
```
### Project Structure
```
esengine/
├── packages/ # Engine packages (runtime, editor, platform)
├── docs/ # Documentation source
├── examples/ # Example projects
├── scripts/ # Build utilities
└── thirdparty/ # Third-party dependencies
# Run tests
pnpm test
```
## Documentation
- [Getting Started](https://esengine.cn/guide/getting-started.html)
- [Architecture Guide](https://esengine.cn/guide/)
- [ECS Framework Guide](./packages/framework/core/README.md)
- [Behavior Tree Guide](./packages/framework/behavior-tree/README.md)
- [API Reference](https://esengine.cn/api/README)
## Community
- [GitHub Issues](https://github.com/esengine/esengine/issues) - Bug reports and feature requests
- [GitHub Discussions](https://github.com/esengine/esengine/discussions) - Questions and ideas
- [Discord](https://discord.gg/gCAgzXFW) - Chat with the community
## Contributing
Contributions are welcome. Please read the contributing guidelines before submitting a pull request.
Contributions are welcome! Please read our contributing guidelines before submitting a pull request.
1. Fork the repository
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
@@ -268,10 +287,10 @@ Contributions are welcome. Please read the contributing guidelines before submit
## License
ESEngine is licensed under the [MIT License](LICENSE).
ESEngine is licensed under the [MIT License](LICENSE). Free for personal and commercial use.
---
<p align="center">
Made with ❤️ by the ESEngine team
Made with care by the ESEngine community
</p>

View File

@@ -5,7 +5,7 @@
</h1>
<p align="center">
<strong>跨平台 2D 游戏引擎</strong>
<strong>TypeScript 模块化游戏框架</strong>
</p>
<p align="center">
@@ -23,62 +23,58 @@
<p align="center">
<a href="https://esengine.cn/">文档</a> ·
<a href="https://esengine.cn/api/README">API 参考</a> ·
<a href="https://github.com/esengine/esengine/releases">下载编辑器</a> ·
<a href="./examples/">示例</a>
</p>
---
## 概述
## ESEngine 是什么?
ESEngine 是一款基于现代 Web 技术从零构建的跨平台 2D 游戏引擎。它提供完整的工具集,让开发者专注于游戏创作而非基础设施搭建
ESEngine 是一套**引擎无关的游戏开发模块**,可与 Cocos Creator、Laya、Phaser、PixiJS 等任何 JavaScript 游戏引擎配合使用
一套代码即可导出到 Web 浏览器、微信小游戏等多个平台
## 核心特性
| 特性 | 描述 |
|-----|------|
| **ECS 架构** | 数据驱动的实体-组件-系统模式,提供灵活且缓存友好的游戏逻辑 |
| **高性能渲染** | Rust/WebAssembly 2D 渲染器,支持自动精灵批处理和 WebGL 2.0 |
| **可视化编辑器** | 基于 Tauri 的跨平台桌面编辑器,支持场景管理和资源工作流 |
| **模块化设计** | 按需引入,每个功能都是独立的包 |
| **多平台导出** | 一套代码部署到 Web、微信小游戏等平台 |
| **物理集成** | 基于 Rapier 的 2D 物理,支持编辑器可视化 |
| **可视化脚本** | 行为树和蓝图系统,适合策划使用 |
## 技术栈
- **运行时**: TypeScript, Rust, WebAssembly
- **渲染器**: WebGL 2.0, WGPU (计划中)
- **编辑器**: Tauri, React, Zustand
- **物理**: Rapier2D
- **构建**: pnpm, Turborepo, Rollup
## 许可证
ESEngine **完全免费开源**,采用 [MIT 协议](LICENSE)。无版税,无附加条件。
## 安装
### npm
核心是一个高性能的 **ECS实体-组件-系统)** 框架,配套 AI、网络、物理等可选模块
```bash
npm install @esengine/ecs-framework
```
### 编辑器
## 功能模块
从 [Releases](https://github.com/esengine/esengine/releases) 页面下载预编译版本(支持 Windows、macOS
| 模块 | 描述 | 需要渲染引擎 |
|------|------|:----------:|
| **ECS 核心** | 实体-组件-系统框架,支持响应式查询 | 否 |
| **行为树** | AI 行为树,支持可视化编辑 | 否 |
| **蓝图** | 可视化脚本系统 | 否 |
| **状态机** | 有限状态机 | 否 |
| **定时器** | 定时器和冷却系统 | 否 |
| **空间索引** | 空间查询(四叉树、网格) | 否 |
| **寻路** | A* 和导航网格寻路 | 否 |
| **网络** | 客户端/服务端网络通信 (TSRPC) | 否 |
> 所有框架模块都可以独立使用,无需依赖特定渲染引擎。
## 快速开始
### 使用 CLI推荐
在现有项目中添加 ECS 的最简单方式:
```bash
# 在项目目录中运行
npx @esengine/cli init
```
CLI 会自动检测项目类型Cocos Creator 2.x/3.x、LayaAir 3.x 或 Node.js并生成相应的集成代码。
### 手动配置
```typescript
import {
Core, Scene, Entity, Component, EntitySystem,
Matcher, Time, ECSComponent, ECSSystem
} from '@esengine/ecs-framework';
// 定义组件(纯数据)
@ECSComponent('Position')
class Position extends Component {
x = 0;
@@ -91,6 +87,7 @@ class Velocity extends Component {
dy = 0;
}
// 定义系统(逻辑)
@ECSSystem('Movement')
class MovementSystem extends EntitySystem {
constructor() {
@@ -118,7 +115,7 @@ player.addComponent(new Velocity());
Core.setScene(scene);
// 游戏循环
// 集成到你的游戏循环
function gameLoop(currentTime: number) {
Core.update(currentTime / 1000);
requestAnimationFrame(gameLoop);
@@ -126,96 +123,132 @@ function gameLoop(currentTime: number) {
requestAnimationFrame(gameLoop);
```
## 模块
## 与其他引擎配合使用
ESEngine 采用 Monorepo 组织,包含多个模块化包。
ESEngine 的框架模块设计为可与你喜欢的渲染引擎配合使用:
### 核心
### 与 Cocos Creator 配合
| 包名 | 描述 |
```typescript
import { Component as CCComponent, _decorator } from 'cc';
import { Core, Scene, Matcher, EntitySystem } from '@esengine/ecs-framework';
import { BehaviorTreeExecutionSystem } from '@esengine/behavior-tree';
const { ccclass } = _decorator;
@ccclass('GameManager')
export class GameManager extends CCComponent {
private ecsScene!: Scene;
start() {
Core.create();
this.ecsScene = new Scene();
// 添加 ECS 系统
this.ecsScene.addSystem(new BehaviorTreeExecutionSystem());
this.ecsScene.addSystem(new MyGameSystem());
Core.setScene(this.ecsScene);
}
update(dt: number) {
Core.update(dt);
}
}
```
### 与 Laya 3.x 配合
```typescript
import { Core, Scene } from '@esengine/ecs-framework';
import { FSMSystem } from '@esengine/fsm';
const { regClass } = Laya;
@regClass()
export class ECSManager extends Laya.Script {
private ecsScene = new Scene();
onAwake(): void {
Core.create();
this.ecsScene.addSystem(new FSMSystem());
Core.setScene(this.ecsScene);
}
onUpdate(): void {
Core.update(Laya.timer.delta / 1000);
}
onDestroy(): void {
Core.destroy();
}
}
```
## 包列表
### 框架包(引擎无关)
这些包**零渲染依赖**,可与任何引擎配合使用:
```bash
npm install @esengine/ecs-framework # ECS 核心
npm install @esengine/behavior-tree # AI 行为树
npm install @esengine/blueprint # 可视化脚本
npm install @esengine/fsm # 状态机
npm install @esengine/timer # 定时器和冷却
npm install @esengine/spatial # 空间索引
npm install @esengine/pathfinding # 寻路
npm install @esengine/network # 网络
```
### ESEngine 运行时(可选)
如果你需要完整的引擎解决方案:
| 分类 | 包名 |
|------|------|
| `@esengine/ecs-framework` | ECS 框架核心,包含实体管理、组件系统和查询 |
| `@esengine/math` | 向量、矩阵和数学工具 |
| `@esengine/engine` | Rust/WASM 2D 渲染器 |
| `@esengine/engine-core` | 引擎模块系统和生命周期管理 |
| **核心** | `engine-core`, `asset-system`, `material-system` |
| **渲染** | `sprite`, `tilemap`, `particle`, `camera`, `mesh-3d` |
| **物理** | `physics-rapier2d` |
| **平台** | `platform-web`, `platform-wechat` |
### 运行时
### 编辑器(可选)
| 包名 | 描述 |
|------|------|
| `@esengine/sprite` | 2D 精灵渲染和动画 |
| `@esengine/tilemap` | Tilemap 渲染 |
| `@esengine/physics-rapier2d` | 2D 物理模拟 (Rapier) |
| `@esengine/behavior-tree` | 行为树 AI 系统 |
| `@esengine/blueprint` | 可视化脚本运行时 |
| `@esengine/camera` | 相机控制和管理 |
| `@esengine/audio` | 音频播放 |
| `@esengine/ui` | UI 组件 |
| `@esengine/material-system` | 材质和着色器系统 |
| `@esengine/asset-system` | 资源加载和管理 |
基于 Tauri 构建的可视化编辑器:
### 编辑器扩展
- 从 [Releases](https://github.com/esengine/esengine/releases) 下载
- 支持行为树编辑、Tilemap 绘制、可视化脚本
| 包名 | 描述 |
|------|------|
| `@esengine/sprite-editor` | 精灵检视器和工具 |
| `@esengine/tilemap-editor` | 可视化 Tilemap 编辑器 |
| `@esengine/physics-rapier2d-editor` | 物理碰撞体可视化 |
| `@esengine/behavior-tree-editor` | 可视化行为树编辑器 |
| `@esengine/blueprint-editor` | 可视化脚本编辑器 |
| `@esengine/material-editor` | 材质编辑器 |
## 项目结构
### 平台
| 包名 | 描述 |
|------|------|
| `@esengine/platform-common` | 平台抽象接口 |
| `@esengine/platform-web` | Web 浏览器运行时 |
| `@esengine/platform-wechat` | 微信小游戏运行时 |
## 编辑器
ESEngine 编辑器是基于 Tauri 和 React 构建的跨平台桌面应用。
### 功能
- 场景层级和实体管理
- 组件检视器,支持自定义属性编辑器
- 资源浏览器,支持拖放
- Tilemap 编辑器,支持绘制和填充工具
- 行为树可视化编辑器
- 蓝图可视化脚本
- 材质和着色器编辑
- 内置性能分析器
- 多语言支持(英文、中文)
### 截图
![ESEngine Editor](screenshots/main_screetshot.png)
## 平台支持
| 平台 | 运行时 | 编辑器 |
|------|:------:|:------:|
| Web 浏览器 | ✓ | - |
| Windows | - | ✓ |
| macOS | - | ✓ |
| 微信小游戏 | 开发中 | - |
| Playable 可玩广告 | 计划中 | - |
| Android | 计划中 | - |
| iOS | 计划中 | - |
```
esengine/
├── packages/
│ ├── framework/ # 引擎无关模块(可发布到 NPM
│ │ ├── core/ # ECS 框架
│ │ ├── math/ # 数学工具
│ │ ├── behavior-tree/ # AI 行为树
│ │ ├── blueprint/ # 可视化脚本
│ │ ├── fsm/ # 有限状态机
│ │ ├── timer/ # 定时器系统
│ │ ├── spatial/ # 空间查询
│ │ ├── pathfinding/ # 寻路
│ │ ├── procgen/ # 程序化生成
│ │ └── network/ # 网络
│ │
│ ├── engine/ # ESEngine 运行时
│ ├── rendering/ # 渲染模块
│ ├── physics/ # 物理模块
│ ├── editor/ # 可视化编辑器
│ └── rust/ # WASM 渲染器
├── docs/ # 文档
└── examples/ # 示例
```
## 从源码构建
### 前置要求
- Node.js 18+
- pnpm 10+
- Rust 工具链(用于 WASM 渲染器)
- wasm-pack
### 安装
```bash
git clone https://github.com/esengine/esengine.git
cd esengine
@@ -223,43 +256,29 @@ cd esengine
pnpm install
pnpm build
# 可选:构建 WASM 渲染器
pnpm build:wasm
```
# 框架包类型检查
pnpm type-check:framework
### 运行编辑器
```bash
cd packages/editor-app
pnpm tauri:dev
```
### 项目结构
```
esengine/
├── packages/ # 引擎包(运行时、编辑器、平台)
├── docs/ # 文档源码
├── examples/ # 示例项目
├── scripts/ # 构建工具
└── thirdparty/ # 第三方依赖
# 运行测试
pnpm test
```
## 文档
- [快速入门](https://esengine.cn/guide/getting-started.html)
- [架构指南](https://esengine.cn/guide/)
- [ECS 框架指南](./packages/framework/core/README.md)
- [行为树指南](./packages/framework/behavior-tree/README.md)
- [API 参考](https://esengine.cn/api/README)
## 社区
- [QQ 交流群](https://jq.qq.com/?_wv=1027&k=29w1Nud6) - 中文社区
- [Discord](https://discord.gg/gCAgzXFW) - 国际社区
- [GitHub Issues](https://github.com/esengine/esengine/issues) - Bug 反馈和功能建议
- [GitHub Discussions](https://github.com/esengine/esengine/discussions) - 问题和想法
- [QQ 交流群](https://jq.qq.com/?_wv=1027&k=29w1Nud6) - 中文社区
## 贡献
欢迎贡献代码提交 PR 前请阅读贡献指南。
欢迎贡献代码提交 PR 前请阅读贡献指南。
1. Fork 仓库
2. 创建功能分支 (`git checkout -b feature/amazing-feature`)
@@ -269,10 +288,10 @@ esengine/
## 许可证
ESEngine 基于 [MIT 协议](LICENSE) 开源。
ESEngine 基于 [MIT 协议](LICENSE) 开源,个人和商业使用均免费
---
<p align="center">
由 ESEngine 团队用 ❤️ 打造
由 ESEngine 社区用心打造
</p>

View File

@@ -6,7 +6,7 @@ import { fileURLToPath } from 'url'
const __dirname = dirname(fileURLToPath(import.meta.url))
const corePackageJson = JSON.parse(
readFileSync(join(__dirname, '../../packages/core/package.json'), 'utf-8')
readFileSync(join(__dirname, '../../packages/framework/core/package.json'), 'utf-8')
)
// Import i18n messages

View File

@@ -4,6 +4,49 @@
---
## v2.4.2 (2025-12-25)
### Features
- **IncrementalSerializer 实体过滤**: 增量序列化支持 `entityFilter` 选项 (#335)
- 创建快照时可按条件过滤实体
- 支持按标签、组件类型等自定义过滤逻辑
- 适用于只同步部分实体的场景(如只同步玩家)
```typescript
// 只快照玩家实体
const snapshot = IncrementalSerializer.createSnapshot(scene, {
entityFilter: (entity) => entity.tag === PLAYER_TAG
});
// 只快照有特定组件的实体
const snapshot = IncrementalSerializer.createSnapshot(scene, {
entityFilter: (entity) => entity.hasComponent(PlayerMarker)
});
```
### Refactor
- 优化 `PlatformWorkerPool` 代码规范,提取为独立模块 (#335)
- 优化 `WorkerEntitySystem` 实现,改进代码结构 (#334)
- 代码规范化与依赖清理 (#317)
- 代码结构优化,添加 `GlobalTypes.ts` 统一类型定义 (#316)
---
## v2.4.1 (2025-12-23)
### Bug Fixes
- 修复 `IntervalSystem` 时间累加 bug间隔计时更加准确
- 修复 Cocos Creator 兼容性问题,类型导出更完整
### Documentation
- 新增 `Core.paused` 属性文档说明
---
## v2.4.0 (2025-12-15)
### Features

View File

@@ -4,6 +4,49 @@ This document records the version update history of the `@esengine/ecs-framework
---
## v2.4.2 (2025-12-25)
### Features
- **IncrementalSerializer Entity Filter**: Incremental serialization supports `entityFilter` option (#335)
- Filter entities by condition when creating snapshots
- Support custom filter logic by tag, component type, etc.
- Suitable for scenarios that only sync partial entities (e.g., only sync players)
```typescript
// Only snapshot player entities
const snapshot = IncrementalSerializer.createSnapshot(scene, {
entityFilter: (entity) => entity.tag === PLAYER_TAG
});
// Only snapshot entities with specific component
const snapshot = IncrementalSerializer.createSnapshot(scene, {
entityFilter: (entity) => entity.hasComponent(PlayerMarker)
});
```
### Refactor
- Optimize `PlatformWorkerPool` code style, extract as standalone module (#335)
- Optimize `WorkerEntitySystem` implementation, improve code structure (#334)
- Code standardization and dependency cleanup (#317)
- Code structure optimization, add `GlobalTypes.ts` for unified type definitions (#316)
---
## v2.4.1 (2025-12-23)
### Bug Fixes
- Fix `IntervalSystem` time accumulation bug, interval timing is now more accurate
- Fix Cocos Creator compatibility issue, more complete type exports
### Documentation
- Add `Core.paused` property documentation
---
## v2.4.0 (2025-12-15)
### Features

View File

@@ -4,7 +4,24 @@ This guide will help you get started with ECS Framework, from installation to cr
## Installation
### NPM Installation
### Using CLI (Recommended)
The easiest way to add ECS to your existing project:
```bash
# In your project directory
npx @esengine/cli init
```
The CLI automatically detects your project type (Cocos Creator 2.x/3.x, LayaAir 3.x, or Node.js) and generates the necessary integration code, including:
- `ECSManager` component/script - Manages ECS lifecycle
- Example components and systems - Helps you get started quickly
- Automatic dependency installation
### Manual NPM Installation
If you prefer manual configuration:
```bash
# Using npm
@@ -333,27 +350,39 @@ function gameLoop(deltaTime: number) {
## Game Engine Integration
### Laya Engine Integration
### Laya 3.x Engine Integration
Using `Laya.Script` component to manage ECS lifecycle is recommended:
```typescript
import { Stage } from "laya/display/Stage";
import { Laya } from "Laya";
import { Core } from '@esengine/ecs-framework';
import { Core, Scene } from '@esengine/ecs-framework';
// Initialize Laya
Laya.init(800, 600).then(() => {
// Initialize ECS
Core.create(true);
Core.setScene(new GameScene());
const { regClass } = Laya;
// Start game loop
Laya.timer.frameLoop(1, this, () => {
const deltaTime = Laya.timer.delta / 1000;
Core.update(deltaTime); // Auto-updates global services and scene
});
});
@regClass()
export class ECSManager extends Laya.Script {
private ecsScene = new GameScene();
onAwake(): void {
// Initialize ECS
Core.create({ debug: true });
Core.setScene(this.ecsScene);
}
onUpdate(): void {
// Auto-updates global services and scene
Core.update(Laya.timer.delta / 1000);
}
onDestroy(): void {
// Cleanup resources
Core.destroy();
}
}
```
In Laya IDE, attach the `ECSManager` script to a node in your scene.
### Cocos Creator Integration
```typescript

View File

@@ -4,7 +4,24 @@
## 安装
### NPM 安装
### 使用 CLI推荐
在现有项目中添加 ECS 的最简单方式:
```bash
# 在项目目录中运行
npx @esengine/cli init
```
CLI 会自动检测项目类型Cocos Creator 2.x/3.x、LayaAir 3.x 或 Node.js并生成相应的集成代码包括
- `ECSManager` 组件/脚本 - 负责 ECS 生命周期管理
- 示例组件和系统 - 帮助快速上手
- 自动安装依赖
### NPM 手动安装
如果你更喜欢手动配置,可以直接安装:
```bash
# 使用 npm
@@ -333,27 +350,39 @@ function gameLoop(deltaTime: number) {
## 与游戏引擎集成
### Laya 引擎集成
### Laya 3.x 引擎集成
推荐使用 `Laya.Script` 组件来管理 ECS 生命周期:
```typescript
import { Stage } from "laya/display/Stage";
import { Laya } from "Laya";
import { Core } from '@esengine/ecs-framework';
import { Core, Scene } from '@esengine/ecs-framework';
// 初始化 Laya
Laya.init(800, 600).then(() => {
// 初始化 ECS
Core.create(true);
Core.setScene(new GameScene());
const { regClass } = Laya;
// 启动游戏循环
Laya.timer.frameLoop(1, this, () => {
const deltaTime = Laya.timer.delta / 1000;
Core.update(deltaTime); // 自动更新全局服务和场景
});
});
@regClass()
export class ECSManager extends Laya.Script {
private ecsScene = new GameScene();
onAwake(): void {
// 初始化 ECS
Core.create({ debug: true });
Core.setScene(this.ecsScene);
}
onUpdate(): void {
// 自动更新全局服务和场景
Core.update(Laya.timer.delta / 1000);
}
onDestroy(): void {
// 清理资源
Core.destroy();
}
}
```
在 Laya IDE 中,将 `ECSManager` 脚本挂载到场景中的节点上即可。
### Cocos Creator 集成
```typescript

View File

@@ -5,7 +5,16 @@
"private": true,
"packageManager": "pnpm@10.22.0",
"workspaces": [
"packages/*"
"packages/framework/*",
"packages/engine/*",
"packages/rendering/*",
"packages/physics/*",
"packages/streaming/*",
"packages/network-ext/*",
"packages/editor/*",
"packages/editor/plugins/*",
"packages/rust/*",
"packages/tools/*"
],
"keywords": [
"ecs",
@@ -17,6 +26,9 @@
"egret"
],
"scripts": {
"changeset": "changeset",
"changeset:version": "changeset version",
"changeset:publish": "changeset publish",
"bootstrap": "lerna bootstrap",
"clean": "turbo run clean",
"build": "turbo run build",
@@ -25,23 +37,24 @@
"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",
"build:npm:core": "cd packages/framework/core && npm run build:npm",
"build:npm:math": "cd packages/framework/math && npm run build:npm",
"test": "turbo run test",
"test:coverage": "turbo run test:coverage",
"test:ci": "turbo run test:ci",
"test:ci:framework": "turbo run test:ci --filter=@esengine/ecs-framework --filter=@esengine/ecs-framework-math --filter=@esengine/behavior-tree --filter=@esengine/blueprint --filter=@esengine/fsm --filter=@esengine/timer --filter=@esengine/spatial --filter=@esengine/procgen --filter=@esengine/pathfinding --filter=@esengine/network-protocols --filter=@esengine/network",
"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",
"publish:all:dist": "npm run publish:core && npm run publish:math",
"publish:core": "cd packages/core && npm run publish:npm",
"publish:core:patch": "cd packages/core && npm run publish:patch",
"publish:math": "cd packages/math && npm run publish:npm",
"publish:math:patch": "cd packages/math && npm run publish:patch",
"publish:core": "cd packages/framework/core && npm run publish:npm",
"publish:core:patch": "cd packages/framework/core && npm run publish:patch",
"publish:math": "cd packages/framework/math && npm run publish:npm",
"publish:math:patch": "cd packages/framework/math && npm run publish:patch",
"publish": "lerna publish",
"version": "lerna version",
"release": "semantic-release",
"release:core": "cd packages/core && semantic-release",
"release:core": "cd packages/framework/core && semantic-release",
"contributors:add": "all-contributors add",
"contributors:generate": "all-contributors generate",
"contributors:check": "all-contributors check",
@@ -55,15 +68,19 @@
"format": "prettier --write \"packages/**/src/**/*.{ts,tsx,js,jsx}\"",
"format:check": "prettier --check \"packages/**/src/**/*.{ts,tsx,js,jsx}\"",
"type-check": "turbo run type-check",
"type-check:framework": "turbo run type-check --filter=@esengine/ecs-framework --filter=@esengine/ecs-framework-math --filter=@esengine/behavior-tree --filter=@esengine/blueprint --filter=@esengine/fsm --filter=@esengine/timer --filter=@esengine/spatial --filter=@esengine/procgen --filter=@esengine/pathfinding --filter=@esengine/network-protocols --filter=@esengine/network",
"lint": "turbo run lint",
"lint:framework": "turbo run lint --filter=@esengine/ecs-framework --filter=@esengine/ecs-framework-math --filter=@esengine/behavior-tree --filter=@esengine/blueprint --filter=@esengine/fsm --filter=@esengine/timer --filter=@esengine/spatial --filter=@esengine/procgen --filter=@esengine/pathfinding --filter=@esengine/network-protocols --filter=@esengine/network",
"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",
"build:wasm": "cd packages/rust/engine && wasm-pack build --dev --out-dir pkg",
"build:wasm:release": "cd packages/rust/engine && wasm-pack build --release --out-dir pkg",
"copy-modules": "node scripts/copy-engine-modules.mjs"
},
"author": "yhh",
"license": "MIT",
"devDependencies": {
"@changesets/changelog-github": "^0.5.2",
"@changesets/cli": "^2.29.8",
"@commitlint/cli": "^18.6.0",
"@commitlint/config-conventional": "^18.6.0",
"@eslint/js": "^9.39.1",
@@ -121,4 +138,3 @@
"ws": "^8.18.2"
}
}

View File

@@ -1,46 +0,0 @@
{
"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"
}

View File

@@ -1,13 +0,0 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"composite": true,
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"],
"references": [
{ "path": "../core" }
]
}

View File

@@ -1,49 +0,0 @@
{
"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"
}

View File

@@ -1,17 +0,0 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"composite": true,
"outDir": "./dist",
"rootDir": "./src",
"declaration": true,
"jsx": "react-jsx"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"],
"references": [
{ "path": "../core" },
{ "path": "../editor-core" },
{ "path": "../behavior-tree" }
]
}

View File

@@ -1,41 +0,0 @@
/**
* @esengine/behavior-tree
*
* AI Behavior Tree System with runtime execution and visual editor support
* AI 行为树系统,支持运行时执行和可视化编辑
*
* @packageDocumentation
*/
// Constants
export { BehaviorTreeAssetType } from './constants';
// 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 and plugin
export { BehaviorTreeRuntimeModule, BehaviorTreePlugin } from './BehaviorTreeRuntimeModule';
// Service tokens | 服务令牌
export { BehaviorTreeSystemToken } from './tokens';

View File

@@ -1,49 +0,0 @@
{
"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"
}

View File

@@ -1,34 +0,0 @@
/**
* @esengine/blueprint - Visual scripting system for ECS Framework
* 蓝图可视化脚本系统
*/
// Types
export * from './types';
// Runtime
export * from './runtime';
// Nodes (import to register)
import './nodes';
// Re-export commonly used items
export { NodeRegistry, RegisterNode } from './runtime/NodeRegistry';
export { BlueprintVM } from './runtime/BlueprintVM';
export {
createBlueprintComponentData,
initializeBlueprintVM,
startBlueprint,
stopBlueprint,
tickBlueprint,
cleanupBlueprint
} from './runtime/BlueprintComponent';
export {
createBlueprintSystem,
triggerBlueprintEvent,
triggerCustomBlueprintEvent
} from './runtime/BlueprintSystem';
export { createEmptyBlueprint, validateBlueprintAsset } from './types/blueprint';
// Plugin
export { BlueprintPlugin } from './BlueprintPlugin';

View File

@@ -1,8 +0,0 @@
/**
* Event Nodes - Entry points for blueprint execution
* 事件节点 - 蓝图执行的入口点
*/
export * from './EventBeginPlay';
export * from './EventTick';
export * from './EventEndPlay';

View File

@@ -1,39 +0,0 @@
{
"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
}
}
}

View File

@@ -1,16 +0,0 @@
{
"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" }
]
}

View File

@@ -1,13 +0,0 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"composite": true,
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"],
"references": [
{ "path": "../core" }
]
}

View File

@@ -1,7 +0,0 @@
import { defineConfig } from 'tsup';
import { runtimeOnlyPreset } from '../build-config/src/presets/plugin-tsup';
export default defineConfig({
...runtimeOnlyPreset(),
tsconfig: 'tsconfig.build.json'
});

View File

@@ -1,161 +0,0 @@
import type { IComponent } from '../Types';
import { Int32 } from './Core/SoAStorage';
/**
* 游戏组件基类
*
* ECS架构中的组件Component应该是纯数据容器。
* 所有游戏逻辑应该在 EntitySystem 中实现,而不是在组件内部。
*
* @example
* 推荐做法:纯数据组件
* ```typescript
* class HealthComponent extends Component {
* public health: number = 100;
* public maxHealth: number = 100;
* }
* ```
*
* @example
* 推荐做法:在 System 中处理逻辑
* ```typescript
* class HealthSystem extends EntitySystem {
* process(entities: Entity[]): void {
* for (const entity of entities) {
* const health = entity.getComponent(HealthComponent);
* if (health && health.health <= 0) {
* entity.destroy();
* }
* }
* }
* }
* ```
*/
export abstract class Component implements IComponent {
/**
* 组件ID生成器
*
* 用于为每个组件分配唯一的ID。
*
* Component ID generator.
* Used to assign unique IDs to each component.
*/
private static idGenerator: number = 0;
/**
* 组件唯一标识符
*
* 在整个游戏生命周期中唯一的数字ID。
*/
public readonly id: number;
/**
* 所属实体ID
*
* 存储实体ID而非引用避免循环引用符合ECS数据导向设计。
*/
@Int32
public entityId: number | null = null;
/**
* 最后写入的 epoch
*
* 用于帧级变更检测,记录组件最后一次被修改时的 epoch。
* 0 表示从未被标记为已修改。
*
* Last write epoch.
* Used for frame-level change detection, records the epoch when component was last modified.
* 0 means never marked as modified.
*/
private _lastWriteEpoch: number = 0;
/**
* 获取最后写入的 epoch
*
* Get last write epoch.
*/
public get lastWriteEpoch(): number {
return this._lastWriteEpoch;
}
/**
* 创建组件实例
*
* 自动分配唯一ID给组件。
*/
constructor() {
this.id = Component.idGenerator++;
}
/**
* 标记组件为已修改
*
* 调用此方法会更新组件的 lastWriteEpoch 为当前帧的 epoch。
* 系统可以通过比较 lastWriteEpoch 和上次检查的 epoch 来判断组件是否发生变更。
*
* Mark component as modified.
* Calling this method updates the component's lastWriteEpoch to the current frame's epoch.
* Systems can compare lastWriteEpoch with their last checked epoch to detect changes.
*
* @param epoch 当前帧的 epoch | Current frame's epoch
*
* @example
* ```typescript
* // 在修改组件数据后调用
* velocity.x = 10;
* velocity.markDirty(scene.epochManager.current);
* ```
*/
public markDirty(epoch: number): void {
this._lastWriteEpoch = epoch;
}
/**
* 组件添加到实体时的回调
*
* 当组件被添加到实体时调用,可以在此方法中进行初始化操作。
*
* @remarks
* 这是一个生命周期钩子,用于组件的初始化逻辑。
* 虽然保留此方法,但建议将复杂的初始化逻辑放在 System 中处理。
*/
public onAddedToEntity(): void {}
/**
* 组件从实体移除时的回调
*
* 当组件从实体中移除时调用,可以在此方法中进行清理操作。
*
* @remarks
* 这是一个生命周期钩子,用于组件的清理逻辑。
* 虽然保留此方法,但建议将复杂的清理逻辑放在 System 中处理。
*/
public onRemovedFromEntity(): void {}
/**
* 组件反序列化后的回调
*
* 当组件从场景文件加载或快照恢复后调用,可以在此方法中恢复运行时数据。
*
* @remarks
* 这是一个生命周期钩子,用于恢复无法序列化的运行时数据。
* 例如:从图片路径重新加载图片尺寸信息,重建缓存等。
*
* @example
* ```typescript
* class TilemapComponent extends Component {
* public tilesetImage: string = '';
* private _tilesetData: TilesetData | undefined;
*
* public async onDeserialized(): Promise<void> {
* if (this.tilesetImage) {
* // 重新加载 tileset 图片并恢复运行时数据
* const img = await loadImage(this.tilesetImage);
* this.setTilesetInfo(img.width, img.height, ...);
* }
* }
* }
* ```
*/
public onDeserialized(): void | Promise<void> {}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,14 +0,0 @@
// ECS系统导出
export { EntitySystem } from './EntitySystem';
export { ProcessingSystem } from './ProcessingSystem';
export { PassiveSystem } from './PassiveSystem';
export { IntervalSystem } from './IntervalSystem';
export { WorkerEntitySystem } from './WorkerEntitySystem';
export { HierarchySystem } from './HierarchySystem';
// Worker系统相关类型导出
export type {
WorkerProcessFunction,
WorkerSystemConfig,
SharedArrayBufferProcessFunction
} from './WorkerEntitySystem';

View File

@@ -1,137 +0,0 @@
import type { IPlatformAdapter } from './IPlatformAdapter';
import { createLogger, type ILogger } from '../Utils/Logger';
/**
* 平台管理器
* 用户需要手动注册平台适配器
*/
export class PlatformManager {
private static instance: PlatformManager;
private adapter: IPlatformAdapter | null = null;
private readonly logger: ILogger;
private constructor() {
this.logger = createLogger('PlatformManager');
}
/**
* 获取单例实例
*/
public static getInstance(): PlatformManager {
if (!PlatformManager.instance) {
PlatformManager.instance = new PlatformManager();
}
return PlatformManager.instance;
}
/**
* 获取当前平台适配器
*/
public getAdapter(): IPlatformAdapter {
if (!this.adapter) {
throw new Error('平台适配器未注册,请调用 registerAdapter() 注册适配器');
}
return this.adapter;
}
/**
* 注册平台适配器
*/
public registerAdapter(adapter: IPlatformAdapter): void {
this.adapter = adapter;
this.logger.info(`平台适配器已注册: ${adapter.name}`, {
name: adapter.name,
version: adapter.version,
supportsWorker: adapter.isWorkerSupported(),
supportsSharedArrayBuffer: adapter.isSharedArrayBufferSupported(),
hardwareConcurrency: adapter.getHardwareConcurrency()
});
}
/**
* 检查是否已注册适配器
*/
public hasAdapter(): boolean {
return this.adapter !== null;
}
/**
* 获取平台适配器信息(用于调试)
*/
public getAdapterInfo(): any {
return this.adapter ? {
name: this.adapter.name,
version: this.adapter.version,
config: this.adapter.getPlatformConfig()
} : null;
}
/**
* 检查当前平台是否支持特定功能
*/
public supportsFeature(feature: 'worker' | 'shared-array-buffer' | 'transferable-objects' | 'module-worker'): boolean {
if (!this.adapter) return false;
const config = this.adapter.getPlatformConfig();
switch (feature) {
case 'worker':
return this.adapter.isWorkerSupported();
case 'shared-array-buffer':
return this.adapter.isSharedArrayBufferSupported();
case 'transferable-objects':
return config.supportsTransferableObjects;
case 'module-worker':
return config.supportsModuleWorker;
default:
return false;
}
}
/**
* 获取基础的Worker配置信息不做自动决策
* 用户应该根据自己的业务需求来配置Worker参数
*/
public getBasicWorkerConfig(): {
platformSupportsWorker: boolean;
platformSupportsSharedArrayBuffer: boolean;
platformMaxWorkerCount: number;
platformLimitations: any;
} {
if (!this.adapter) {
return {
platformSupportsWorker: false,
platformSupportsSharedArrayBuffer: false,
platformMaxWorkerCount: 1,
platformLimitations: {}
};
}
const config = this.adapter.getPlatformConfig();
return {
platformSupportsWorker: this.adapter.isWorkerSupported(),
platformSupportsSharedArrayBuffer: this.adapter.isSharedArrayBufferSupported(),
platformMaxWorkerCount: config.maxWorkerCount,
platformLimitations: config.limitations || {}
};
}
/**
* 异步获取完整的平台配置信息(包含性能信息)
*/
public async getFullPlatformConfig(): Promise<any> {
if (!this.adapter) {
throw new Error('平台适配器未注册');
}
// 如果适配器支持异步获取配置,使用异步方法
if (typeof this.adapter.getPlatformConfigAsync === 'function') {
return await this.adapter.getPlatformConfigAsync();
}
// 否则返回同步配置
return this.adapter.getPlatformConfig();
}
}

View File

@@ -1,52 +0,0 @@
{
"name": "@esengine/ecs-engine-bindgen",
"version": "0.1.0",
"description": "Bridge layer between ECS Framework and Rust Engine | ECS框架与Rust引擎之间的桥接层",
"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",
"clean": "rimraf dist"
},
"repository": {
"type": "git",
"url": "https://github.com/esengine/esengine.git",
"directory": "packages/ecs-engine-bindgen"
},
"keywords": [
"ecs",
"game-engine",
"bridge",
"wasm",
"typescript"
],
"author": "ESEngine Team",
"license": "MIT",
"optionalDependencies": {
"es-engine": "file:../engine/pkg"
},
"devDependencies": {
"@esengine/ecs-framework": "workspace:*",
"@esengine/ecs-framework-math": "workspace:*",
"@esengine/engine-core": "workspace:*",
"@esengine/sprite": "workspace:*",
"@esengine/camera": "workspace:*",
"@esengine/asset-system": "workspace:*",
"@esengine/material-system": "workspace:*",
"tsup": "^8.5.1",
"typescript": "^5.8.0",
"rimraf": "^5.0.0"
}
}

View File

@@ -1,88 +0,0 @@
import { create } from 'zustand';
/**
* 编辑器交互状态
* 管理编辑器的交互状态(连接、框选、菜单等)
*/
interface EditorState {
/**
* 正在连接的源节点ID
*/
connectingFrom: string | null;
/**
* 正在连接的源属性
*/
connectingFromProperty: string | null;
/**
* 连接目标位置(鼠标位置)
*/
connectingToPos: { x: number; y: number } | null;
/**
* 是否正在框选
*/
isBoxSelecting: boolean;
/**
* 框选起始位置
*/
boxSelectStart: { x: number; y: number } | null;
/**
* 框选结束位置
*/
boxSelectEnd: { x: number; y: number } | null;
// Actions
setConnectingFrom: (nodeId: string | null) => void;
setConnectingFromProperty: (propertyName: string | null) => void;
setConnectingToPos: (pos: { x: number; y: number } | null) => void;
clearConnecting: () => void;
setIsBoxSelecting: (isSelecting: boolean) => void;
setBoxSelectStart: (pos: { x: number; y: number } | null) => void;
setBoxSelectEnd: (pos: { x: number; y: number } | null) => void;
clearBoxSelect: () => void;
}
/**
* Editor Store
*/
export const useEditorStore = create<EditorState>((set) => ({
connectingFrom: null,
connectingFromProperty: null,
connectingToPos: null,
isBoxSelecting: false,
boxSelectStart: null,
boxSelectEnd: null,
setConnectingFrom: (nodeId: string | null) => set({ connectingFrom: nodeId }),
setConnectingFromProperty: (propertyName: string | null) =>
set({ connectingFromProperty: propertyName }),
setConnectingToPos: (pos: { x: number; y: number } | null) => set({ connectingToPos: pos }),
clearConnecting: () =>
set({
connectingFrom: null,
connectingFromProperty: null,
connectingToPos: null
}),
setIsBoxSelecting: (isSelecting: boolean) => set({ isBoxSelecting: isSelecting }),
setBoxSelectStart: (pos: { x: number; y: number } | null) => set({ boxSelectStart: pos }),
setBoxSelectEnd: (pos: { x: number; y: number } | null) => set({ boxSelectEnd: pos }),
clearBoxSelect: () =>
set({
isBoxSelecting: false,
boxSelectStart: null,
boxSelectEnd: null
})
}));

View File

@@ -1,131 +0,0 @@
import { create } from 'zustand';
/**
* UI 状态
* 管理UI相关的状态选中、拖拽、画布
*/
interface UIState {
/**
* 选中的节点ID列表
*/
selectedNodeIds: string[];
/**
* 正在拖拽的节点ID
*/
draggingNodeId: string | null;
/**
* 拖拽起始位置映射
*/
dragStartPositions: Map<string, { x: number; y: number }>;
/**
* 是否正在拖拽节点
*/
isDraggingNode: boolean;
/**
* 拖拽偏移量
*/
dragDelta: { dx: number; dy: number };
/**
* 画布偏移
*/
canvasOffset: { x: number; y: number };
/**
* 画布缩放
*/
canvasScale: number;
/**
* 是否正在平移画布
*/
isPanning: boolean;
/**
* 平移起始位置
*/
panStart: { x: number; y: number };
// Actions
setSelectedNodeIds: (nodeIds: string[]) => void;
toggleNodeSelection: (nodeId: string) => void;
clearSelection: () => void;
startDragging: (nodeId: string, startPositions: Map<string, { x: number; y: number }>) => void;
stopDragging: () => void;
setIsDraggingNode: (isDragging: boolean) => void;
setDragDelta: (delta: { dx: number; dy: number }) => void;
setCanvasOffset: (offset: { x: number; y: number }) => void;
setCanvasScale: (scale: number) => void;
setIsPanning: (isPanning: boolean) => void;
setPanStart: (panStart: { x: number; y: number }) => void;
resetView: () => void;
}
/**
* UI Store
*/
export const useUIStore = create<UIState>((set, get) => ({
selectedNodeIds: [],
draggingNodeId: null,
dragStartPositions: new Map(),
isDraggingNode: false,
dragDelta: { dx: 0, dy: 0 },
canvasOffset: { x: 0, y: 0 },
canvasScale: 1,
isPanning: false,
panStart: { x: 0, y: 0 },
setSelectedNodeIds: (nodeIds: string[]) => set({ selectedNodeIds: nodeIds }),
toggleNodeSelection: (nodeId: string) => {
const { selectedNodeIds } = get();
if (selectedNodeIds.includes(nodeId)) {
set({ selectedNodeIds: selectedNodeIds.filter((id) => id !== nodeId) });
} else {
set({ selectedNodeIds: [...selectedNodeIds, nodeId] });
}
},
clearSelection: () => set({ selectedNodeIds: [] }),
startDragging: (nodeId: string, startPositions: Map<string, { x: number; y: number }>) =>
set({
draggingNodeId: nodeId,
dragStartPositions: startPositions,
isDraggingNode: true
}),
stopDragging: () =>
set({
draggingNodeId: null,
dragStartPositions: new Map(),
isDraggingNode: false,
dragDelta: { dx: 0, dy: 0 }
}),
setIsDraggingNode: (isDragging: boolean) => set({ isDraggingNode: isDragging }),
setDragDelta: (delta: { dx: number; dy: number }) => set({ dragDelta: delta }),
setCanvasOffset: (offset: { x: number; y: number }) => set({ canvasOffset: offset }),
setCanvasScale: (scale: number) => set({ canvasScale: scale }),
setIsPanning: (isPanning: boolean) => set({ isPanning }),
setPanStart: (panStart: { x: number; y: number }) => set({ panStart }),
resetView: () =>
set({
canvasOffset: { x: 0, y: 0 },
canvasScale: 1,
isPanning: false
})
}));

View File

@@ -1,2 +0,0 @@
export { useUIStore } from './UIStore';
export { useEditorStore } from './EditorStore';

View File

@@ -1,22 +0,0 @@
/**
* Asset Browser - 资产浏览器
* 包装 ContentBrowser 组件,保持向后兼容
*/
import { ContentBrowser } from './ContentBrowser';
interface AssetBrowserProps {
projectPath: string | null;
locale: string;
onOpenScene?: (scenePath: string) => void;
}
export function AssetBrowser({ projectPath, locale, onOpenScene }: AssetBrowserProps) {
return (
<ContentBrowser
projectPath={projectPath}
locale={locale}
onOpenScene={onOpenScene}
/>
);
}

View File

@@ -1,141 +0,0 @@
import { useState, useEffect } from 'react';
import { RefreshCw, Folder } from 'lucide-react';
import { TauriAPI } from '../api/tauri';
interface AssetPickerProps {
value: string;
onChange: (value: string) => void;
projectPath: string | null;
filter?: 'btree' | 'ecs';
label?: string;
}
/**
* 资产选择器组件
* 用于选择项目中的资产文件
*/
export function AssetPicker({ value, onChange, projectPath, filter = 'btree', label }: AssetPickerProps) {
const [assets, setAssets] = useState<string[]>([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
if (projectPath) {
loadAssets();
}
}, [projectPath]);
const loadAssets = async () => {
if (!projectPath) return;
setLoading(true);
try {
if (filter === 'btree') {
const btrees = await TauriAPI.scanBehaviorTrees(projectPath);
setAssets(btrees);
}
} catch (error) {
console.error('Failed to load assets:', error);
setAssets([]);
} finally {
setLoading(false);
}
};
const handleBrowse = async () => {
try {
if (filter === 'btree') {
const path = await TauriAPI.openBehaviorTreeDialog();
if (path && projectPath) {
const behaviorsPath = `${projectPath}\\.ecs\\behaviors\\`.replace(/\\/g, '\\\\');
const relativePath = path.replace(behaviorsPath, '')
.replace(/\\/g, '/')
.replace('.btree', '');
onChange(relativePath);
await loadAssets();
}
}
} catch (error) {
console.error('Failed to browse asset:', error);
}
};
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
{label && (
<label style={{ fontSize: '11px', color: '#aaa', fontWeight: '500' }}>
{label}
</label>
)}
<div style={{ display: 'flex', gap: '4px', alignItems: 'center' }}>
<select
value={value}
onChange={(e) => onChange(e.target.value)}
disabled={loading || !projectPath}
style={{
flex: 1,
padding: '4px 8px',
backgroundColor: '#1e1e1e',
border: '1px solid #3e3e42',
borderRadius: '3px',
color: '#cccccc',
fontSize: '12px',
cursor: loading || !projectPath ? 'not-allowed' : 'pointer'
}}
>
<option value="">{loading ? '加载中...' : '选择资产...'}</option>
{assets.map((asset) => (
<option key={asset} value={asset}>
{asset}
</option>
))}
</select>
<button
onClick={loadAssets}
disabled={loading || !projectPath}
style={{
padding: '4px 8px',
backgroundColor: '#0e639c',
border: 'none',
borderRadius: '3px',
color: '#fff',
cursor: loading || !projectPath ? 'not-allowed' : 'pointer',
display: 'flex',
alignItems: 'center',
opacity: loading || !projectPath ? 0.5 : 1
}}
title="刷新资产列表"
>
<RefreshCw size={14} />
</button>
<button
onClick={handleBrowse}
disabled={loading || !projectPath}
style={{
padding: '4px 8px',
backgroundColor: '#0e639c',
border: 'none',
borderRadius: '3px',
color: '#fff',
cursor: loading || !projectPath ? 'not-allowed' : 'pointer',
display: 'flex',
alignItems: 'center',
opacity: loading || !projectPath ? 0.5 : 1
}}
title="浏览文件..."
>
<Folder size={14} />
</button>
</div>
{!projectPath && (
<div style={{ fontSize: '10px', color: '#ff6b6b', marginTop: '2px' }}>
</div>
)}
{value && assets.length > 0 && !assets.includes(value) && (
<div style={{ fontSize: '10px', color: '#ffa726', marginTop: '2px' }}>
警告: 资产 "{value}"
</div>
)}
</div>
);
}

View File

@@ -1,316 +0,0 @@
import { useState, useEffect } from 'react';
import { X, Folder, Search, ArrowLeft, Grid, List, FileCode } from 'lucide-react';
import { TauriAPI, DirectoryEntry } from '../api/tauri';
import { useLocale } from '../hooks/useLocale';
import '../styles/AssetPickerDialog.css';
interface AssetPickerDialogProps {
projectPath: string;
fileExtension: string;
onSelect: (assetId: string) => void;
onClose: () => void;
locale: string;
/** 资产基础路径(相对于项目根目录),用于计算 assetId */
assetBasePath?: string;
}
interface AssetItem {
name: string;
path: string;
isDir: boolean;
extension?: string;
size?: number;
modified?: number;
}
type ViewMode = 'list' | 'grid';
export function AssetPickerDialog({ projectPath, fileExtension, onSelect, onClose, locale, assetBasePath }: AssetPickerDialogProps) {
const { t, locale: currentLocale } = useLocale();
// 计算实际的资产目录路径
const actualAssetPath = assetBasePath
? `${projectPath}/${assetBasePath}`.replace(/\\/g, '/').replace(/\/+/g, '/')
: projectPath;
const [currentPath, setCurrentPath] = useState(actualAssetPath);
const [assets, setAssets] = useState<AssetItem[]>([]);
const [selectedPath, setSelectedPath] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const [searchQuery, setSearchQuery] = useState('');
const [viewMode, setViewMode] = useState<ViewMode>('list');
useEffect(() => {
loadAssets(currentPath);
}, [currentPath]);
const loadAssets = async (path: string) => {
setLoading(true);
try {
const entries = await TauriAPI.listDirectory(path);
const assetItems: AssetItem[] = entries
.map((entry: DirectoryEntry) => {
const extension = entry.is_dir ? undefined :
(entry.name.includes('.') ? entry.name.split('.').pop() : undefined);
return {
name: entry.name,
path: entry.path,
isDir: entry.is_dir,
extension,
size: entry.size,
modified: entry.modified
};
})
.filter((item) => item.isDir || item.extension === fileExtension)
.sort((a, b) => {
if (a.isDir === b.isDir) return a.name.localeCompare(b.name);
return a.isDir ? -1 : 1;
});
setAssets(assetItems);
} catch (error) {
console.error('Failed to load assets:', error);
setAssets([]);
} finally {
setLoading(false);
}
};
// 过滤搜索结果
const filteredAssets = assets.filter((item) =>
item.name.toLowerCase().includes(searchQuery.toLowerCase())
);
// 格式化文件大小
const formatFileSize = (bytes?: number): string => {
if (!bytes) return '';
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
};
// 格式化修改时间
const formatDate = (timestamp?: number): string => {
if (!timestamp) return '';
const date = new Date(timestamp * 1000);
const localeMap: Record<string, string> = { zh: 'zh-CN', en: 'en-US', es: 'es-ES' };
return date.toLocaleDateString(localeMap[currentLocale] || 'en-US', {
year: 'numeric',
month: 'short',
day: 'numeric'
});
};
// 返回上级目录
const handleGoBack = () => {
const parentPath = currentPath.split(/[/\\]/).slice(0, -1).join('/');
const minPath = actualAssetPath.replace(/[/\\]$/, '');
if (parentPath && parentPath !== minPath) {
setCurrentPath(parentPath);
} else if (currentPath !== actualAssetPath) {
setCurrentPath(actualAssetPath);
}
};
// 只能返回到资产基础目录,不能再往上
const canGoBack = currentPath !== actualAssetPath;
const handleItemClick = (item: AssetItem) => {
if (item.isDir) {
setCurrentPath(item.path);
} else {
setSelectedPath(item.path);
}
};
const handleItemDoubleClick = (item: AssetItem) => {
if (!item.isDir) {
const assetId = calculateAssetId(item.path);
onSelect(assetId);
}
};
const handleSelect = () => {
if (selectedPath) {
const assetId = calculateAssetId(selectedPath);
onSelect(assetId);
}
};
/**
* 计算资产ID
* 将绝对路径转换为相对于资产基础目录的assetId不含扩展名
*/
const calculateAssetId = (absolutePath: string): string => {
const normalized = absolutePath.replace(/\\/g, '/');
const baseNormalized = actualAssetPath.replace(/\\/g, '/');
// 获取相对于资产基础目录的路径
let relativePath = normalized;
if (normalized.startsWith(baseNormalized)) {
relativePath = normalized.substring(baseNormalized.length);
}
// 移除开头的斜杠
relativePath = relativePath.replace(/^\/+/, '');
// 移除文件扩展名
const assetId = relativePath.replace(new RegExp(`\\.${fileExtension}$`), '');
return assetId;
};
const getBreadcrumbs = () => {
const basePathNormalized = actualAssetPath.replace(/\\/g, '/');
const currentPathNormalized = currentPath.replace(/\\/g, '/');
const relative = currentPathNormalized.replace(basePathNormalized, '');
const parts = relative.split('/').filter((p) => p);
// 根路径名称(显示"行为树"或"Assets"
const rootName = assetBasePath
? assetBasePath.split('/').pop() || 'Assets'
: 'Content';
const crumbs = [{ name: rootName, path: actualAssetPath }];
let accPath = actualAssetPath;
for (const part of parts) {
accPath = `${accPath}/${part}`;
crumbs.push({ name: part, path: accPath });
}
return crumbs;
};
const breadcrumbs = getBreadcrumbs();
return (
<div className="asset-picker-overlay" onClick={onClose}>
<div className="asset-picker-dialog" onClick={(e) => e.stopPropagation()}>
<div className="asset-picker-header">
<h3>{t('assetPicker.title')}</h3>
<button className="asset-picker-close" onClick={onClose}>
<X size={18} />
</button>
</div>
<div className="asset-picker-toolbar">
<button
className="toolbar-button"
onClick={handleGoBack}
disabled={!canGoBack}
title={t('assetPicker.back')}
>
<ArrowLeft size={16} />
</button>
<div className="asset-picker-breadcrumb">
{breadcrumbs.map((crumb, index) => (
<span key={crumb.path}>
<span
className="breadcrumb-item"
onClick={() => setCurrentPath(crumb.path)}
>
{crumb.name}
</span>
{index < breadcrumbs.length - 1 && <span className="breadcrumb-separator"> / </span>}
</span>
))}
</div>
<div className="view-mode-buttons">
<button
className={`toolbar-button ${viewMode === 'list' ? 'active' : ''}`}
onClick={() => setViewMode('list')}
title={t('assetPicker.listView')}
>
<List size={16} />
</button>
<button
className={`toolbar-button ${viewMode === 'grid' ? 'active' : ''}`}
onClick={() => setViewMode('grid')}
title={t('assetPicker.gridView')}
>
<Grid size={16} />
</button>
</div>
</div>
<div className="asset-picker-search">
<Search size={16} className="search-icon" />
<input
type="text"
placeholder={t('assetPicker.search')}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="search-input"
/>
{searchQuery && (
<button
className="search-clear"
onClick={() => setSearchQuery('')}
>
<X size={14} />
</button>
)}
</div>
<div className="asset-picker-content">
{loading ? (
<div className="asset-picker-loading">{t('assetPicker.loading')}</div>
) : filteredAssets.length === 0 ? (
<div className="asset-picker-empty">{t('assetPicker.empty')}</div>
) : (
<div className={`asset-picker-list ${viewMode}`}>
{filteredAssets.map((item, index) => (
<div
key={index}
className={`asset-picker-item ${selectedPath === item.path ? 'selected' : ''}`}
onClick={() => handleItemClick(item)}
onDoubleClick={() => handleItemDoubleClick(item)}
>
<div className="asset-icon">
{item.isDir ? (
<Folder size={viewMode === 'grid' ? 32 : 18} style={{ color: '#ffa726' }} />
) : (
<FileCode size={viewMode === 'grid' ? 32 : 18} style={{ color: '#66bb6a' }} />
)}
</div>
<div className="asset-info">
<span className="asset-name">{item.name}</span>
{viewMode === 'list' && !item.isDir && (
<div className="asset-meta">
{item.size && <span className="asset-size">{formatFileSize(item.size)}</span>}
{item.modified && <span className="asset-date">{formatDate(item.modified)}</span>}
</div>
)}
</div>
</div>
))}
</div>
)}
</div>
<div className="asset-picker-footer">
<div className="footer-info">
{t('assetPicker.itemCount', { count: filteredAssets.length })}
</div>
<div className="footer-buttons">
<button className="asset-picker-cancel" onClick={onClose}>
{t('assetPicker.cancel')}
</button>
<button
className="asset-picker-select"
onClick={handleSelect}
disabled={!selectedPath}
>
{t('assetPicker.select')}
</button>
</div>
</div>
</div>
</div>
);
}

View File

@@ -1,541 +0,0 @@
import { useState, useEffect } from 'react';
import { Entity } from '@esengine/ecs-framework';
import { EntityStoreService, MessageHub } from '@esengine/editor-core';
import { PropertyInspector } from './PropertyInspector';
import { FileSearch, ChevronDown, ChevronRight, X, Settings } from 'lucide-react';
import '../styles/EntityInspector.css';
interface EntityInspectorProps {
entityStore: EntityStoreService;
messageHub: MessageHub;
}
export function EntityInspector({ entityStore: _entityStore, messageHub }: EntityInspectorProps) {
const [selectedEntity, setSelectedEntity] = useState<Entity | null>(null);
const [remoteEntity, setRemoteEntity] = useState<any | null>(null);
const [remoteEntityDetails, setRemoteEntityDetails] = useState<any | null>(null);
const [expandedComponents, setExpandedComponents] = useState<Set<number>>(new Set());
const [componentVersion, setComponentVersion] = useState(0);
useEffect(() => {
const handleSelection = (data: { entity: Entity | null }) => {
setSelectedEntity((prev) => {
// Only reset version when selecting a different entity
// 只在选择不同实体时重置版本
if (prev?.id !== data.entity?.id) {
setComponentVersion(0);
} else {
// Same entity re-selected, trigger refresh
// 同一实体重新选择,触发刷新
setComponentVersion((v) => v + 1);
}
return data.entity;
});
setRemoteEntity(null);
setRemoteEntityDetails(null);
};
const handleRemoteSelection = (data: { entity: any }) => {
setRemoteEntity(data.entity);
setRemoteEntityDetails(null);
setSelectedEntity(null);
};
const handleEntityDetails = (event: Event) => {
const customEvent = event as CustomEvent;
const details = customEvent.detail;
setRemoteEntityDetails(details);
};
const handleComponentChange = () => {
setComponentVersion((prev) => prev + 1);
};
const unsubSelect = messageHub.subscribe('entity:selected', handleSelection);
const unsubRemoteSelect = messageHub.subscribe('remote-entity:selected', handleRemoteSelection);
const unsubComponentAdded = messageHub.subscribe('component:added', handleComponentChange);
const unsubComponentRemoved = messageHub.subscribe('component:removed', handleComponentChange);
const unsubPropertyChanged = messageHub.subscribe('component:property:changed', handleComponentChange);
window.addEventListener('profiler:entity-details', handleEntityDetails);
return () => {
unsubSelect();
unsubRemoteSelect();
unsubComponentAdded();
unsubComponentRemoved();
unsubPropertyChanged();
window.removeEventListener('profiler:entity-details', handleEntityDetails);
};
}, [messageHub]);
const handleRemoveComponent = (index: number) => {
if (!selectedEntity) return;
const component = selectedEntity.components[index];
if (component) {
selectedEntity.removeComponent(component);
messageHub.publish('component:removed', { entity: selectedEntity, component });
}
};
const toggleComponentExpanded = (index: number) => {
setExpandedComponents((prev) => {
const newSet = new Set(prev);
if (newSet.has(index)) {
newSet.delete(index);
} else {
newSet.add(index);
}
return newSet;
});
};
const handlePropertyChange = (component: any, propertyName: string, value: any) => {
if (!selectedEntity) {
return;
}
// Actually update the component property
// 实际更新组件属性
component[propertyName] = value;
messageHub.publish('component:property:changed', {
entity: selectedEntity,
component,
propertyName,
value
});
// Also publish scene:modified so other panels can react
messageHub.publish('scene:modified', {});
};
const renderRemoteProperty = (key: string, value: any) => {
if (value === null || value === undefined) {
return (
<div key={key} className="property-field">
<label className="property-label">{key}</label>
<span className="property-value-text">null</span>
</div>
);
}
if (Array.isArray(value)) {
return (
<div key={key} className="property-field">
<label className="property-label">{key}</label>
<div style={{ flex: 1, display: 'flex', flexWrap: 'wrap', gap: '4px' }}>
{value.length === 0 ? (
<span className="property-value-text" style={{ opacity: 0.5 }}>Empty Array</span>
) : (
value.map((item, index) => (
<span
key={index}
style={{
padding: '2px 6px',
background: 'var(--color-bg-inset)',
border: '1px solid var(--color-border-default)',
borderRadius: '3px',
fontSize: '10px',
color: 'var(--color-text-primary)',
fontFamily: 'var(--font-family-mono)'
}}
>
{typeof item === 'object' ? JSON.stringify(item) : String(item)}
</span>
))
)}
</div>
</div>
);
}
const valueType = typeof value;
if (valueType === 'boolean') {
return (
<div key={key} className="property-field property-field-boolean">
<label className="property-label">{key}</label>
<div className={`property-toggle ${value ? 'property-toggle-on' : 'property-toggle-off'} property-toggle-readonly`}>
<span className="property-toggle-thumb" />
</div>
</div>
);
}
if (valueType === 'number') {
return (
<div key={key} className="property-field">
<label className="property-label">{key}</label>
<input
type="number"
className="property-input property-input-number"
value={value}
disabled
/>
</div>
);
}
if (valueType === 'string') {
return (
<div key={key} className="property-field">
<label className="property-label">{key}</label>
<input
type="text"
className="property-input property-input-text"
value={value}
disabled
/>
</div>
);
}
if (valueType === 'object' && value.r !== undefined && value.g !== undefined && value.b !== undefined) {
const r = Math.round(value.r * 255);
const g = Math.round(value.g * 255);
const b = Math.round(value.b * 255);
const a = value.a !== undefined ? value.a : 1;
const hexColor = `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`;
return (
<div key={key} className="property-field">
<label className="property-label">{key}</label>
<div className="property-color-wrapper">
<div className="property-color-preview" style={{ backgroundColor: hexColor, opacity: a }} />
<input
type="text"
className="property-input property-input-color-text"
value={`${hexColor.toUpperCase()} (${a.toFixed(2)})`}
disabled
style={{ flex: 1 }}
/>
</div>
</div>
);
}
if (valueType === 'object' && value.minX !== undefined && value.maxX !== undefined && value.minY !== undefined && value.maxY !== undefined) {
return (
<div key={key} className="property-field" style={{ flexDirection: 'column', alignItems: 'stretch' }}>
<label className="property-label" style={{ flex: 'none', marginBottom: '4px' }}>{key}</label>
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
<div className="property-vector-compact">
<div className="property-vector-axis-compact">
<span className="property-vector-axis-label property-vector-axis-x">X</span>
<input
type="number"
className="property-input property-input-number-compact"
value={value.minX}
disabled
placeholder="Min"
/>
</div>
<div className="property-vector-axis-compact">
<span className="property-vector-axis-label property-vector-axis-x">X</span>
<input
type="number"
className="property-input property-input-number-compact"
value={value.maxX}
disabled
placeholder="Max"
/>
</div>
</div>
<div className="property-vector-compact">
<div className="property-vector-axis-compact">
<span className="property-vector-axis-label property-vector-axis-y">Y</span>
<input
type="number"
className="property-input property-input-number-compact"
value={value.minY}
disabled
placeholder="Min"
/>
</div>
<div className="property-vector-axis-compact">
<span className="property-vector-axis-label property-vector-axis-y">Y</span>
<input
type="number"
className="property-input property-input-number-compact"
value={value.maxY}
disabled
placeholder="Max"
/>
</div>
</div>
</div>
</div>
);
}
if (valueType === 'object' && value.x !== undefined && value.y !== undefined) {
if (value.z !== undefined) {
return (
<div key={key} className="property-field">
<label className="property-label">{key}</label>
<div className="property-vector-compact">
<div className="property-vector-axis-compact">
<span className="property-vector-axis-label property-vector-axis-x">X</span>
<input
type="number"
className="property-input property-input-number-compact"
value={value.x}
disabled
/>
</div>
<div className="property-vector-axis-compact">
<span className="property-vector-axis-label property-vector-axis-y">Y</span>
<input
type="number"
className="property-input property-input-number-compact"
value={value.y}
disabled
/>
</div>
<div className="property-vector-axis-compact">
<span className="property-vector-axis-label property-vector-axis-z">Z</span>
<input
type="number"
className="property-input property-input-number-compact"
value={value.z}
disabled
/>
</div>
</div>
</div>
);
} else {
return (
<div key={key} className="property-field">
<label className="property-label">{key}</label>
<div className="property-vector-compact">
<div className="property-vector-axis-compact">
<span className="property-vector-axis-label property-vector-axis-x">X</span>
<input
type="number"
className="property-input property-input-number-compact"
value={value.x}
disabled
/>
</div>
<div className="property-vector-axis-compact">
<span className="property-vector-axis-label property-vector-axis-y">Y</span>
<input
type="number"
className="property-input property-input-number-compact"
value={value.y}
disabled
/>
</div>
</div>
</div>
);
}
}
return (
<div key={key} className="property-field">
<label className="property-label">{key}</label>
<span className="property-value-text">{JSON.stringify(value)}</span>
</div>
);
};
if (!selectedEntity && !remoteEntity) {
return (
<div className="entity-inspector">
<div className="inspector-header">
<FileSearch size={16} className="inspector-header-icon" />
<h3>Inspector</h3>
</div>
<div className="inspector-content">
<div className="empty-state">
<FileSearch size={48} strokeWidth={1.5} className="empty-icon" />
<div className="empty-title">No entity selected</div>
<div className="empty-hint">Select an entity from the hierarchy</div>
</div>
</div>
</div>
);
}
// 显示远程实体
if (remoteEntity) {
const displayData = remoteEntityDetails || remoteEntity;
const hasDetailedComponents = remoteEntityDetails && remoteEntityDetails.components && remoteEntityDetails.components.length > 0;
return (
<div className="entity-inspector">
<div className="inspector-header">
<FileSearch size={16} className="inspector-header-icon" />
<h3>Inspector</h3>
</div>
<div className="inspector-content scrollable">
<div className="inspector-section">
<div className="section-header">
<Settings size={12} className="section-icon" />
<span>Entity Info (Remote)</span>
</div>
<div className="section-content">
<div className="info-row">
<span className="info-label">ID:</span>
<span className="info-value">{displayData.id}</span>
</div>
<div className="info-row">
<span className="info-label">Name:</span>
<span className="info-value">{displayData.name}</span>
</div>
<div className="info-row">
<span className="info-label">Enabled:</span>
<span className="info-value">{displayData.enabled ? 'Yes' : 'No'}</span>
</div>
{displayData.scene && (
<div className="info-row">
<span className="info-label">Scene:</span>
<span className="info-value">{displayData.scene}</span>
</div>
)}
</div>
</div>
<div className="inspector-section">
<div className="section-header">
<Settings size={12} className="section-icon" />
<span>Components ({displayData.componentCount})</span>
</div>
<div className="section-content">
{hasDetailedComponents ? (
<ul className="component-list">
{remoteEntityDetails!.components.map((component: any, index: number) => {
const isExpanded = expandedComponents.has(index);
return (
<li key={index} className={`component-item ${isExpanded ? 'expanded' : ''}`}>
<div className="component-header" onClick={() => toggleComponentExpanded(index)}>
<button
className="component-expand-btn"
title={isExpanded ? 'Collapse' : 'Expand'}
>
{isExpanded ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
</button>
<Settings size={14} className="component-icon" />
<span className="component-name">{component.typeName}</span>
</div>
{isExpanded && (
<div className="component-properties animate-slideDown">
<div className="property-inspector">
{Object.entries(component.properties).map(([key, value]) =>
renderRemoteProperty(key, value)
)}
</div>
</div>
)}
</li>
);
})}
</ul>
) : displayData.componentTypes && displayData.componentTypes.length > 0 ? (
<ul className="component-list">
{displayData.componentTypes.map((componentType: string, index: number) => (
<li key={index} className="component-item">
<div className="component-header">
<Settings size={14} className="component-icon" />
<span className="component-name">{componentType}</span>
</div>
</li>
))}
</ul>
) : (
<div className="empty-state-small">No components</div>
)}
</div>
</div>
</div>
</div>
);
}
const components = selectedEntity!.components;
return (
<div className="entity-inspector">
<div className="inspector-header">
<FileSearch size={16} className="inspector-header-icon" />
<h3>Inspector</h3>
</div>
<div className="inspector-content scrollable">
<div className="inspector-section">
<div className="section-header">
<Settings size={12} className="section-icon" />
<span>Entity Info</span>
</div>
<div className="section-content">
<div className="info-row">
<span className="info-label">ID:</span>
<span className="info-value">{selectedEntity!.id}</span>
</div>
<div className="info-row">
<span className="info-label">Name:</span>
<span className="info-value">Entity {selectedEntity!.id}</span>
</div>
<div className="info-row">
<span className="info-label">Enabled:</span>
<span className="info-value">{selectedEntity!.enabled ? 'Yes' : 'No'}</span>
</div>
</div>
</div>
<div className="inspector-section">
<div className="section-header">
<Settings size={12} className="section-icon" />
<span>Components ({components.length})</span>
</div>
<div className="section-content">
{components.length === 0 ? (
<div className="empty-state-small">No components</div>
) : (
<ul className="component-list" key={componentVersion}>
{components.map((component, index) => {
const isExpanded = expandedComponents.has(index);
return (
<li key={index} className={`component-item ${isExpanded ? 'expanded' : ''}`}>
<div className="component-header" onClick={() => toggleComponentExpanded(index)}>
<button
className="component-expand-btn"
title={isExpanded ? 'Collapse' : 'Expand'}
>
{isExpanded ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
</button>
<Settings size={14} className="component-icon" />
<span className="component-name">{component.constructor.name}</span>
<button
className="remove-component-btn"
onClick={(e) => {
e.stopPropagation();
handleRemoveComponent(index);
}}
title="Remove Component"
>
<X size={14} />
</button>
</div>
{isExpanded && (
<div className="component-properties animate-slideDown">
<PropertyInspector
key={`${index}-${componentVersion}`}
component={component}
onChange={(propertyName, value) => handlePropertyChange(component, propertyName, value)}
/>
</div>
)}
</li>
);
})}
</ul>
)}
</div>
</div>
</div>
</div>
);
}

View File

@@ -1,241 +0,0 @@
import { useState, useEffect } from 'react';
import { Activity, Cpu, Layers, Package, Wifi, WifiOff, Maximize2, Pause, Play, BarChart3 } from 'lucide-react';
import type { ProfilerData } from '../services/tokens';
import { SettingsService } from '../services/SettingsService';
import { Core } from '@esengine/ecs-framework';
import { MessageHub } from '@esengine/editor-core';
import { getProfilerService } from '../services/getService';
import '../styles/ProfilerDockPanel.css';
export function ProfilerDockPanel() {
const [profilerData, setProfilerData] = useState<ProfilerData | null>(null);
const [isConnected, setIsConnected] = useState(false);
const [isServerRunning, setIsServerRunning] = useState(false);
const [port, setPort] = useState('8080');
const [isPaused, setIsPaused] = useState(false);
useEffect(() => {
const settings = SettingsService.getInstance();
setPort(settings.get('profiler.port', '8080'));
const handleSettingsChange = ((event: CustomEvent) => {
const newPort = event.detail['profiler.port'];
if (newPort) {
setPort(newPort);
}
}) as EventListener;
window.addEventListener('settings:changed', handleSettingsChange);
return () => {
window.removeEventListener('settings:changed', handleSettingsChange);
};
}, []);
useEffect(() => {
const profilerService = getProfilerService();
if (!profilerService) {
console.warn('[ProfilerDockPanel] ProfilerService not available - plugin may be disabled');
setIsServerRunning(false);
setIsConnected(false);
return;
}
// 订阅数据更新
const unsubscribe = profilerService.subscribe((data: ProfilerData) => {
if (!isPaused) {
setProfilerData(data);
}
});
// 定期检查连接状态
const checkStatus = () => {
setIsConnected(profilerService.isConnected());
setIsServerRunning(profilerService.isServerActive());
};
checkStatus();
const interval = setInterval(checkStatus, 1000);
return () => {
unsubscribe();
clearInterval(interval);
};
}, [isPaused]);
const fps = profilerData?.fps || 0;
const totalFrameTime = profilerData?.totalFrameTime || 0;
const systems = (profilerData?.systems || []).slice(0, 5); // Only show top 5 systems in dock panel
const entityCount = profilerData?.entityCount || 0;
const componentCount = profilerData?.componentCount || 0;
const targetFrameTime = 16.67;
const handleOpenDetails = () => {
const messageHub = Core.services.resolve(MessageHub);
if (messageHub) {
messageHub.publish('ui:openWindow', { windowId: 'profiler' });
}
};
const handleOpenAdvancedProfiler = () => {
const messageHub = Core.services.resolve(MessageHub);
if (messageHub) {
messageHub.publish('ui:openWindow', { windowId: 'advancedProfiler' });
}
};
const handleTogglePause = () => {
setIsPaused(!isPaused);
};
return (
<div className="profiler-dock-panel">
<div className="profiler-dock-header">
<h3>Performance Monitor</h3>
<div className="profiler-dock-header-actions">
{isConnected && (
<>
<button
className="profiler-dock-pause-btn"
onClick={handleTogglePause}
title={isPaused ? 'Resume data updates' : 'Pause data updates'}
>
{isPaused ? <Play size={14} /> : <Pause size={14} />}
</button>
<button
className="profiler-dock-details-btn"
onClick={handleOpenAdvancedProfiler}
title="Open advanced profiler"
>
<BarChart3 size={14} />
</button>
<button
className="profiler-dock-details-btn"
onClick={handleOpenDetails}
title="Open detailed profiler"
>
<Maximize2 size={14} />
</button>
</>
)}
<div className="profiler-dock-status">
{isConnected ? (
<>
<Wifi size={12} />
<span className="status-text connected">Connected</span>
</>
) : isServerRunning ? (
<>
<WifiOff size={12} />
<span className="status-text waiting">Waiting...</span>
</>
) : (
<>
<WifiOff size={12} />
<span className="status-text disconnected">Server Off</span>
</>
)}
</div>
</div>
</div>
{!isServerRunning ? (
<div className="profiler-dock-empty">
<Cpu size={32} />
<p>Profiler server not running</p>
<p className="hint">Open Profiler window and connect to start monitoring</p>
</div>
) : !isConnected ? (
<div className="profiler-dock-empty">
<Activity size={32} />
<p>Waiting for game connection...</p>
<p className="hint">Connect to: <code>ws://localhost:{port}</code></p>
</div>
) : (
<div className="profiler-dock-content">
<div className="profiler-dock-stats">
<div className="stat-card">
<div className="stat-icon">
<Activity size={16} />
</div>
<div className="stat-info">
<div className="stat-label">FPS</div>
<div className={`stat-value ${fps < 55 ? 'warning' : ''}`}>{fps}</div>
</div>
</div>
<div className="stat-card">
<div className="stat-icon">
<Cpu size={16} />
</div>
<div className="stat-info">
<div className="stat-label">Frame Time</div>
<div className={`stat-value ${totalFrameTime > targetFrameTime ? 'warning' : ''}`}>
{totalFrameTime.toFixed(1)}ms
</div>
</div>
</div>
<div className="stat-card">
<div className="stat-icon">
<Layers size={16} />
</div>
<div className="stat-info">
<div className="stat-label">Entities</div>
<div className="stat-value">{entityCount}</div>
</div>
</div>
<div className="stat-card">
<div className="stat-icon">
<Package size={16} />
</div>
<div className="stat-info">
<div className="stat-label">Components</div>
<div className="stat-value">{componentCount}</div>
</div>
</div>
</div>
{systems.length > 0 && (
<div className="profiler-dock-systems">
<h4>Top Systems</h4>
<div className="systems-list">
{systems.map((system) => (
<div key={system.name} className="system-item">
<div className="system-item-header">
<span className="system-item-name">{system.name}</span>
<span className="system-item-time">
{system.executionTime.toFixed(2)}ms
</span>
</div>
<div className="system-item-bar">
<div
className="system-item-bar-fill"
style={{
width: `${Math.min(system.percentage, 100)}%`,
backgroundColor: system.executionTime > targetFrameTime
? 'var(--color-danger)'
: system.executionTime > targetFrameTime * 0.5
? 'var(--color-warning)'
: 'var(--color-success)'
}}
/>
</div>
<div className="system-item-footer">
<span className="system-item-percentage">{system.percentage.toFixed(1)}%</span>
{system.entityCount > 0 && (
<span className="system-item-entities">{system.entityCount} entities</span>
)}
</div>
</div>
))}
</div>
</div>
)}
</div>
)}
</div>
);
}

View File

@@ -1,229 +0,0 @@
import { useState, useEffect, useRef } from 'react';
import { Core } from '@esengine/ecs-framework';
import { Activity, BarChart3, Clock, Cpu, RefreshCw, Pause, Play } from 'lucide-react';
import '../styles/ProfilerPanel.css';
interface SystemPerformanceData {
name: string;
executionTime: number;
entityCount: number;
averageTime: number;
minTime: number;
maxTime: number;
percentage: number;
}
export function ProfilerPanel() {
const [systems, setSystems] = useState<SystemPerformanceData[]>([]);
const [totalFrameTime, setTotalFrameTime] = useState(0);
const [isPaused, setIsPaused] = useState(false);
const [sortBy, setSortBy] = useState<'time' | 'average' | 'name'>('time');
const animationRef = useRef<number>();
useEffect(() => {
const updateProfilerData = () => {
if (isPaused) {
animationRef.current = requestAnimationFrame(updateProfilerData);
return;
}
const performanceMonitor = Core.performanceMonitor;
if (!performanceMonitor?.isEnabled) {
animationRef.current = requestAnimationFrame(updateProfilerData);
return;
}
const systemDataMap = performanceMonitor.getAllSystemData();
const systemStatsMap = performanceMonitor.getAllSystemStats();
const systemsData: SystemPerformanceData[] = [];
let total = 0;
for (const [name, data] of systemDataMap.entries()) {
const stats = systemStatsMap.get(name);
if (stats) {
systemsData.push({
name,
executionTime: data.executionTime,
entityCount: data.entityCount,
averageTime: stats.averageTime,
minTime: stats.minTime,
maxTime: stats.maxTime,
percentage: 0
});
total += data.executionTime;
}
}
// Calculate percentages
systemsData.forEach((system) => {
system.percentage = total > 0 ? (system.executionTime / total) * 100 : 0;
});
// Sort systems
systemsData.sort((a, b) => {
switch (sortBy) {
case 'time':
return b.executionTime - a.executionTime;
case 'average':
return b.averageTime - a.averageTime;
case 'name':
return a.name.localeCompare(b.name);
default:
return 0;
}
});
setSystems(systemsData);
setTotalFrameTime(total);
animationRef.current = requestAnimationFrame(updateProfilerData);
};
animationRef.current = requestAnimationFrame(updateProfilerData);
return () => {
if (animationRef.current) {
cancelAnimationFrame(animationRef.current);
}
};
}, [isPaused, sortBy]);
const handleReset = () => {
Core.performanceMonitor?.reset();
};
const fps = totalFrameTime > 0 ? Math.round(1000 / totalFrameTime) : 0;
const targetFrameTime = 16.67; // 60 FPS
const isOverBudget = totalFrameTime > targetFrameTime;
return (
<div className="profiler-panel">
<div className="profiler-toolbar">
<div className="profiler-toolbar-left">
<div className="profiler-stats-summary">
<div className="summary-item">
<Clock size={14} />
<span className="summary-label">Frame:</span>
<span className={`summary-value ${isOverBudget ? 'over-budget' : ''}`}>
{totalFrameTime.toFixed(2)}ms
</span>
</div>
<div className="summary-item">
<Activity size={14} />
<span className="summary-label">FPS:</span>
<span className={`summary-value ${fps < 55 ? 'low-fps' : ''}`}>{fps}</span>
</div>
<div className="summary-item">
<BarChart3 size={14} />
<span className="summary-label">Systems:</span>
<span className="summary-value">{systems.length}</span>
</div>
</div>
</div>
<div className="profiler-toolbar-right">
<select
className="profiler-sort"
value={sortBy}
onChange={(e) => setSortBy(e.target.value as any)}
>
<option value="time">Sort by Time</option>
<option value="average">Sort by Average</option>
<option value="name">Sort by Name</option>
</select>
<button
className="profiler-btn"
onClick={() => setIsPaused(!isPaused)}
title={isPaused ? 'Resume' : 'Pause'}
>
{isPaused ? <Play size={14} /> : <Pause size={14} />}
</button>
<button
className="profiler-btn"
onClick={handleReset}
title="Reset Statistics"
>
<RefreshCw size={14} />
</button>
</div>
</div>
<div className="profiler-content">
{systems.length === 0 ? (
<div className="profiler-empty">
<Cpu size={48} />
<p>No performance data available</p>
<p className="profiler-empty-hint">
Make sure Core debug mode is enabled and systems are running
</p>
</div>
) : (
<div className="profiler-systems">
{systems.map((system, index) => (
<div key={system.name} className="system-row">
<div className="system-header">
<div className="system-info">
<span className="system-rank">#{index + 1}</span>
<span className="system-name">{system.name}</span>
{system.entityCount > 0 && (
<span className="system-entities">
({system.entityCount} entities)
</span>
)}
</div>
<div className="system-metrics">
<span className="metric-time">{system.executionTime.toFixed(2)}ms</span>
<span className="metric-percentage">{system.percentage.toFixed(1)}%</span>
</div>
</div>
<div className="system-bar">
<div
className="system-bar-fill"
style={{
width: `${Math.min(system.percentage, 100)}%`,
backgroundColor: system.executionTime > targetFrameTime
? 'var(--color-danger)'
: system.executionTime > targetFrameTime * 0.5
? 'var(--color-warning)'
: 'var(--color-success)'
}}
/>
</div>
<div className="system-stats">
<div className="stat-item">
<span className="stat-label">Avg:</span>
<span className="stat-value">{system.averageTime.toFixed(2)}ms</span>
</div>
<div className="stat-item">
<span className="stat-label">Min:</span>
<span className="stat-value">{system.minTime.toFixed(2)}ms</span>
</div>
<div className="stat-item">
<span className="stat-label">Max:</span>
<span className="stat-value">{system.maxTime.toFixed(2)}ms</span>
</div>
</div>
</div>
))}
</div>
)}
</div>
<div className="profiler-footer">
<div className="profiler-legend">
<div className="legend-item">
<div className="legend-color" style={{ background: 'var(--color-success)' }} />
<span>Good (&lt;8ms)</span>
</div>
<div className="legend-item">
<div className="legend-color" style={{ background: 'var(--color-warning)' }} />
<span>Warning (8-16ms)</span>
</div>
<div className="legend-item">
<div className="legend-color" style={{ background: 'var(--color-danger)' }} />
<span>Critical (&gt;16ms)</span>
</div>
</div>
</div>
</div>
);
}

View File

@@ -1,589 +0,0 @@
import { useState, useEffect, useRef } from 'react';
import { Core } from '@esengine/ecs-framework';
import { Activity, BarChart3, Clock, Cpu, RefreshCw, Pause, Play, X, Wifi, WifiOff, Server, Search, Table2, TreePine } from 'lucide-react';
import { ProfilerService } from '../services/ProfilerService';
import { SettingsService } from '../services/SettingsService';
import { getProfilerService } from '../services/getService';
import '../styles/ProfilerWindow.css';
interface SystemPerformanceData {
name: string;
executionTime: number;
entityCount: number;
averageTime: number;
minTime: number;
maxTime: number;
percentage: number;
level: number;
children?: SystemPerformanceData[];
isExpanded?: boolean;
}
interface ProfilerWindowProps {
onClose: () => void;
}
type DataSource = 'local' | 'remote';
export function ProfilerWindow({ onClose }: ProfilerWindowProps) {
const [systems, setSystems] = useState<SystemPerformanceData[]>([]);
const [totalFrameTime, setTotalFrameTime] = useState(0);
const [isPaused, setIsPaused] = useState(false);
const [sortBy] = useState<'time' | 'average' | 'name'>('time');
const [dataSource, setDataSource] = useState<DataSource>('local');
const [viewMode, setViewMode] = useState<'tree' | 'table'>('table');
const [searchQuery, setSearchQuery] = useState('');
const [isConnected, setIsConnected] = useState(false);
const [isServerRunning, setIsServerRunning] = useState(false);
const [port, setPort] = useState('8080');
const animationRef = useRef<number>();
const frameTimesRef = useRef<number[]>([]);
const lastFpsRef = useRef<number>(0);
useEffect(() => {
const settings = SettingsService.getInstance();
setPort(settings.get('profiler.port', '8080'));
const handleSettingsChange = ((event: CustomEvent) => {
const newPort = event.detail['profiler.port'];
if (newPort) {
setPort(newPort);
}
}) as EventListener;
window.addEventListener('settings:changed', handleSettingsChange);
return () => {
window.removeEventListener('settings:changed', handleSettingsChange);
};
}, []);
// Check ProfilerService connection status
useEffect(() => {
const profilerService = getProfilerService();
if (!profilerService) {
return;
}
const checkStatus = () => {
setIsConnected(profilerService.isConnected());
setIsServerRunning(profilerService.isServerActive());
};
checkStatus();
const interval = setInterval(checkStatus, 1000);
return () => clearInterval(interval);
}, []);
const buildSystemTree = (flatSystems: Map<string, any>, statsMap: Map<string, any>): SystemPerformanceData[] => {
const coreUpdate = flatSystems.get('Core.update');
const servicesUpdate = flatSystems.get('Services.update');
if (!coreUpdate) return [];
const coreStats = statsMap.get('Core.update');
const coreNode: SystemPerformanceData = {
name: 'Core.update',
executionTime: coreUpdate.executionTime,
entityCount: 0,
averageTime: coreStats?.averageTime || 0,
minTime: coreStats?.minTime || 0,
maxTime: coreStats?.maxTime || 0,
percentage: 100,
level: 0,
children: [],
isExpanded: true
};
if (servicesUpdate) {
const servicesStats = statsMap.get('Services.update');
coreNode.children!.push({
name: 'Services.update',
executionTime: servicesUpdate.executionTime,
entityCount: 0,
averageTime: servicesStats?.averageTime || 0,
minTime: servicesStats?.minTime || 0,
maxTime: servicesStats?.maxTime || 0,
percentage: coreUpdate.executionTime > 0
? (servicesUpdate.executionTime / coreUpdate.executionTime) * 100
: 0,
level: 1,
isExpanded: false
});
}
const sceneSystems: SystemPerformanceData[] = [];
for (const [name, data] of flatSystems.entries()) {
if (name !== 'Core.update' && name !== 'Services.update') {
const stats = statsMap.get(name);
if (stats) {
sceneSystems.push({
name,
executionTime: data.executionTime,
entityCount: data.entityCount,
averageTime: stats.averageTime,
minTime: stats.minTime,
maxTime: stats.maxTime,
percentage: 0,
level: 1,
isExpanded: false
});
}
}
}
sceneSystems.forEach((system) => {
system.percentage = coreUpdate.executionTime > 0
? (system.executionTime / coreUpdate.executionTime) * 100
: 0;
});
sceneSystems.sort((a, b) => b.executionTime - a.executionTime);
coreNode.children!.push(...sceneSystems);
return [coreNode];
};
// Subscribe to local performance data
useEffect(() => {
if (dataSource !== 'local') return;
const updateProfilerData = () => {
if (isPaused) {
animationRef.current = requestAnimationFrame(updateProfilerData);
return;
}
const performanceMonitor = Core.performanceMonitor;
if (!performanceMonitor?.isEnabled) {
animationRef.current = requestAnimationFrame(updateProfilerData);
return;
}
const systemDataMap = performanceMonitor.getAllSystemData();
const systemStatsMap = performanceMonitor.getAllSystemStats();
const tree = buildSystemTree(systemDataMap, systemStatsMap);
const coreData = systemDataMap.get('Core.update');
setSystems(tree);
setTotalFrameTime(coreData?.executionTime || 0);
animationRef.current = requestAnimationFrame(updateProfilerData);
};
animationRef.current = requestAnimationFrame(updateProfilerData);
return () => {
if (animationRef.current) {
cancelAnimationFrame(animationRef.current);
}
};
}, [isPaused, sortBy, dataSource]);
// Subscribe to remote performance data from ProfilerService
useEffect(() => {
if (dataSource !== 'remote') return;
const profilerService = getProfilerService();
if (!profilerService) {
console.warn('[ProfilerWindow] ProfilerService not available');
return;
}
const unsubscribe = profilerService.subscribe((data) => {
if (isPaused) return;
handleRemoteDebugData({
performance: {
frameTime: data.totalFrameTime,
systemPerformance: data.systems.map((sys) => ({
systemName: sys.name,
lastExecutionTime: sys.executionTime,
averageTime: sys.averageTime,
minTime: 0,
maxTime: 0,
entityCount: sys.entityCount,
percentage: sys.percentage
}))
}
});
});
return () => unsubscribe();
}, [dataSource, isPaused]);
const handleReset = () => {
if (dataSource === 'local') {
Core.performanceMonitor?.reset();
} else {
// Reset remote data
setSystems([]);
setTotalFrameTime(0);
}
};
const handleRemoteDebugData = (debugData: any) => {
if (isPaused) return;
const performance = debugData.performance;
if (!performance) return;
if (!performance.systemPerformance || !Array.isArray(performance.systemPerformance)) {
return;
}
const flatSystemsMap = new Map();
const statsMap = new Map();
for (const system of performance.systemPerformance) {
flatSystemsMap.set(system.systemName, {
executionTime: system.lastExecutionTime || system.averageTime || 0,
entityCount: system.entityCount || 0
});
statsMap.set(system.systemName, {
averageTime: system.averageTime || 0,
minTime: system.minTime || 0,
maxTime: system.maxTime || 0
});
}
const tree = buildSystemTree(flatSystemsMap, statsMap);
setSystems(tree);
setTotalFrameTime(performance.frameTime || 0);
};
const handleDataSourceChange = (newSource: DataSource) => {
if (newSource === 'remote' && dataSource === 'local') {
// Switching to remote
if (animationRef.current) {
cancelAnimationFrame(animationRef.current);
}
}
setDataSource(newSource);
setSystems([]);
setTotalFrameTime(0);
};
const toggleExpand = (systemName: string) => {
const toggleNode = (nodes: SystemPerformanceData[]): SystemPerformanceData[] => {
return nodes.map((node) => {
if (node.name === systemName) {
return { ...node, isExpanded: !node.isExpanded };
}
if (node.children) {
return { ...node, children: toggleNode(node.children) };
}
return node;
});
};
setSystems(toggleNode(systems));
};
const flattenTree = (nodes: SystemPerformanceData[]): SystemPerformanceData[] => {
const result: SystemPerformanceData[] = [];
for (const node of nodes) {
result.push(node);
if (node.isExpanded && node.children) {
result.push(...flattenTree(node.children));
}
}
return result;
};
// Calculate FPS using rolling average for stability
// 使用滑动平均计算 FPS 以保持稳定
const calculateFps = () => {
// Add any positive frame time
// 添加任何正数的帧时间
if (totalFrameTime > 0) {
frameTimesRef.current.push(totalFrameTime);
// Keep last 60 samples
if (frameTimesRef.current.length > 60) {
frameTimesRef.current.shift();
}
}
if (frameTimesRef.current.length > 0) {
const avgFrameTime = frameTimesRef.current.reduce((a, b) => a + b, 0) / frameTimesRef.current.length;
// Cap FPS between 0-999, and ensure avgFrameTime is reasonable
if (avgFrameTime > 0.01) {
lastFpsRef.current = Math.min(999, Math.round(1000 / avgFrameTime));
}
}
return lastFpsRef.current;
};
const fps = calculateFps();
const targetFrameTime = 16.67;
const isOverBudget = totalFrameTime > targetFrameTime;
let displaySystems = viewMode === 'tree' ? flattenTree(systems) : systems;
// Apply search filter
if (searchQuery.trim()) {
const query = searchQuery.toLowerCase();
if (viewMode === 'tree') {
displaySystems = displaySystems.filter((sys) =>
sys.name.toLowerCase().includes(query)
);
} else {
// For table view, flatten and filter
const flatList: SystemPerformanceData[] = [];
const flatten = (nodes: SystemPerformanceData[]) => {
for (const node of nodes) {
flatList.push(node);
if (node.children) flatten(node.children);
}
};
flatten(systems);
displaySystems = flatList.filter((sys) =>
sys.name.toLowerCase().includes(query)
);
}
} else if (viewMode === 'table') {
// For table view without search, flatten all
const flatList: SystemPerformanceData[] = [];
const flatten = (nodes: SystemPerformanceData[]) => {
for (const node of nodes) {
flatList.push(node);
if (node.children) flatten(node.children);
}
};
flatten(systems);
displaySystems = flatList;
}
return (
<div className="profiler-window-overlay" onClick={onClose}>
<div className="profiler-window" onClick={(e) => e.stopPropagation()}>
<div className="profiler-window-header">
<div className="profiler-window-title">
<BarChart3 size={20} />
<h2>Performance Profiler</h2>
{isPaused && (
<span className="paused-indicator">PAUSED</span>
)}
</div>
<button className="profiler-window-close" onClick={onClose} title="Close">
<X size={20} />
</button>
</div>
<div className="profiler-window-toolbar">
<div className="profiler-toolbar-left">
<div className="profiler-mode-switch">
<button
className={`mode-btn ${dataSource === 'local' ? 'active' : ''}`}
onClick={() => handleDataSourceChange('local')}
title="Local Core Instance"
>
<Cpu size={14} />
<span>Local</span>
</button>
<button
className={`mode-btn ${dataSource === 'remote' ? 'active' : ''}`}
onClick={() => handleDataSourceChange('remote')}
title="Remote Game Connection"
>
<Server size={14} />
<span>Remote</span>
</button>
</div>
{dataSource === 'remote' && (
<div className="profiler-connection">
<div className="connection-port-display">
<Server size={14} />
<span>ws://localhost:{port}</span>
</div>
{isConnected ? (
<div className="connection-status-indicator connected">
<Wifi size={14} />
<span>Connected</span>
</div>
) : isServerRunning ? (
<div className="connection-status-indicator waiting">
<WifiOff size={14} />
<span>Waiting for game...</span>
</div>
) : (
<div className="connection-status-indicator disconnected">
<WifiOff size={14} />
<span>Server Off</span>
</div>
)}
</div>
)}
{dataSource === 'local' && (
<div className="profiler-stats-summary">
<div className="summary-item">
<Clock size={14} />
<span className="summary-label">Frame:</span>
<span className={`summary-value ${isOverBudget ? 'over-budget' : ''}`}>
{totalFrameTime.toFixed(2)}ms
</span>
</div>
<div className="summary-item">
<Activity size={14} />
<span className="summary-label">FPS:</span>
<span className={`summary-value ${fps < 55 ? 'low-fps' : ''}`}>{fps}</span>
</div>
<div className="summary-item">
<BarChart3 size={14} />
<span className="summary-label">Systems:</span>
<span className="summary-value">{systems.length}</span>
</div>
</div>
)}
</div>
<div className="profiler-toolbar-right">
<div className="profiler-search">
<Search size={14} />
<input
type="text"
placeholder="Search systems..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="search-input"
/>
</div>
<div className="view-mode-switch">
<button
className={`view-mode-btn ${viewMode === 'table' ? 'active' : ''}`}
onClick={() => setViewMode('table')}
title="Table View"
>
<Table2 size={14} />
</button>
<button
className={`view-mode-btn ${viewMode === 'tree' ? 'active' : ''}`}
onClick={() => setViewMode('tree')}
title="Tree View"
>
<TreePine size={14} />
</button>
</div>
<button
className={`profiler-btn ${isPaused ? 'paused' : ''}`}
onClick={() => setIsPaused(!isPaused)}
title={isPaused ? 'Resume' : 'Pause'}
>
{isPaused ? <Play size={14} /> : <Pause size={14} />}
</button>
<button
className="profiler-btn"
onClick={handleReset}
title="Reset Statistics"
>
<RefreshCw size={14} />
</button>
</div>
</div>
<div className="profiler-window-content">
{displaySystems.length === 0 ? (
<div className="profiler-empty">
<Cpu size={48} />
<p>No performance data available</p>
<p className="profiler-empty-hint">
{searchQuery ? 'No systems match your search' : 'Make sure Core debug mode is enabled and systems are running'}
</p>
</div>
) : viewMode === 'table' ? (
<table className="profiler-table">
<thead>
<tr>
<th className="col-name">System Name</th>
<th className="col-time">Current</th>
<th className="col-time">Average</th>
<th className="col-time">Min</th>
<th className="col-time">Max</th>
<th className="col-percent">%</th>
<th className="col-entities">Entities</th>
</tr>
</thead>
<tbody>
{displaySystems.map((system) => (
<tr key={system.name} className={`level-${system.level}`}>
<td className="col-name">
<span className="system-name-cell" style={{ paddingLeft: `${system.level * 16}px` }}>
{system.name}
</span>
</td>
<td className="col-time">
<span className={`time-value ${system.executionTime > targetFrameTime ? 'critical' : system.executionTime > targetFrameTime * 0.5 ? 'warning' : ''}`}>
{system.executionTime.toFixed(2)}ms
</span>
</td>
<td className="col-time">{system.averageTime.toFixed(2)}ms</td>
<td className="col-time">{system.minTime.toFixed(2)}ms</td>
<td className="col-time">{system.maxTime.toFixed(2)}ms</td>
<td className="col-percent">{system.percentage.toFixed(1)}%</td>
<td className="col-entities">{system.entityCount || '-'}</td>
</tr>
))}
</tbody>
</table>
) : (
<div className="profiler-tree">
{displaySystems.map((system) => (
<div key={system.name} className={`tree-row level-${system.level}`}>
<div className="tree-row-header">
<div className="tree-row-left">
{system.children && system.children.length > 0 && (
<button
className="expand-btn"
onClick={() => toggleExpand(system.name)}
>
{system.isExpanded ? '▼' : '▶'}
</button>
)}
<span className="system-name">{system.name}</span>
{system.entityCount > 0 && (
<span className="system-entities">({system.entityCount})</span>
)}
</div>
<div className="tree-row-right">
<span className={`time-value ${system.executionTime > targetFrameTime ? 'critical' : system.executionTime > targetFrameTime * 0.5 ? 'warning' : ''}`}>
{system.executionTime.toFixed(2)}ms
</span>
<span className="percentage-badge">{system.percentage.toFixed(1)}%</span>
</div>
</div>
<div className="tree-row-stats">
<span>Avg: {system.averageTime.toFixed(2)}ms</span>
<span>Min: {system.minTime.toFixed(2)}ms</span>
<span>Max: {system.maxTime.toFixed(2)}ms</span>
</div>
</div>
))}
</div>
)}
</div>
<div className="profiler-window-footer">
<div className="profiler-legend">
<div className="legend-item">
<div className="legend-color" style={{ background: 'var(--color-success)' }} />
<span>Good (&lt;8ms)</span>
</div>
<div className="legend-item">
<div className="legend-color" style={{ background: 'var(--color-warning)' }} />
<span>Warning (8-16ms)</span>
</div>
<div className="legend-item">
<div className="legend-color" style={{ background: 'var(--color-danger)' }} />
<span>Critical (&gt;16ms)</span>
</div>
</div>
</div>
</div>
</div>
);
}

View File

@@ -1,21 +0,0 @@
/**
* Inspector Components
* Inspector 组件导出
*/
// 主组件 | Main components
export * from './InspectorPanel';
export * from './EntityInspectorPanel';
export * from './ComponentPropertyEditor';
// 类型 | Types
export * from './types';
// 头部组件 | Header components
export * from './header';
// 分组组件 | Section components
export * from './sections';
// 控件组件 | Control components
export * from './controls';

View File

@@ -1,2 +0,0 @@
export { Inspector } from './Inspector';
export type { InspectorProps, InspectorTarget, AssetFileInfo } from './types';

View File

@@ -1,235 +0,0 @@
.game-view {
position: relative;
width: 100%;
height: 100%;
background: #000;
display: flex;
flex-direction: column;
overflow: hidden;
}
.game-view-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;
z-index: var(--z-index-above);
}
.game-view-toolbar-left {
display: flex;
align-items: center;
gap: 4px;
}
.game-view-toolbar-right {
display: flex;
align-items: center;
gap: 4px;
}
.game-view-btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 2px;
min-width: 32px;
height: 32px;
background: transparent;
border: 1px solid transparent;
border-radius: var(--radius-sm);
color: var(--color-text-secondary);
cursor: pointer;
transition: all var(--transition-fast);
padding: 0 6px;
}
.game-view-btn:hover:not(:disabled) {
background: var(--color-bg-hover);
color: var(--color-text-primary);
border-color: var(--color-border-hover);
}
.game-view-btn.active {
background: var(--color-primary);
color: var(--color-text-inverse);
border-color: var(--color-primary);
}
.game-view-btn:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.game-view-btn:active:not(:disabled) {
transform: scale(0.95);
}
.game-view-divider {
width: 1px;
height: 24px;
background: var(--color-border-default);
margin: 0 4px;
}
.game-view-dropdown {
position: relative;
}
.game-view-dropdown-menu {
position: absolute;
top: 100%;
left: 0;
margin-top: 4px;
background: var(--color-bg-elevated);
border: 1px solid var(--color-border-default);
border-radius: var(--radius-sm);
box-shadow: var(--shadow-lg);
z-index: var(--z-index-dropdown);
min-width: 160px;
padding: 4px;
animation: dropdownFadeIn 0.15s ease-out;
}
@keyframes dropdownFadeIn {
from {
opacity: 0;
transform: translateY(-4px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.game-view-dropdown-menu button {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
padding: 8px 12px;
background: transparent;
border: none;
border-radius: var(--radius-xs);
color: var(--color-text-primary);
font-size: 13px;
cursor: pointer;
text-align: left;
}
.game-view-dropdown-menu button:hover {
background: var(--color-bg-hover);
}
.game-view-canvas {
flex: 1;
width: 100%;
height: 100%;
display: block;
background: #000;
user-select: none;
}
.game-view-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.6);
pointer-events: none;
}
.game-view-overlay-content {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
color: var(--color-text-secondary);
}
.game-view-overlay-content svg {
opacity: 0.5;
}
.game-view-overlay-content span {
font-size: 14px;
}
.game-view-stats {
position: absolute;
bottom: 12px;
right: 12px;
display: flex;
flex-direction: column;
gap: 4px;
background: rgba(0, 0, 0, 0.8);
backdrop-filter: blur(8px);
border: 1px solid var(--color-border-subtle);
border-radius: var(--radius-sm);
padding: 8px 12px;
font-family: var(--font-family-mono);
font-size: 11px;
pointer-events: none;
z-index: var(--z-index-above);
animation: fadeIn 0.2s ease-out;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(4px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.game-view-stat {
display: flex;
align-items: center;
gap: 8px;
white-space: nowrap;
}
.game-view-stat-label {
color: var(--color-text-secondary);
font-weight: 500;
}
.game-view-stat-value {
color: var(--color-primary);
font-weight: 600;
font-variant-numeric: tabular-nums;
}
.game-view:fullscreen {
background: #000;
}
.game-view:fullscreen .game-view-toolbar {
display: none;
}
.game-view:fullscreen .game-view-overlay {
display: none;
}
@media (prefers-reduced-motion: reduce) {
.game-view-btn {
transition: none;
}
.game-view-dropdown-menu {
animation: none;
}
}

View File

@@ -1,509 +0,0 @@
.plugin-manager-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.6);
display: flex;
align-items: center;
justify-content: center;
z-index: var(--z-index-modal);
backdrop-filter: blur(4px);
}
.plugin-manager-window {
background: var(--color-bg-elevated);
border: 1px solid var(--color-border-default);
border-radius: 8px;
width: 90%;
max-width: 1000px;
height: 80%;
max-height: 700px;
display: flex;
flex-direction: column;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
overflow: hidden;
}
.plugin-manager-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
background: var(--color-bg-overlay);
border-bottom: 1px solid var(--color-border-default);
}
.plugin-manager-title {
display: flex;
align-items: center;
gap: 12px;
color: var(--color-text-primary);
}
.plugin-manager-title h2 {
margin: 0;
font-size: 18px;
font-weight: 600;
}
.plugin-manager-close {
background: none;
border: none;
color: var(--color-text-secondary);
cursor: pointer;
padding: 6px;
border-radius: 4px;
transition: all 0.2s;
display: flex;
align-items: center;
justify-content: center;
}
.plugin-manager-close:hover {
background: var(--color-bg-hover);
color: var(--color-text-primary);
}
.plugin-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 20px;
background: var(--color-bg-elevated);
border-bottom: 1px solid var(--color-border-default);
gap: 12px;
}
.plugin-toolbar-left,
.plugin-toolbar-right {
display: flex;
align-items: center;
gap: 8px;
}
.plugin-search {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
background: var(--color-bg-base);
border: 1px solid var(--color-border-default);
border-radius: 4px;
color: var(--color-text-secondary);
}
.plugin-search input {
border: none;
background: none;
outline: none;
color: var(--color-text-primary);
font-size: 13px;
min-width: 250px;
}
.plugin-stats {
display: flex;
align-items: center;
gap: 12px;
padding: 0 8px;
font-size: 12px;
}
.plugin-stats .stat-item {
display: flex;
align-items: center;
gap: 4px;
}
.plugin-stats .stat-item.enabled {
color: var(--color-success);
}
.plugin-stats .stat-item.disabled {
color: var(--color-text-secondary);
}
.plugin-view-mode {
display: flex;
gap: 2px;
background: var(--color-bg-base);
border: 1px solid var(--color-border-default);
border-radius: 4px;
overflow: hidden;
}
.plugin-view-mode button {
padding: 6px 10px;
background: none;
border: none;
color: var(--color-text-secondary);
cursor: pointer;
transition: all 0.2s;
display: flex;
align-items: center;
justify-content: center;
}
.plugin-view-mode button:hover {
background: var(--color-bg-hover);
}
.plugin-view-mode button.active {
background: var(--color-primary);
color: white;
}
.plugin-content {
flex: 1;
overflow-y: auto;
padding: 20px;
}
.plugin-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
color: var(--color-text-secondary);
gap: 16px;
}
.plugin-categories {
display: flex;
flex-direction: column;
gap: 16px;
}
.plugin-category {
background: var(--color-bg-overlay);
border: 1px solid var(--color-border-default);
border-radius: 6px;
overflow: hidden;
}
.plugin-category-header {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 16px;
background: var(--color-bg-elevated);
cursor: pointer;
user-select: none;
transition: background 0.2s;
}
.plugin-category-header:hover {
background: var(--color-bg-hover);
}
.plugin-category-toggle {
background: none;
border: none;
padding: 0;
color: var(--color-text-secondary);
cursor: pointer;
display: flex;
align-items: center;
}
.plugin-category-icon {
font-size: 18px;
}
.plugin-category-name {
flex: 1;
font-size: 14px;
font-weight: 600;
color: var(--color-text-primary);
}
.plugin-category-count {
font-size: 12px;
color: var(--color-text-secondary);
background: var(--color-bg-overlay);
padding: 3px 10px;
border-radius: 12px;
}
.plugin-category-content {
padding: 16px;
}
.plugin-category-content.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 12px;
}
.plugin-category-content.list {
display: flex;
flex-direction: column;
gap: 8px;
}
/* Plugin Card (Grid View) */
.plugin-card {
background: var(--color-bg-base);
border: 1px solid var(--color-border-default);
border-radius: 6px;
padding: 14px;
transition: all 0.2s;
}
.plugin-card:hover {
border-color: var(--color-primary);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.plugin-card.disabled {
opacity: 0.6;
}
.plugin-card-header {
display: flex;
align-items: flex-start;
gap: 10px;
margin-bottom: 10px;
}
.plugin-card-icon {
font-size: 24px;
color: var(--color-primary);
}
.plugin-card-info {
flex: 1;
}
.plugin-card-title {
font-size: 14px;
font-weight: 600;
color: var(--color-text-primary);
margin-bottom: 2px;
}
.plugin-card-version {
font-size: 11px;
color: var(--color-text-secondary);
}
.plugin-toggle {
background: none;
border: none;
padding: 4px;
cursor: pointer;
color: var(--color-text-secondary);
transition: all 0.2s;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
}
.plugin-toggle:hover {
background: var(--color-bg-hover);
}
.plugin-toggle.enabled {
color: var(--color-success);
}
.plugin-toggle.disabled {
color: var(--color-text-secondary);
}
.plugin-card-description {
font-size: 12px;
color: var(--color-text-secondary);
line-height: 1.5;
margin-bottom: 10px;
}
.plugin-card-footer {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 10px;
border-top: 1px solid var(--color-border-default);
font-size: 11px;
}
.plugin-card-category {
color: var(--color-text-secondary);
}
.plugin-card-installed {
color: var(--color-text-tertiary);
}
/* Plugin List (List View) */
.plugin-list-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 14px;
background: var(--color-bg-base);
border: 1px solid var(--color-border-default);
border-radius: 6px;
transition: all 0.2s;
}
.plugin-list-item:hover {
border-color: var(--color-primary);
}
.plugin-list-item.disabled {
opacity: 0.6;
}
.plugin-list-icon {
font-size: 20px;
color: var(--color-primary);
}
.plugin-list-info {
flex: 1;
}
.plugin-list-name {
display: flex;
align-items: baseline;
gap: 8px;
font-size: 14px;
font-weight: 600;
color: var(--color-text-primary);
margin-bottom: 2px;
}
.plugin-list-version {
font-size: 11px;
font-weight: normal;
color: var(--color-text-secondary);
}
.plugin-list-description {
font-size: 12px;
color: var(--color-text-secondary);
line-height: 1.4;
}
.plugin-list-status {
margin-right: 8px;
}
.status-badge {
padding: 4px 10px;
border-radius: 4px;
font-size: 11px;
font-weight: 500;
}
.status-badge.enabled {
background: rgba(34, 197, 94, 0.15);
color: var(--color-success);
}
.status-badge.disabled {
background: var(--color-bg-overlay);
color: var(--color-text-secondary);
}
.plugin-list-toggle {
padding: 6px 14px;
background: var(--color-bg-overlay);
border: 1px solid var(--color-border-default);
border-radius: 4px;
color: var(--color-text-primary);
font-size: 12px;
cursor: pointer;
transition: all 0.2s;
white-space: nowrap;
}
.plugin-list-toggle:hover {
background: var(--color-primary);
border-color: var(--color-primary);
color: white;
}
/* Scrollbar */
.plugin-content::-webkit-scrollbar {
width: 10px;
}
.plugin-content::-webkit-scrollbar-track {
background: var(--color-bg-elevated);
}
.plugin-content::-webkit-scrollbar-thumb {
background: var(--color-border-default);
border-radius: 5px;
}
.plugin-content::-webkit-scrollbar-thumb:hover {
background: var(--color-text-secondary);
}
/* Plugin Manager Tabs */
.plugin-manager-tabs {
display: flex;
gap: 4px;
padding: 0 20px;
background: var(--color-bg-overlay);
border-bottom: 1px solid var(--color-border-default);
}
.plugin-manager-tab {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 20px;
background: transparent;
border: none;
border-bottom: 2px solid transparent;
color: var(--color-text-secondary);
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.plugin-manager-tab:hover {
color: var(--color-text-primary);
background: rgba(255, 255, 255, 0.05);
}
.plugin-manager-tab.active {
color: var(--color-accent);
border-bottom-color: var(--color-accent);
}
.plugin-publish-btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
background: var(--color-accent, #0e639c);
color: white;
border: none;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.plugin-publish-btn:hover {
background: var(--color-accent-hover, #1177bb);
}
.plugin-card-footer .plugin-publish-btn {
margin-left: auto;
}
.plugin-list-item .plugin-publish-btn {
margin-left: 8px;
}

View File

@@ -1,495 +0,0 @@
.plugin-market-panel {
display: flex;
flex-direction: column;
height: 100%;
background: var(--color-bg-primary, #1e1e1e);
color: var(--color-text-primary, #cccccc);
}
.plugin-market-toolbar {
display: flex;
align-items: center;
gap: 8px;
padding: 0 8px;
border-bottom: 1px solid var(--color-border, #333);
background: var(--color-bg-secondary, #252526);
height: 26px;
}
.plugin-market-search {
flex: 1;
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
background: var(--color-bg-primary, #1e1e1e);
border: 1px solid var(--color-border, #333);
border-radius: 4px;
}
.plugin-market-search input {
flex: 1;
background: transparent;
border: none;
outline: none;
color: var(--color-text-primary, #cccccc);
font-size: 14px;
}
.plugin-market-filters {
display: flex;
gap: 8px;
align-items: center;
}
.plugin-market-filter-select {
padding: 8px 12px;
background: var(--color-bg-primary, #1e1e1e);
border: 1px solid var(--color-border, #333);
border-radius: 4px;
color: var(--color-text-primary, #cccccc);
font-size: 13px;
cursor: pointer;
}
.plugin-market-filter-select:hover {
border-color: var(--color-accent, #0e639c);
}
.plugin-market-refresh {
padding: 8px 12px;
background: var(--color-bg-primary, #1e1e1e);
border: 1px solid var(--color-border, #333);
border-radius: 4px;
color: var(--color-text-primary, #cccccc);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}
.plugin-market-refresh:hover {
background: var(--color-bg-hover, #2d2d30);
}
.plugin-market-publish {
padding: 8px 16px;
background: var(--color-accent, #0e639c);
border: none;
border-radius: 4px;
color: white;
cursor: pointer;
display: flex;
align-items: center;
gap: 6px;
font-size: 13px;
font-weight: 500;
transition: all 0.2s;
}
.plugin-market-publish:hover {
background: var(--color-accent-hover, #1177bb);
}
.plugin-market-content {
flex: 1;
overflow-y: auto;
padding: 16px;
}
.plugin-market-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 16px;
}
.plugin-market-card {
background: var(--color-bg-secondary, #252526);
border: 1px solid var(--color-border, #333);
border-radius: 8px;
padding: 16px;
display: flex;
flex-direction: column;
gap: 12px;
transition: all 0.2s;
}
.plugin-market-card:hover {
border-color: var(--color-accent, #0e639c);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
.plugin-market-card-header {
display: flex;
gap: 12px;
align-items: flex-start;
}
.plugin-market-card-icon {
flex-shrink: 0;
width: 48px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
background: var(--color-accent-bg, rgba(14, 99, 156, 0.1));
border-radius: 8px;
color: var(--color-accent, #0e639c);
}
.plugin-market-card-info {
flex: 1;
min-width: 0;
}
.plugin-market-card-title {
display: flex;
align-items: center;
gap: 6px;
font-size: 16px;
font-weight: 600;
margin-bottom: 4px;
}
.plugin-market-card-title span:first-child {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.plugin-market-badge {
display: inline-flex;
align-items: center;
padding: 2px 6px;
border-radius: 4px;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
}
.plugin-market-badge.official {
background: rgba(52, 199, 89, 0.15);
color: #34c759;
}
.plugin-market-card-meta {
display: flex;
gap: 12px;
font-size: 12px;
color: var(--color-text-secondary, #858585);
}
.plugin-market-card-author {
display: flex;
align-items: center;
gap: 4px;
}
.plugin-market-card-description {
font-size: 13px;
line-height: 1.5;
color: var(--color-text-secondary, #858585);
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
.plugin-market-card-tags {
display: flex;
gap: 6px;
flex-wrap: wrap;
}
.plugin-market-tag {
padding: 4px 8px;
background: var(--color-bg-tertiary, #333);
border-radius: 4px;
font-size: 11px;
color: var(--color-text-secondary, #858585);
}
.plugin-market-card-footer {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 12px;
border-top: 1px solid var(--color-border, #333);
}
.plugin-market-card-link {
display: flex;
align-items: center;
gap: 4px;
font-size: 12px;
color: var(--color-accent, #0e639c);
text-decoration: none;
background: transparent;
border: none;
padding: 0;
cursor: pointer;
transition: opacity 0.2s;
}
.plugin-market-card-link:hover {
opacity: 0.8;
}
.plugin-market-card-actions {
display: flex;
gap: 8px;
}
.plugin-market-btn {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
border: none;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.plugin-market-btn.install {
background: var(--color-accent, #0e639c);
color: white;
}
.plugin-market-btn.install:hover {
background: var(--color-accent-hover, #1177bb);
}
.plugin-market-btn.installed {
background: rgba(52, 199, 89, 0.15);
color: #34c759;
}
.plugin-market-btn.installed:hover {
background: rgba(52, 199, 89, 0.25);
}
.plugin-market-btn.update {
background: rgba(255, 149, 0, 0.15);
color: #ff9500;
}
.plugin-market-btn.update:hover {
background: rgba(255, 149, 0, 0.25);
}
.plugin-market-btn.installing {
background: var(--color-bg-tertiary, #333);
color: var(--color-text-secondary, #858585);
cursor: not-allowed;
}
.plugin-market-loading,
.plugin-market-error,
.plugin-market-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
gap: 16px;
color: var(--color-text-secondary, #858585);
}
.plugin-market-error {
gap: 12px;
max-width: 600px;
margin: 0 auto;
padding: 40px 20px;
text-align: center;
}
.plugin-market-error .error-icon {
color: #ff9500;
margin-bottom: 8px;
}
.plugin-market-error h3 {
font-size: 20px;
font-weight: 600;
color: var(--color-text-primary, #cccccc);
margin: 0;
}
.plugin-market-error .error-description {
font-size: 14px;
color: var(--color-text-secondary, #858585);
line-height: 1.6;
margin: 8px 0;
}
.plugin-market-error .error-details {
background: rgba(255, 59, 48, 0.1);
border: 1px solid rgba(255, 59, 48, 0.3);
border-radius: 6px;
padding: 12px 16px;
margin: 16px 0;
max-width: 100%;
}
.plugin-market-error .error-message {
font-size: 12px;
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
color: #ff3b30;
margin: 0;
word-break: break-word;
}
.plugin-market-error .retry-button,
.plugin-market-loading button {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 20px;
background: var(--color-accent, #0e639c);
color: white;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
margin-top: 8px;
}
.plugin-market-error .retry-button:hover,
.plugin-market-loading button:hover {
background: var(--color-accent-hover, #1177bb);
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(14, 99, 156, 0.3);
}
.spinning {
animation: spin 1s linear infinite;
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.plugin-market-direct-source-toggle {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 14px;
background: rgba(14, 99, 156, 0.1);
border: 1px solid rgba(14, 99, 156, 0.3);
border-radius: 6px;
font-size: 13px;
font-weight: 500;
color: var(--color-accent, #0e639c);
cursor: pointer;
transition: all 0.2s ease;
user-select: none;
}
.plugin-market-direct-source-toggle:hover {
background: rgba(14, 99, 156, 0.15);
border-color: rgba(14, 99, 156, 0.5);
transform: translateY(-1px);
box-shadow: 0 2px 6px rgba(14, 99, 156, 0.2);
}
.plugin-market-direct-source-toggle input[type="checkbox"] {
position: relative;
width: 38px;
height: 20px;
margin: 0;
cursor: pointer;
appearance: none;
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 10px;
transition: all 0.3s ease;
}
.plugin-market-direct-source-toggle input[type="checkbox"]:checked {
background: var(--color-accent, #0e639c);
border-color: var(--color-accent, #0e639c);
}
.plugin-market-direct-source-toggle input[type="checkbox"]::before {
content: '';
position: absolute;
width: 16px;
height: 16px;
top: 1px;
left: 1px;
background: white;
border-radius: 50%;
transition: all 0.3s ease;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
.plugin-market-direct-source-toggle input[type="checkbox"]:checked::before {
left: 19px;
}
.plugin-market-direct-source-toggle .toggle-label {
white-space: nowrap;
font-weight: 500;
}
/* 版本选择器 */
.plugin-market-version-select {
padding: 2px 6px;
background: var(--color-bg-tertiary, #333);
border: 1px solid var(--color-border, #333);
border-radius: 3px;
color: var(--color-text-primary, #cccccc);
font-size: 11px;
cursor: pointer;
transition: all 0.2s;
}
.plugin-market-version-select:hover {
border-color: var(--color-accent, #0e639c);
background: var(--color-bg-hover, #2d2d30);
}
.plugin-market-version-select:focus {
outline: none;
border-color: var(--color-accent, #0e639c);
}
/* 更新日志 */
.plugin-market-version-changes {
margin: 8px 0;
padding: 8px;
background: rgba(14, 99, 156, 0.1);
border: 1px solid rgba(14, 99, 156, 0.2);
border-radius: 4px;
}
.plugin-market-version-changes summary {
cursor: pointer;
font-size: 12px;
font-weight: 500;
color: var(--color-accent, #0e639c);
padding: 4px;
user-select: none;
}
.plugin-market-version-changes summary:hover {
opacity: 0.8;
}
.plugin-market-version-changes p {
margin: 8px 0 0 0;
padding-left: 4px;
font-size: 12px;
line-height: 1.5;
color: var(--color-text-secondary, #858585);
}

View File

@@ -1,382 +0,0 @@
.plugin-panel {
display: flex;
flex-direction: column;
height: 100%;
background: var(--color-bg-secondary);
color: var(--color-text-primary);
}
.plugin-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 8px;
background: var(--color-bg-tertiary);
border-bottom: 1px solid var(--color-border);
gap: 8px;
height: 26px;
}
.plugin-toolbar-left,
.plugin-toolbar-right {
display: flex;
align-items: center;
gap: 8px;
}
.plugin-search {
display: flex;
align-items: center;
gap: 6px;
padding: 4px 8px;
background: var(--color-bg-primary);
border: 1px solid var(--color-border);
border-radius: 4px;
color: var(--color-text-secondary);
}
.plugin-search input {
border: none;
background: none;
outline: none;
color: var(--color-text-primary);
font-size: 12px;
min-width: 200px;
}
.plugin-stats {
display: flex;
align-items: center;
gap: 12px;
padding: 0 8px;
font-size: 12px;
}
.plugin-stats .stat-item {
display: flex;
align-items: center;
gap: 4px;
}
.plugin-stats .stat-item.enabled {
color: var(--color-success);
}
.plugin-stats .stat-item.disabled {
color: var(--color-text-secondary);
}
.plugin-view-mode {
display: flex;
gap: 2px;
background: var(--color-bg-primary);
border: 1px solid var(--color-border);
border-radius: 4px;
overflow: hidden;
}
.plugin-view-mode button {
padding: 4px 8px;
background: none;
border: none;
color: var(--color-text-secondary);
cursor: pointer;
transition: all 0.2s;
}
.plugin-view-mode button:hover {
background: var(--color-bg-hover);
}
.plugin-view-mode button.active {
background: var(--color-primary);
color: white;
}
.plugin-content {
flex: 1;
overflow-y: auto;
padding: 16px;
}
.plugin-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
color: var(--color-text-secondary);
gap: 16px;
}
.plugin-categories {
display: flex;
flex-direction: column;
gap: 16px;
}
.plugin-category {
background: var(--color-bg-tertiary);
border: 1px solid var(--color-border);
border-radius: 6px;
overflow: hidden;
}
.plugin-category-header {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 12px;
background: var(--color-bg-secondary);
cursor: pointer;
user-select: none;
transition: background 0.2s;
}
.plugin-category-header:hover {
background: var(--color-bg-hover);
}
.plugin-category-toggle {
background: none;
border: none;
padding: 0;
color: var(--color-text-secondary);
cursor: pointer;
display: flex;
align-items: center;
}
.plugin-category-icon {
font-size: 18px;
}
.plugin-category-name {
flex: 1;
font-size: 13px;
font-weight: 600;
color: var(--color-text-primary);
}
.plugin-category-count {
font-size: 12px;
color: var(--color-text-secondary);
background: var(--color-bg-tertiary);
padding: 2px 8px;
border-radius: 10px;
}
.plugin-category-content {
padding: 12px;
}
.plugin-category-content.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 12px;
}
.plugin-category-content.list {
display: flex;
flex-direction: column;
gap: 8px;
}
/* Plugin Card (Grid View) */
.plugin-card {
background: var(--color-bg-primary);
border: 1px solid var(--color-border);
border-radius: 6px;
padding: 12px;
transition: all 0.2s;
}
.plugin-card:hover {
border-color: var(--color-primary);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.plugin-card.disabled {
opacity: 0.6;
}
.plugin-card-header {
display: flex;
align-items: flex-start;
gap: 10px;
margin-bottom: 10px;
}
.plugin-card-icon {
font-size: 24px;
color: var(--color-primary);
}
.plugin-card-info {
flex: 1;
}
.plugin-card-title {
font-size: 14px;
font-weight: 600;
color: var(--color-text-primary);
margin-bottom: 2px;
}
.plugin-card-version {
font-size: 11px;
color: var(--color-text-secondary);
}
.plugin-toggle {
background: none;
border: none;
padding: 4px;
cursor: pointer;
color: var(--color-text-secondary);
transition: all 0.2s;
border-radius: 4px;
}
.plugin-toggle:hover {
background: var(--color-bg-hover);
}
.plugin-toggle.enabled {
color: var(--color-success);
}
.plugin-toggle.disabled {
color: var(--color-text-secondary);
}
.plugin-card-description {
font-size: 12px;
color: var(--color-text-secondary);
line-height: 1.5;
margin-bottom: 10px;
}
.plugin-card-footer {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 10px;
border-top: 1px solid var(--color-border);
font-size: 11px;
}
.plugin-card-category {
color: var(--color-text-secondary);
}
.plugin-card-installed {
color: var(--color-text-tertiary);
}
/* Plugin List (List View) */
.plugin-list-item {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 12px;
background: var(--color-bg-primary);
border: 1px solid var(--color-border);
border-radius: 6px;
transition: all 0.2s;
}
.plugin-list-item:hover {
border-color: var(--color-primary);
}
.plugin-list-item.disabled {
opacity: 0.6;
}
.plugin-list-icon {
font-size: 20px;
color: var(--color-primary);
}
.plugin-list-info {
flex: 1;
}
.plugin-list-name {
display: flex;
align-items: baseline;
gap: 8px;
font-size: 13px;
font-weight: 600;
color: var(--color-text-primary);
margin-bottom: 2px;
}
.plugin-list-version {
font-size: 11px;
font-weight: normal;
color: var(--color-text-secondary);
}
.plugin-list-description {
font-size: 12px;
color: var(--color-text-secondary);
line-height: 1.4;
}
.plugin-list-status {
margin-right: 8px;
}
.status-badge {
padding: 3px 8px;
border-radius: 4px;
font-size: 11px;
font-weight: 500;
}
.status-badge.enabled {
background: rgba(34, 197, 94, 0.15);
color: var(--color-success);
}
.status-badge.disabled {
background: var(--color-bg-tertiary);
color: var(--color-text-secondary);
}
.plugin-list-toggle {
padding: 6px 12px;
background: var(--color-bg-tertiary);
border: 1px solid var(--color-border);
border-radius: 4px;
color: var(--color-text-primary);
font-size: 12px;
cursor: pointer;
transition: all 0.2s;
}
.plugin-list-toggle:hover {
background: var(--color-primary);
border-color: var(--color-primary);
color: white;
}
/* Scrollbar */
.plugin-content::-webkit-scrollbar {
width: 8px;
}
.plugin-content::-webkit-scrollbar-track {
background: var(--color-bg-secondary);
}
.plugin-content::-webkit-scrollbar-thumb {
background: var(--color-border);
border-radius: 4px;
}
.plugin-content::-webkit-scrollbar-thumb:hover {
background: var(--color-text-secondary);
}

View File

@@ -1,861 +0,0 @@
/* 统一滚动条样式 */
.plugin-publish-wizard ::-webkit-scrollbar {
width: 8px;
height: 8px;
}
.plugin-publish-wizard ::-webkit-scrollbar-track {
background: var(--color-bg-primary, #1e1e1e);
border-radius: 4px;
}
.plugin-publish-wizard ::-webkit-scrollbar-thumb {
background: var(--color-border, #333);
border-radius: 4px;
transition: background 0.2s;
}
.plugin-publish-wizard ::-webkit-scrollbar-thumb:hover {
background: var(--color-text-secondary, #858585);
}
.plugin-publish-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: var(--z-index-modal);
}
.plugin-publish-wizard {
background: var(--color-bg-primary, #1e1e1e);
border-radius: 12px;
width: 90%;
max-width: 600px;
max-height: 80vh;
display: flex;
flex-direction: column;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
}
.plugin-publish-wizard.inline {
width: 100%;
max-width: none;
max-height: 100%;
height: 100%;
border-radius: 0;
box-shadow: none;
background: transparent;
overflow: hidden;
}
.plugin-publish-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 24px;
border-bottom: 1px solid var(--color-border, #333);
}
.plugin-publish-wizard.inline .plugin-publish-header {
padding: 16px 20px;
background: var(--color-bg-secondary, #252526);
}
.plugin-publish-header h2 {
margin: 0;
font-size: 18px;
font-weight: 600;
color: var(--color-text-primary, #cccccc);
}
.plugin-publish-close {
background: none;
border: none;
cursor: pointer;
color: var(--color-text-secondary, #858585);
padding: 4px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
transition: all 0.2s;
}
.plugin-publish-close:hover {
background: var(--color-bg-hover, #2d2d30);
color: var(--color-text-primary, #cccccc);
}
.plugin-publish-content {
flex: 1;
overflow-y: auto;
padding: 24px;
}
.plugin-publish-wizard.inline .plugin-publish-content {
padding: 20px;
background: var(--color-bg-primary, #1e1e1e);
height: 0;
min-height: 0;
}
.publish-step {
display: flex;
flex-direction: column;
gap: 20px;
}
.publish-step h3 {
margin: 0 0 12px 0;
font-size: 16px;
font-weight: 600;
color: var(--color-text-primary, #cccccc);
}
.github-auth {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
padding: 20px;
}
.form-group {
display: flex;
flex-direction: column;
gap: 8px;
width: 100%;
}
.form-group label {
font-size: 13px;
font-weight: 500;
color: var(--color-text-primary, #cccccc);
}
.form-group input,
.form-group textarea,
.form-group select {
padding: 8px 12px;
background: var(--color-bg-secondary, #252526);
border: 1px solid var(--color-border, #333);
border-radius: 4px;
color: var(--color-text-primary, #cccccc);
font-size: 13px;
outline: none;
transition: border-color 0.2s;
}
.form-group input:focus,
.form-group textarea:focus,
.form-group select:focus {
border-color: var(--color-accent, #0e639c);
}
.form-group small {
font-size: 12px;
color: var(--color-text-secondary, #858585);
}
.form-group textarea {
resize: vertical;
min-height: 80px;
font-family: inherit;
}
.btn-primary,
.btn-secondary,
.btn-link {
padding: 8px 16px;
border-radius: 4px;
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
}
.btn-primary {
background: var(--color-accent, #0e639c);
color: white;
border: none;
}
.btn-primary:hover {
background: var(--color-accent-hover, #1177bb);
}
.btn-secondary {
background: var(--color-bg-secondary, #252526);
color: var(--color-text-primary, #cccccc);
border: 1px solid var(--color-border, #333);
}
.btn-secondary:hover {
background: var(--color-bg-hover, #2d2d30);
}
.btn-link {
background: none;
color: var(--color-accent, #0e639c);
border: none;
padding: 6px 12px;
}
.btn-link:hover {
opacity: 0.8;
}
.button-group {
display: flex;
gap: 12px;
justify-content: flex-end;
}
.error-message {
display: flex;
align-items: center;
gap: 8px;
padding: 12px;
background: rgba(255, 59, 48, 0.1);
border: 1px solid rgba(255, 59, 48, 0.3);
border-radius: 4px;
color: #ff3b30;
font-size: 13px;
}
.confirm-details {
background: var(--color-bg-secondary, #252526);
border-radius: 8px;
padding: 16px;
display: flex;
flex-direction: column;
gap: 12px;
}
.detail-row {
display: flex;
gap: 12px;
}
.detail-label {
font-weight: 600;
color: var(--color-text-secondary, #858585);
min-width: 120px;
}
.detail-value {
color: var(--color-text-primary, #cccccc);
word-break: break-all;
}
.publish-step.publishing,
.publish-step.success,
.publish-step.error {
align-items: center;
text-align: center;
padding: 40px 20px;
}
.publish-step.publishing svg,
.publish-step.success svg,
.publish-step.error svg {
margin-bottom: 16px;
}
.review-message {
color: var(--color-text-secondary, #858585);
font-size: 13px;
line-height: 1.5;
max-width: 400px;
margin: 16px 0;
}
.spinning {
animation: spin 1s linear infinite;
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
/* OAuth Authentication Styles */
.auth-tabs {
display: flex;
gap: 8px;
margin: 16px 0;
width: 100%;
}
.auth-tab {
flex: 1;
padding: 10px 16px;
background: var(--color-bg-secondary, #252526);
border: 1px solid var(--color-border, #333);
border-radius: 6px;
color: var(--color-text-secondary, #858585);
cursor: pointer;
font-size: 13px;
font-weight: 500;
transition: all 0.2s;
}
.auth-tab:hover {
background: var(--color-bg-hover, #2d2d30);
border-color: var(--color-accent, #0e639c);
}
.auth-tab.active {
background: var(--color-accent, #0e639c);
border-color: var(--color-accent, #0e639c);
color: white;
}
.oauth-auth,
.token-auth {
display: flex;
flex-direction: column;
gap: 16px;
width: 100%;
align-items: center;
}
.oauth-instructions {
text-align: left;
width: 100%;
padding: 16px;
background: var(--color-bg-secondary, #252526);
border-radius: 8px;
border: 1px solid var(--color-border, #333);
}
.oauth-instructions p {
margin: 8px 0;
font-size: 13px;
color: var(--color-text-secondary, #858585);
line-height: 1.6;
}
.oauth-pending,
.oauth-success,
.oauth-error {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
padding: 32px 16px;
width: 100%;
}
.oauth-pending h4,
.oauth-success h4,
.oauth-error h4 {
margin: 0;
font-size: 16px;
font-weight: 600;
}
.user-code-display {
display: flex;
flex-direction: column;
gap: 12px;
width: 100%;
max-width: 400px;
margin-top: 16px;
}
.user-code-display label {
font-size: 13px;
font-weight: 500;
color: var(--color-text-secondary, #858585);
}
.code-box {
display: flex;
align-items: center;
gap: 12px;
padding: 16px;
background: var(--color-bg-secondary, #252526);
border: 2px solid var(--color-accent, #0e639c);
border-radius: 8px;
}
.code-text {
flex: 1;
font-size: 24px;
font-weight: 700;
font-family: 'Courier New', monospace;
letter-spacing: 4px;
color: var(--color-accent, #0e639c);
text-align: center;
}
.btn-copy {
background: transparent;
border: none;
font-size: 20px;
cursor: pointer;
padding: 4px 8px;
border-radius: 4px;
transition: background 0.2s;
}
.btn-copy:hover {
background: var(--color-bg-hover, #2d2d30);
}
.error-details {
width: 100%;
max-width: 500px;
max-height: 200px;
overflow-y: auto;
background: var(--color-bg-secondary, #252526);
border: 1px solid #ff3b30;
border-radius: 8px;
padding: 12px;
margin: 12px 0;
}
.error-details pre {
margin: 0;
font-size: 12px;
font-family: 'Courier New', monospace;
color: #ff3b30;
white-space: pre-wrap;
word-break: break-word;
}
/* 发布进度样式 */
.publish-progress {
display: flex;
flex-direction: column;
gap: 12px;
margin-top: 20px;
padding: 16px;
background: var(--color-bg-secondary, #252526);
border-radius: 8px;
}
.progress-bar {
width: 100%;
height: 8px;
background: var(--color-bg-hover, #2d2d30);
border-radius: 4px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #007acc 0%, #4fc3f7 100%);
transition: width 0.3s ease;
}
.progress-message {
margin: 0;
font-size: 14px;
color: var(--color-text-primary, #cccccc);
text-align: center;
}
.progress-percent {
margin: 0;
font-size: 12px;
color: var(--color-text-secondary, #858585);
text-align: center;
font-family: monospace;
}
.build-log {
width: 100%;
max-height: 300px;
overflow-y: auto;
background: var(--color-bg-hover, #2d2d30);
border: 1px solid var(--color-border, #333);
border-radius: 4px;
padding: 12px;
margin-top: 16px;
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
font-size: 12px;
line-height: 1.5;
}
.log-line {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 4px;
color: var(--color-text-primary, #cccccc);
word-break: break-all;
}
.log-line svg {
flex-shrink: 0;
}
/* 现有 PR 提示框 */
.existing-pr-notice {
display: flex;
gap: 12px;
padding: 16px;
margin: 16px 0;
background: rgba(244, 180, 0, 0.1);
border: 1px solid rgba(244, 180, 0, 0.3);
border-radius: 8px;
color: var(--color-text-primary, #cccccc);
}
.existing-pr-notice svg {
color: #f4b400;
flex-shrink: 0;
margin-top: 2px;
}
.existing-pr-notice .notice-content {
flex: 1;
}
.existing-pr-notice strong {
display: block;
margin-bottom: 8px;
color: #f4b400;
font-size: 14px;
}
.existing-pr-notice p {
margin: 0 0 12px 0;
font-size: 13px;
line-height: 1.5;
color: var(--color-text-secondary, #858585);
}
.existing-pr-notice .btn-link {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
background: rgba(74, 158, 255, 0.15);
border: 1px solid rgba(74, 158, 255, 0.3);
border-radius: 6px;
color: #4a9eff;
font-size: 13px;
cursor: pointer;
transition: all 0.2s;
}
.existing-pr-notice .btn-link:hover {
background: rgba(74, 158, 255, 0.25);
border-color: #4a9eff;
}
/* 版本信息样式 */
.version-info {
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: 8px;
padding: 12px;
background: rgba(52, 199, 89, 0.1);
border: 1px solid rgba(52, 199, 89, 0.3);
border-radius: 6px;
}
.version-notice {
display: flex;
align-items: center;
gap: 8px;
color: #34c759;
font-size: 13px;
font-weight: 500;
}
.btn-version-suggest {
align-self: flex-start;
padding: 6px 12px;
background: rgba(14, 99, 156, 0.15);
border: 1px solid rgba(14, 99, 156, 0.3);
border-radius: 4px;
color: var(--color-accent, #0e639c);
font-size: 12px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.btn-version-suggest:hover {
background: rgba(14, 99, 156, 0.25);
border-color: var(--color-accent, #0e639c);
transform: translateY(-1px);
}
.version-history {
margin-top: 8px;
padding: 12px;
background: var(--color-bg-secondary, #252526);
border: 1px solid var(--color-border, #333);
border-radius: 6px;
}
.version-history summary {
cursor: pointer;
font-size: 13px;
font-weight: 500;
color: var(--color-text-primary, #cccccc);
padding: 4px;
user-select: none;
}
.version-history summary:hover {
color: var(--color-accent, #0e639c);
}
.version-history ul {
list-style: none;
padding: 0;
margin: 12px 0 0 0;
display: flex;
flex-direction: column;
gap: 4px;
}
.version-history li {
padding: 6px 12px;
background: var(--color-bg-primary, #1e1e1e);
border-radius: 4px;
font-size: 12px;
font-family: 'Consolas', 'Monaco', monospace;
color: var(--color-text-secondary, #858585);
}
/* 插件源选择样式 */
.source-type-selection {
display: flex;
flex-direction: column;
gap: 12px;
margin: 20px 0;
}
.source-type-btn {
display: flex;
align-items: flex-start;
gap: 16px;
padding: 20px;
background: var(--color-bg-secondary, #252526);
border: 2px solid var(--color-border, #333);
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
text-align: left;
width: 100%;
}
.source-type-btn:hover {
background: var(--color-bg-hover, #2d2d30);
border-color: var(--color-accent, #0e639c);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}
.source-type-btn.active {
background: rgba(14, 99, 156, 0.15);
border-color: var(--color-accent, #0e639c);
box-shadow: 0 0 0 3px rgba(14, 99, 156, 0.1);
}
.source-type-btn svg {
color: var(--color-accent, #0e639c);
flex-shrink: 0;
margin-top: 2px;
}
.source-type-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 6px;
}
.source-type-info strong {
font-size: 14px;
font-weight: 600;
color: var(--color-text-primary, #cccccc);
}
.source-type-info p {
margin: 0;
font-size: 12px;
line-height: 1.5;
color: var(--color-text-secondary, #858585);
}
.selected-source {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 16px;
background: rgba(52, 199, 89, 0.1);
border: 1px solid rgba(52, 199, 89, 0.3);
border-radius: 8px;
margin-top: 16px;
}
.selected-source svg {
color: #34c759;
flex-shrink: 0;
margin-top: 2px;
}
.source-details {
flex: 1;
display: flex;
flex-direction: column;
gap: 4px;
}
.source-path {
font-size: 12px;
color: var(--color-text-secondary, #858585);
word-break: break-all;
font-family: 'Consolas', 'Monaco', monospace;
}
.source-name {
font-size: 14px;
font-weight: 600;
color: #34c759;
}
/* ZIP 文件要求说明 */
.zip-requirements-details {
margin-top: 20px;
padding: 16px;
background: var(--color-bg-secondary, #252526);
border: 1px solid var(--color-border, #333);
border-radius: 8px;
}
.zip-requirements-details summary {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
font-size: 14px;
font-weight: 600;
color: var(--color-text-primary, #cccccc);
padding: 8px;
user-select: none;
list-style: none;
transition: color 0.2s;
}
.zip-requirements-details summary::-webkit-details-marker {
display: none;
}
.zip-requirements-details summary:hover {
color: var(--color-accent, #0e639c);
}
.zip-requirements-details summary svg {
color: #f4b400;
flex-shrink: 0;
}
.zip-requirements-details[open] summary {
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px solid var(--color-border, #333);
}
.zip-requirements-content {
display: flex;
flex-direction: column;
gap: 20px;
padding: 4px 8px;
}
.requirement-section h4 {
margin: 0 0 8px 0;
font-size: 13px;
font-weight: 600;
color: var(--color-accent, #0e639c);
}
.requirement-section p {
margin: 0 0 12px 0;
font-size: 13px;
line-height: 1.5;
color: var(--color-text-secondary, #858585);
}
.requirement-section ul {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
gap: 8px;
}
.requirement-section li {
display: flex;
align-items: flex-start;
gap: 8px;
font-size: 13px;
line-height: 1.5;
color: var(--color-text-primary, #cccccc);
}
.requirement-section li::before {
content: '✓';
color: #34c759;
font-weight: bold;
flex-shrink: 0;
}
.requirement-section code {
padding: 2px 6px;
background: var(--color-bg-primary, #1e1e1e);
border: 1px solid var(--color-border, #333);
border-radius: 3px;
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
font-size: 12px;
color: #4ec9b0;
}
.build-script-example {
margin: 0;
padding: 12px;
background: var(--color-bg-primary, #1e1e1e);
border: 1px solid var(--color-border, #333);
border-radius: 4px;
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
font-size: 12px;
line-height: 1.6;
color: var(--color-text-secondary, #858585);
overflow-x: auto;
}
.recommendation-notice {
padding: 12px 16px;
background: rgba(14, 99, 156, 0.1);
border: 1px solid rgba(14, 99, 156, 0.3);
border-radius: 6px;
font-size: 13px;
line-height: 1.5;
color: var(--color-accent, #0e639c);
text-align: center;
}

View File

@@ -1,287 +0,0 @@
.plugin-update-dialog-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: var(--z-index-modal);
}
.plugin-update-dialog {
background: var(--color-bg-primary, #1e1e1e);
border: 1px solid var(--color-border, #333);
border-radius: 8px;
width: 90%;
max-width: 600px;
max-height: 80vh;
display: flex;
flex-direction: column;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
}
.update-dialog-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
border-bottom: 1px solid var(--color-border, #333);
}
.update-dialog-header h2 {
margin: 0;
font-size: 18px;
font-weight: 600;
}
.update-dialog-close {
background: none;
border: none;
padding: 4px;
cursor: pointer;
color: var(--color-text-secondary, #888);
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
}
.update-dialog-close:hover {
background: var(--color-bg-hover, rgba(255, 255, 255, 0.1));
color: var(--color-text-primary, #fff);
}
.update-dialog-content {
flex: 1;
overflow-y: auto;
padding: 20px;
}
.update-dialog-step {
display: flex;
flex-direction: column;
gap: 16px;
}
.update-dialog-step h3 {
margin: 0;
font-size: 16px;
font-weight: 600;
}
.step-description {
color: var(--color-text-secondary, #888);
margin: 0;
}
.current-plugin-info {
padding: 12px;
background: rgba(14, 99, 156, 0.1);
border: 1px solid rgba(14, 99, 156, 0.3);
border-radius: 6px;
}
.current-plugin-info h4 {
margin: 0 0 8px 0;
font-size: 14px;
}
.current-plugin-info p {
margin: 0;
color: var(--color-text-secondary, #888);
}
.selected-folder-info {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
background: var(--color-bg-secondary, #252525);
border: 1px solid var(--color-border, #333);
border-radius: 4px;
font-size: 13px;
color: var(--color-text-secondary, #888);
}
.form-group {
display: flex;
flex-direction: column;
gap: 8px;
}
.form-group label {
font-size: 13px;
font-weight: 500;
}
.form-group input,
.form-group textarea {
padding: 8px 12px;
background: var(--color-bg-secondary, #252525);
border: 1px solid var(--color-border, #333);
border-radius: 4px;
color: var(--color-text-primary, #fff);
font-size: 13px;
font-family: inherit;
}
.form-group textarea {
resize: vertical;
}
.version-input-group {
display: flex;
gap: 8px;
}
.version-input-group input {
flex: 1;
}
.btn-browse,
.btn-suggest,
.btn-view-pr,
.btn-close,
.btn-back,
.btn-primary {
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 13px;
font-weight: 500;
display: inline-flex;
align-items: center;
gap: 6px;
transition: all 0.2s;
}
.btn-browse {
background: var(--color-accent, #0e639c);
color: white;
}
.btn-browse:hover {
background: var(--color-accent-hover, #0d5a8c);
}
.btn-suggest {
background: rgba(14, 99, 156, 0.15);
color: var(--color-accent, #0e639c);
border: 1px solid rgba(14, 99, 156, 0.3);
white-space: nowrap;
}
.btn-suggest:hover {
background: rgba(14, 99, 156, 0.25);
}
.update-dialog-actions {
display: flex;
gap: 8px;
justify-content: flex-end;
margin-top: 8px;
}
.btn-back {
background: var(--color-bg-secondary, #252525);
color: var(--color-text-primary, #fff);
border: 1px solid var(--color-border, #333);
}
.btn-back:hover {
background: var(--color-bg-hover, rgba(255, 255, 255, 0.1));
}
.btn-primary {
background: var(--color-accent, #0e639c);
color: white;
}
.btn-primary:hover:not(:disabled) {
background: var(--color-accent-hover, #0d5a8c);
}
.btn-primary:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.progress-container {
display: flex;
flex-direction: column;
gap: 8px;
}
.progress-bar {
width: 100%;
height: 8px;
background: var(--color-bg-secondary, #252525);
border-radius: 4px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: var(--color-accent, #0e639c);
transition: width 0.3s ease;
}
.progress-message {
margin: 0;
font-size: 13px;
color: var(--color-text-secondary, #888);
}
.build-log {
margin-top: 12px;
padding: 12px;
background: var(--color-bg-secondary, #252525);
border: 1px solid var(--color-border, #333);
border-radius: 4px;
max-height: 300px;
overflow-y: auto;
font-family: 'Courier New', monospace;
font-size: 12px;
}
.log-line {
margin-bottom: 4px;
color: var(--color-text-secondary, #888);
}
.success-step,
.error-step {
align-items: center;
text-align: center;
padding: 20px;
}
.success-icon {
color: var(--color-success, #52c41a);
}
.error-icon {
color: var(--color-error, #ff4d4f);
}
.success-message,
.error-message {
margin: 16px 0;
color: var(--color-text-secondary, #888);
}
.btn-view-pr,
.btn-close {
background: var(--color-accent, #0e639c);
color: white;
margin-top: 8px;
}
.btn-view-pr:hover,
.btn-close:hover {
background: var(--color-accent-hover, #0d5a8c);
}

View File

@@ -1,278 +0,0 @@
.profiler-dock-panel {
display: flex;
flex-direction: column;
height: 100%;
background: var(--color-bg-elevated);
overflow: hidden;
}
.profiler-dock-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 8px;
border-bottom: 1px solid var(--color-border-default);
flex-shrink: 0;
height: 26px;
}
.profiler-dock-header h3 {
margin: 0;
font-size: 13px;
font-weight: 600;
color: var(--color-text-primary);
}
.profiler-dock-header-actions {
display: flex;
align-items: center;
gap: 8px;
}
.profiler-dock-pause-btn,
.profiler-dock-details-btn {
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
padding: 0;
background: var(--color-bg-inset);
border: 1px solid var(--color-border-default);
border-radius: var(--radius-sm);
color: var(--color-text-secondary);
cursor: pointer;
transition: all var(--transition-fast);
}
.profiler-dock-pause-btn:hover,
.profiler-dock-details-btn:hover {
background: var(--color-bg-hover);
border-color: var(--color-border-strong);
color: var(--color-text-primary);
}
.profiler-dock-pause-btn:active,
.profiler-dock-details-btn:active {
transform: scale(0.95);
}
.profiler-dock-status {
display: flex;
align-items: center;
gap: 6px;
font-size: 11px;
padding: 4px 8px;
border-radius: var(--radius-sm);
background: var(--color-bg-inset);
}
.status-text {
font-weight: 500;
}
.status-text.connected {
color: var(--color-success);
}
.status-text.waiting {
color: var(--color-warning);
}
.status-text.disconnected {
color: var(--color-text-tertiary);
}
.profiler-dock-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
flex: 1;
padding: 24px;
text-align: center;
color: var(--color-text-tertiary);
gap: 12px;
}
.profiler-dock-empty p {
margin: 0;
font-size: 13px;
}
.profiler-dock-empty .hint {
font-size: 11px;
opacity: 0.7;
}
.profiler-dock-content {
flex: 1;
overflow-y: auto;
padding: 16px;
display: flex;
flex-direction: column;
gap: 16px;
}
.profiler-dock-content::-webkit-scrollbar {
width: 6px;
}
.profiler-dock-content::-webkit-scrollbar-track {
background: transparent;
}
.profiler-dock-content::-webkit-scrollbar-thumb {
background: var(--color-border-default);
border-radius: 3px;
}
.profiler-dock-stats {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
}
.stat-card {
display: flex;
align-items: center;
gap: 12px;
padding: 12px;
background: var(--color-bg-inset);
border-radius: var(--radius-md);
border: 1px solid var(--color-border-default);
transition: all var(--transition-fast);
}
.stat-card:hover {
border-color: var(--color-border-strong);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.stat-icon {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border-radius: var(--radius-sm);
background: rgba(99, 102, 241, 0.1);
color: rgb(99, 102, 241);
flex-shrink: 0;
}
.stat-info {
flex: 1;
min-width: 0;
}
.stat-label {
font-size: 10px;
color: var(--color-text-tertiary);
text-transform: uppercase;
letter-spacing: 0.5px;
font-weight: 500;
margin-bottom: 2px;
}
.stat-value {
font-size: 18px;
font-weight: 700;
color: var(--color-text-primary);
font-family: var(--font-family-mono);
}
.stat-value.warning {
color: var(--color-warning);
}
.profiler-dock-systems {
display: flex;
flex-direction: column;
gap: 12px;
}
.profiler-dock-systems h4 {
margin: 0;
font-size: 12px;
font-weight: 600;
color: var(--color-text-secondary);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.systems-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.system-item {
padding: 10px 12px;
background: var(--color-bg-inset);
border-radius: var(--radius-md);
border: 1px solid var(--color-border-default);
transition: all var(--transition-fast);
}
.system-item:hover {
border-color: var(--color-border-strong);
}
.system-item-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 6px;
}
.system-item-name {
font-size: 11px;
font-weight: 500;
color: var(--color-text-primary);
font-family: var(--font-family-mono);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.system-item-time {
font-size: 11px;
font-weight: 600;
color: var(--color-text-primary);
font-family: var(--font-family-mono);
flex-shrink: 0;
margin-left: 8px;
}
.system-item-bar {
width: 100%;
height: 4px;
background: var(--color-bg-elevated);
border-radius: 2px;
overflow: hidden;
margin-bottom: 6px;
}
.system-item-bar-fill {
height: 100%;
transition: width 0.3s ease;
border-radius: 2px;
}
.system-item-footer {
display: flex;
align-items: center;
justify-content: space-between;
font-size: 10px;
color: var(--color-text-tertiary);
}
.system-item-percentage {
font-weight: 600;
font-family: var(--font-family-mono);
}
.system-item-entities {
font-size: 9px;
}

View File

@@ -1,304 +0,0 @@
.profiler-panel {
display: flex;
flex-direction: column;
height: 100%;
background: var(--color-bg-base);
}
.profiler-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;
}
.profiler-toolbar-left {
display: flex;
align-items: center;
gap: 12px;
flex: 1;
}
.profiler-toolbar-right {
display: flex;
align-items: center;
gap: 8px;
}
.profiler-stats-summary {
display: flex;
align-items: center;
gap: 20px;
}
.summary-item {
display: flex;
align-items: center;
gap: 6px;
font-size: 12px;
color: var(--color-text-secondary);
}
.summary-item svg {
color: var(--color-primary);
}
.summary-label {
font-weight: 500;
}
.summary-value {
font-family: var(--font-family-mono);
font-weight: 600;
color: var(--color-text-primary);
}
.summary-value.over-budget {
color: var(--color-danger);
}
.summary-value.low-fps {
color: var(--color-warning);
}
.profiler-sort {
padding: 4px 8px;
background: var(--color-bg-inset);
border: 1px solid var(--color-border-default);
border-radius: var(--radius-sm);
color: var(--color-text-primary);
font-size: 11px;
cursor: pointer;
outline: none;
transition: all var(--transition-fast);
}
.profiler-sort:hover {
border-color: var(--color-primary);
}
.profiler-sort:focus {
border-color: var(--color-primary);
}
.profiler-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);
}
.profiler-btn:hover {
background: var(--color-bg-hover);
color: var(--color-text-primary);
}
.profiler-content {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
padding: 12px;
}
.profiler-content::-webkit-scrollbar {
width: 8px;
}
.profiler-content::-webkit-scrollbar-track {
background: var(--color-bg-elevated);
}
.profiler-content::-webkit-scrollbar-thumb {
background: var(--color-border-default);
border-radius: 4px;
}
.profiler-content::-webkit-scrollbar-thumb:hover {
background: var(--color-text-secondary);
}
.profiler-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
color: var(--color-text-tertiary);
gap: 12px;
}
.profiler-empty p {
margin: 0;
font-size: 13px;
}
.profiler-empty-hint {
font-size: 11px !important;
opacity: 0.7;
}
.profiler-systems {
display: flex;
flex-direction: column;
gap: 12px;
}
.system-row {
background: var(--color-bg-elevated);
border: 1px solid var(--color-border-default);
border-radius: var(--radius-md);
padding: 12px;
transition: all var(--transition-fast);
}
.system-row:hover {
border-color: var(--color-border-strong);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.system-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
}
.system-info {
display: flex;
align-items: center;
gap: 8px;
flex: 1;
}
.system-rank {
display: inline-flex;
align-items: center;
justify-content: center;
width: 28px;
height: 22px;
background: var(--color-bg-inset);
border-radius: var(--radius-sm);
font-size: 11px;
font-weight: 600;
font-family: var(--font-family-mono);
color: var(--color-text-secondary);
}
.system-name {
font-size: 13px;
font-weight: 600;
color: var(--color-text-primary);
font-family: var(--font-family-mono);
}
.system-entities {
font-size: 11px;
color: var(--color-text-tertiary);
}
.system-metrics {
display: flex;
align-items: center;
gap: 12px;
}
.metric-time {
font-size: 13px;
font-weight: 600;
font-family: var(--font-family-mono);
color: var(--color-text-primary);
}
.metric-percentage {
font-size: 12px;
font-family: var(--font-family-mono);
color: var(--color-text-secondary);
background: var(--color-bg-inset);
padding: 2px 6px;
border-radius: var(--radius-sm);
}
.system-bar {
width: 100%;
height: 6px;
background: var(--color-bg-inset);
border-radius: 3px;
overflow: hidden;
margin-bottom: 8px;
}
.system-bar-fill {
height: 100%;
transition: width 0.3s ease;
border-radius: 3px;
}
.system-stats {
display: flex;
align-items: center;
gap: 16px;
}
.stat-item {
display: flex;
align-items: center;
gap: 4px;
font-size: 11px;
}
.stat-label {
color: var(--color-text-tertiary);
}
.stat-value {
font-family: var(--font-family-mono);
font-weight: 500;
color: var(--color-text-secondary);
}
.profiler-footer {
padding: 10px 12px;
background: var(--color-bg-elevated);
border-top: 1px solid var(--color-border-default);
flex-shrink: 0;
}
.profiler-legend {
display: flex;
align-items: center;
gap: 16px;
justify-content: center;
}
.legend-item {
display: flex;
align-items: center;
gap: 6px;
font-size: 11px;
color: var(--color-text-secondary);
}
.legend-color {
width: 12px;
height: 12px;
border-radius: 2px;
}
@media (prefers-reduced-motion: reduce) {
.system-row,
.system-bar-fill,
.profiler-btn {
transition: none;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,174 +0,0 @@
.user-profile {
position: relative;
display: flex;
align-items: center;
}
.login-button {
display: flex;
align-items: center;
gap: 4px;
padding: 3px 8px;
background: transparent;
border: none;
border-radius: 2px;
color: #888;
font-size: 10px;
font-weight: 500;
cursor: pointer;
transition: all 0.1s;
}
.login-button:hover:not(:disabled) {
background: rgba(255, 255, 255, 0.08);
color: #ccc;
}
.login-button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.login-button .spinning {
animation: spin 1s linear infinite;
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.user-avatar-button {
display: flex;
align-items: center;
gap: 4px;
padding: 2px 6px 2px 2px;
background: transparent;
border: none;
border-radius: 2px;
color: #888;
font-size: 10px;
font-weight: 500;
cursor: pointer;
transition: all 0.1s;
}
.user-avatar-button:hover {
background: rgba(255, 255, 255, 0.08);
color: #ccc;
}
.user-avatar,
.user-avatar-placeholder {
width: 16px;
height: 16px;
border-radius: 50%;
}
.user-avatar {
object-fit: cover;
border: none;
}
.user-avatar-placeholder {
display: flex;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.1);
color: #888;
border: none;
}
.user-name {
max-width: 80px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.user-menu {
position: absolute;
top: calc(100% + 8px);
right: 0;
min-width: 220px;
background: var(--color-bg-secondary, #252526);
border: 1px solid var(--color-border, #333);
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
z-index: var(--z-index-dropdown);
overflow: hidden;
}
.user-menu-header {
display: flex;
align-items: center;
gap: 12px;
padding: 16px;
background: var(--color-bg-tertiary, #333);
}
.user-menu-avatar {
width: 48px;
height: 48px;
border-radius: 50%;
object-fit: cover;
border: 2px solid var(--color-accent, #0e639c);
}
.user-menu-info {
flex: 1;
min-width: 0;
}
.user-menu-name {
font-size: 14px;
font-weight: 600;
color: var(--color-text-primary, #cccccc);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.user-menu-login {
font-size: 12px;
color: var(--color-text-secondary, #858585);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.user-menu-divider {
height: 1px;
background: var(--color-border, #333);
}
.user-menu-item {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
padding: 12px 16px;
background: transparent;
border: none;
color: var(--color-text-primary, #cccccc);
font-size: 13px;
cursor: pointer;
text-align: left;
transition: background 0.2s;
}
.user-menu-item:hover {
background: var(--color-bg-hover, #2d2d30);
}
.user-menu-item:last-child {
color: #ff3b30;
}
.user-menu-item:last-child:hover {
background: rgba(255, 59, 48, 0.1);
}

View File

@@ -1,230 +0,0 @@
import { ICommand } from './ICommand';
/**
* 命令历史记录配置
*/
export interface CommandManagerConfig {
/**
* 最大历史记录数量
*/
maxHistorySize?: number;
/**
* 是否自动合并相似命令
*/
autoMerge?: boolean;
}
/**
* 命令管理器
* 管理命令的执行、撤销、重做以及历史记录
*/
export class CommandManager {
private undoStack: ICommand[] = [];
private redoStack: ICommand[] = [];
private readonly config: Required<CommandManagerConfig>;
private isExecuting = false;
constructor(config: CommandManagerConfig = {}) {
this.config = {
maxHistorySize: config.maxHistorySize ?? 100,
autoMerge: config.autoMerge ?? true
};
}
/**
* 执行命令
*/
execute(command: ICommand): void {
if (this.isExecuting) {
throw new Error('不能在命令执行过程中执行新命令');
}
this.isExecuting = true;
try {
command.execute();
if (this.config.autoMerge && this.undoStack.length > 0) {
const lastCommand = this.undoStack[this.undoStack.length - 1];
if (lastCommand && lastCommand.canMergeWith(command)) {
const mergedCommand = lastCommand.mergeWith(command);
this.undoStack[this.undoStack.length - 1] = mergedCommand;
this.redoStack = [];
return;
}
}
this.undoStack.push(command);
this.redoStack = [];
if (this.undoStack.length > this.config.maxHistorySize) {
this.undoStack.shift();
}
} finally {
this.isExecuting = false;
}
}
/**
* 撤销上一个命令
*/
undo(): void {
if (this.isExecuting) {
throw new Error('不能在命令执行过程中撤销');
}
const command = this.undoStack.pop();
if (!command) {
return;
}
this.isExecuting = true;
try {
command.undo();
this.redoStack.push(command);
} catch (error) {
this.undoStack.push(command);
throw error;
} finally {
this.isExecuting = false;
}
}
/**
* 重做上一个被撤销的命令
*/
redo(): void {
if (this.isExecuting) {
throw new Error('不能在命令执行过程中重做');
}
const command = this.redoStack.pop();
if (!command) {
return;
}
this.isExecuting = true;
try {
command.execute();
this.undoStack.push(command);
} catch (error) {
this.redoStack.push(command);
throw error;
} finally {
this.isExecuting = false;
}
}
/**
* 检查是否可以撤销
*/
canUndo(): boolean {
return this.undoStack.length > 0;
}
/**
* 检查是否可以重做
*/
canRedo(): boolean {
return this.redoStack.length > 0;
}
/**
* 获取撤销栈的描述列表
*/
getUndoHistory(): string[] {
return this.undoStack.map((cmd) => cmd.getDescription());
}
/**
* 获取重做栈的描述列表
*/
getRedoHistory(): string[] {
return this.redoStack.map((cmd) => cmd.getDescription());
}
/**
* 清空所有历史记录
*/
clear(): void {
this.undoStack = [];
this.redoStack = [];
}
/**
* 批量执行命令(作为单一操作,可以一次撤销)
*/
executeBatch(commands: ICommand[]): void {
if (commands.length === 0) {
return;
}
const batchCommand = new BatchCommand(commands);
this.execute(batchCommand);
}
/**
* 将命令推入撤销栈但不执行
* Push command to undo stack without executing
*
* 用于已经执行过的操作(如拖动变换),只需要记录到历史
* Used for operations that have already been performed (like drag transforms),
* only need to record to history
*/
pushWithoutExecute(command: ICommand): void {
if (this.config.autoMerge && this.undoStack.length > 0) {
const lastCommand = this.undoStack[this.undoStack.length - 1];
if (lastCommand && lastCommand.canMergeWith(command)) {
const mergedCommand = lastCommand.mergeWith(command);
this.undoStack[this.undoStack.length - 1] = mergedCommand;
this.redoStack = [];
return;
}
}
this.undoStack.push(command);
this.redoStack = [];
if (this.undoStack.length > this.config.maxHistorySize) {
this.undoStack.shift();
}
}
}
/**
* 批量命令
* 将多个命令组合为一个命令
*/
class BatchCommand implements ICommand {
constructor(private readonly commands: ICommand[]) {}
execute(): void {
for (const command of this.commands) {
command.execute();
}
}
undo(): void {
for (let i = this.commands.length - 1; i >= 0; i--) {
const command = this.commands[i];
if (command) {
command.undo();
}
}
}
getDescription(): string {
return `批量操作 (${this.commands.length} 个命令)`;
}
canMergeWith(): boolean {
return false;
}
mergeWith(): ICommand {
throw new Error('批量命令不支持合并');
}
}

View File

@@ -1,37 +0,0 @@
import { IService } from '@esengine/ecs-framework';
import { ICompiler } from './ICompiler';
export class CompilerRegistry implements IService {
private compilers: Map<string, ICompiler> = new Map();
register(compiler: ICompiler): void {
if (this.compilers.has(compiler.id)) {
console.warn(`Compiler with id "${compiler.id}" is already registered. Overwriting.`);
}
this.compilers.set(compiler.id, compiler);
}
unregister(compilerId: string): void {
this.compilers.delete(compilerId);
}
get(compilerId: string): ICompiler | undefined {
return this.compilers.get(compilerId);
}
getAll(): ICompiler[] {
return Array.from(this.compilers.values());
}
clear(): void {
this.compilers.clear();
}
dispose(): void {
this.clear();
}
}
// Service identifier for DI registration (用于跨包插件访问)
// 使用 Symbol.for 确保跨包共享同一个 Symbol
export const ICompilerRegistry = Symbol.for('ICompilerRegistry');

View File

@@ -1,88 +0,0 @@
/**
* Component Action Registry Service
*
* Manages component-specific actions for the inspector panel
*/
import { injectable } from 'tsyringe';
import type { IService } from '@esengine/ecs-framework';
import type { ComponentAction } from '../Plugin/EditorModule';
// Re-export ComponentAction type from Plugin system
export type { ComponentAction } from '../Plugin/EditorModule';
@injectable()
export class ComponentActionRegistry implements IService {
private actions: Map<string, ComponentAction[]> = new Map();
/**
* Register a component action
*/
register(action: ComponentAction): void {
const componentName = action.componentName;
if (!this.actions.has(componentName)) {
this.actions.set(componentName, []);
}
const actions = this.actions.get(componentName)!;
const existingIndex = actions.findIndex(a => a.id === action.id);
if (existingIndex >= 0) {
console.warn(`[ComponentActionRegistry] Action '${action.id}' already exists for '${componentName}', overwriting`);
actions[existingIndex] = action;
} else {
actions.push(action);
}
}
/**
* Register multiple actions
*/
registerMany(actions: ComponentAction[]): void {
for (const action of actions) {
this.register(action);
}
}
/**
* Unregister an action by ID
*/
unregister(componentName: string, actionId: string): void {
const actions = this.actions.get(componentName);
if (actions) {
const index = actions.findIndex(a => a.id === actionId);
if (index >= 0) {
actions.splice(index, 1);
}
}
}
/**
* Get all actions for a component type sorted by order
*/
getActionsForComponent(componentName: string): ComponentAction[] {
const actions = this.actions.get(componentName) || [];
return [...actions].sort((a, b) => (a.order ?? 100) - (b.order ?? 100));
}
/**
* Check if a component has any actions
*/
hasActions(componentName: string): boolean {
const actions = this.actions.get(componentName);
return actions !== undefined && actions.length > 0;
}
/**
* Clear all actions
*/
clear(): void {
this.actions.clear();
}
/**
* Dispose resources
*/
dispose(): void {
this.actions.clear();
}
}

View File

@@ -1,211 +0,0 @@
import React from 'react';
import { Component, IService, createLogger } from '@esengine/ecs-framework';
const logger = createLogger('ComponentInspectorRegistry');
/**
* 组件检查器上下文
* Context passed to component inspectors
*/
export interface ComponentInspectorContext {
/** 被检查的组件 */
component: Component;
/** 所属实体 */
entity: any;
/** 版本号(用于触发重渲染) */
version?: number;
/** 属性变更回调 */
onChange?: (propertyName: string, value: any) => void;
/** 动作回调 */
onAction?: (actionId: string, propertyName: string, component: Component) => void;
}
/**
* Inspector render mode.
* 检查器渲染模式。
*/
export type InspectorRenderMode = 'replace' | 'append';
/**
* 组件检查器接口
* Interface for custom component inspectors
*/
export interface IComponentInspector<T extends Component = Component> {
/** 唯一标识符 */
readonly id: string;
/** 显示名称 */
readonly name: string;
/** 优先级(数字越大优先级越高) */
readonly priority?: number;
/** 目标组件类型名称列表 */
readonly targetComponents: string[];
/**
* 渲染模式
* - 'replace': 替换默认的 PropertyInspector默认
* - 'append': 追加到默认的 PropertyInspector 后面
*/
readonly renderMode?: InspectorRenderMode;
/**
* 判断是否可以处理该组件
*/
canHandle(component: Component): component is T;
/**
* 渲染组件检查器
*/
render(context: ComponentInspectorContext): React.ReactElement;
}
/**
* 组件检查器注册表
* Registry for custom component inspectors
*/
export class ComponentInspectorRegistry implements IService {
private inspectors: Map<string, IComponentInspector> = new Map();
/**
* 注册组件检查器
*/
register(inspector: IComponentInspector): void {
if (this.inspectors.has(inspector.id)) {
logger.warn(`Overwriting existing component inspector: ${inspector.id}`);
}
this.inspectors.set(inspector.id, inspector);
logger.debug(`Registered component inspector: ${inspector.name} (${inspector.id})`);
}
/**
* 注销组件检查器
*/
unregister(inspectorId: string): void {
if (this.inspectors.delete(inspectorId)) {
logger.debug(`Unregistered component inspector: ${inspectorId}`);
}
}
/**
* 查找可以处理指定组件的检查器(仅 replace 模式)
* Find inspector that can handle the component (replace mode only)
*/
findInspector(component: Component): IComponentInspector | undefined {
const inspectors = Array.from(this.inspectors.values())
.filter(i => i.renderMode !== 'append')
.sort((a, b) => (b.priority || 0) - (a.priority || 0));
for (const inspector of inspectors) {
try {
if (inspector.canHandle(component)) {
return inspector;
}
} catch (error) {
logger.error(`Error in canHandle for inspector ${inspector.id}:`, error);
}
}
return undefined;
}
/**
* 查找所有追加模式的检查器
* Find all append-mode inspectors for the component
*/
findAppendInspectors(component: Component): IComponentInspector[] {
const result: IComponentInspector[] = [];
const inspectors = Array.from(this.inspectors.values())
.filter(i => i.renderMode === 'append')
.sort((a, b) => (b.priority || 0) - (a.priority || 0));
for (const inspector of inspectors) {
try {
if (inspector.canHandle(component)) {
result.push(inspector);
}
} catch (error) {
logger.error(`Error in canHandle for inspector ${inspector.id}:`, error);
}
}
return result;
}
/**
* 检查是否有自定义检查器replace 模式)
*/
hasInspector(component: Component): boolean {
return this.findInspector(component) !== undefined;
}
/**
* 检查是否有追加检查器
*/
hasAppendInspectors(component: Component): boolean {
return this.findAppendInspectors(component).length > 0;
}
/**
* 渲染组件replace 模式)
* Render component with replace-mode inspector
*/
render(context: ComponentInspectorContext): React.ReactElement | null {
const inspector = this.findInspector(context.component);
if (!inspector) {
return null;
}
try {
return inspector.render(context);
} catch (error) {
logger.error(`Error rendering with inspector ${inspector.id}:`, error);
return React.createElement(
'span',
{ style: { color: '#f87171', fontStyle: 'italic' } },
'[Inspector Render Error]'
);
}
}
/**
* 渲染追加检查器
* Render append-mode inspectors
*/
renderAppendInspectors(context: ComponentInspectorContext): React.ReactElement[] {
const inspectors = this.findAppendInspectors(context.component);
const elements: React.ReactElement[] = [];
for (const inspector of inspectors) {
try {
elements.push(
React.createElement(
React.Fragment,
{ key: inspector.id },
inspector.render(context)
)
);
} catch (error) {
logger.error(`Error rendering append inspector ${inspector.id}:`, error);
elements.push(
React.createElement(
'span',
{ key: inspector.id, style: { color: '#f87171', fontStyle: 'italic' } },
`[${inspector.name} Error]`
)
);
}
}
return elements;
}
/**
* 获取所有注册的检查器
*/
getAllInspectors(): IComponentInspector[] {
return Array.from(this.inspectors.values());
}
dispose(): void {
this.inspectors.clear();
logger.debug('ComponentInspectorRegistry disposed');
}
}

View File

@@ -1,60 +0,0 @@
import { Injectable, IService, Component } from '@esengine/ecs-framework';
export interface ComponentTypeInfo {
name: string;
type?: new (...args: any[]) => Component;
category?: string;
description?: string;
icon?: string;
metadata?: {
path?: string;
fileName?: string;
[key: string]: any;
};
}
/**
* 编辑器组件注册表
* Editor Component Registry
*
* 管理编辑器中可用的组件类型元数据(名称、分类、图标等)。
* 与 ECS 核心的 ComponentRegistry管理组件位掩码不同。
*
* Manages component type metadata (name, category, icon, etc.) for the editor.
* Different from the ECS core ComponentRegistry (which manages component bitmasks).
*/
@Injectable()
export class EditorComponentRegistry implements IService {
private components: Map<string, ComponentTypeInfo> = new Map();
public dispose(): void {
this.components.clear();
}
public register(info: ComponentTypeInfo): void {
this.components.set(info.name, info);
}
public unregister(name: string): void {
this.components.delete(name);
}
public getComponent(name: string): ComponentTypeInfo | undefined {
return this.components.get(name);
}
public getAllComponents(): ComponentTypeInfo[] {
return Array.from(this.components.values());
}
public getComponentsByCategory(category: string): ComponentTypeInfo[] {
return this.getAllComponents().filter((c) => c.category === category);
}
public createInstance(name: string, ...args: any[]): Component | null {
const info = this.components.get(name);
if (!info || !info.type) return null;
return new info.type(...args);
}
}

View File

@@ -1,76 +0,0 @@
/**
* Entity Creation Registry Service
*
* Manages entity creation templates for the scene hierarchy context menu
*/
import { injectable } from 'tsyringe';
import type { IService } from '@esengine/ecs-framework';
import type { EntityCreationTemplate } from '../Types/UITypes';
@injectable()
export class EntityCreationRegistry implements IService {
private templates: Map<string, EntityCreationTemplate> = new Map();
/**
* Register an entity creation template
*/
register(template: EntityCreationTemplate): void {
if (this.templates.has(template.id)) {
console.warn(`[EntityCreationRegistry] Template '${template.id}' already exists, overwriting`);
}
this.templates.set(template.id, template);
}
/**
* Register multiple templates
*/
registerMany(templates: EntityCreationTemplate[]): void {
for (const template of templates) {
this.register(template);
}
}
/**
* Unregister a template by ID
*/
unregister(id: string): void {
this.templates.delete(id);
}
/**
* Get all registered templates sorted by order
*/
getAll(): EntityCreationTemplate[] {
return Array.from(this.templates.values())
.sort((a, b) => (a.order ?? 100) - (b.order ?? 100));
}
/**
* Get a template by ID
*/
get(id: string): EntityCreationTemplate | undefined {
return this.templates.get(id);
}
/**
* Check if a template exists
*/
has(id: string): boolean {
return this.templates.has(id);
}
/**
* Clear all templates
*/
clear(): void {
this.templates.clear();
}
/**
* Dispose resources
*/
dispose(): void {
this.templates.clear();
}
}

View File

@@ -1,50 +0,0 @@
import { IService, createLogger } from '@esengine/ecs-framework';
import { IFieldEditor, IFieldEditorRegistry, FieldEditorContext } from './IFieldEditor';
const logger = createLogger('FieldEditorRegistry');
export class FieldEditorRegistry implements IFieldEditorRegistry, IService {
private editors: Map<string, IFieldEditor> = new Map();
register(editor: IFieldEditor): void {
if (this.editors.has(editor.type)) {
logger.warn(`Overwriting existing field editor: ${editor.type}`);
}
this.editors.set(editor.type, editor);
logger.debug(`Registered field editor: ${editor.name} (${editor.type})`);
}
unregister(type: string): void {
if (this.editors.delete(type)) {
logger.debug(`Unregistered field editor: ${type}`);
}
}
getEditor(type: string, context?: FieldEditorContext): IFieldEditor | undefined {
const editor = this.editors.get(type);
if (editor) {
return editor;
}
const editors = Array.from(this.editors.values())
.sort((a, b) => (b.priority || 0) - (a.priority || 0));
for (const editor of editors) {
if (editor.canHandle(type, context)) {
return editor;
}
}
return undefined;
}
getAllEditors(): IFieldEditor[] {
return Array.from(this.editors.values());
}
dispose(): void {
this.editors.clear();
logger.debug('FieldEditorRegistry disposed');
}
}

View File

@@ -1,209 +0,0 @@
import { IService } from '@esengine/ecs-framework';
import type { FileActionHandler, FileCreationTemplate } from '../Plugin/EditorModule';
// Re-export for backwards compatibility
export type { FileCreationTemplate } from '../Plugin/EditorModule';
/**
* 资产创建消息映射
* Asset creation message mapping
*
* 定义扩展名到创建消息的映射,用于 PropertyInspector 中的资产字段创建按钮
*/
export interface AssetCreationMapping {
/** 文件扩展名(包含点号,如 '.tilemap'| File extension (with dot) */
extension: string;
/** 创建资产时发送的消息名 | Message name to publish when creating asset */
createMessage: string;
/** 是否支持创建(可选,默认 true| Whether creation is supported */
canCreate?: boolean;
}
/**
* FileActionRegistry 服务标识符
* FileActionRegistry service identifier
*/
export const IFileActionRegistry = Symbol.for('IFileActionRegistry');
/**
* 文件操作注册表服务
*
* 管理插件注册的文件操作处理器和文件创建模板
*/
export class FileActionRegistry implements IService {
private actionHandlers: Map<string, FileActionHandler[]> = new Map();
private creationTemplates: FileCreationTemplate[] = [];
private assetCreationMappings: Map<string, AssetCreationMapping> = new Map();
/**
* 注册文件操作处理器
*/
registerActionHandler(handler: FileActionHandler): void {
for (const ext of handler.extensions) {
const handlers = this.actionHandlers.get(ext) || [];
handlers.push(handler);
this.actionHandlers.set(ext, handlers);
}
}
/**
* 注销文件操作处理器
*/
unregisterActionHandler(handler: FileActionHandler): void {
for (const ext of handler.extensions) {
const handlers = this.actionHandlers.get(ext);
if (handlers) {
const index = handlers.indexOf(handler);
if (index !== -1) {
handlers.splice(index, 1);
}
if (handlers.length === 0) {
this.actionHandlers.delete(ext);
}
}
}
}
/**
* 注册文件创建模板
*/
registerCreationTemplate(template: FileCreationTemplate): void {
this.creationTemplates.push(template);
}
/**
* 注销文件创建模板
*/
unregisterCreationTemplate(template: FileCreationTemplate): void {
const index = this.creationTemplates.indexOf(template);
if (index !== -1) {
this.creationTemplates.splice(index, 1);
}
}
/**
* 获取文件扩展名的处理器
*/
getHandlersForExtension(extension: string): FileActionHandler[] {
return this.actionHandlers.get(extension) || [];
}
/**
* 获取文件的处理器
*/
getHandlersForFile(filePath: string): FileActionHandler[] {
const extension = this.getFileExtension(filePath);
return extension ? this.getHandlersForExtension(extension) : [];
}
/**
* 获取所有文件创建模板
*/
getCreationTemplates(): FileCreationTemplate[] {
return this.creationTemplates;
}
/**
* 处理文件双击
*/
async handleDoubleClick(filePath: string): Promise<boolean> {
const handlers = this.getHandlersForFile(filePath);
for (const handler of handlers) {
if (handler.onDoubleClick) {
await handler.onDoubleClick(filePath);
return true;
}
}
return false;
}
/**
* 处理文件打开
*/
async handleOpen(filePath: string): Promise<boolean> {
const handlers = this.getHandlersForFile(filePath);
for (const handler of handlers) {
if (handler.onOpen) {
await handler.onOpen(filePath);
return true;
}
}
return false;
}
/**
* 注册资产创建消息映射
* Register asset creation message mapping
*/
registerAssetCreationMapping(mapping: AssetCreationMapping): void {
const normalizedExt = mapping.extension.startsWith('.')
? mapping.extension.toLowerCase()
: `.${mapping.extension.toLowerCase()}`;
this.assetCreationMappings.set(normalizedExt, {
...mapping,
extension: normalizedExt
});
}
/**
* 注销资产创建消息映射
* Unregister asset creation message mapping
*/
unregisterAssetCreationMapping(extension: string): void {
const normalizedExt = extension.startsWith('.')
? extension.toLowerCase()
: `.${extension.toLowerCase()}`;
this.assetCreationMappings.delete(normalizedExt);
}
/**
* 获取扩展名对应的资产创建消息映射
* Get asset creation mapping for extension
*/
getAssetCreationMapping(extension: string): AssetCreationMapping | undefined {
const normalizedExt = extension.startsWith('.')
? extension.toLowerCase()
: `.${extension.toLowerCase()}`;
return this.assetCreationMappings.get(normalizedExt);
}
/**
* 检查扩展名是否支持创建资产
* Check if extension supports asset creation
*/
canCreateAsset(extension: string): boolean {
const mapping = this.getAssetCreationMapping(extension);
return mapping?.canCreate !== false;
}
/**
* 获取所有资产创建映射
* Get all asset creation mappings
*/
getAllAssetCreationMappings(): AssetCreationMapping[] {
return Array.from(this.assetCreationMappings.values());
}
/**
* 清空所有注册
*/
clear(): void {
this.actionHandlers.clear();
this.creationTemplates = [];
this.assetCreationMappings.clear();
}
/**
* 释放资源
*/
dispose(): void {
this.clear();
}
private getFileExtension(filePath: string): string | null {
const lastDot = filePath.lastIndexOf('.');
if (lastDot === -1) return null;
return filePath.substring(lastDot + 1).toLowerCase();
}
}

View File

@@ -1,81 +0,0 @@
import { IInspectorProvider, InspectorContext } from './IInspectorProvider';
import { IService } from '@esengine/ecs-framework';
import React from 'react';
export class InspectorRegistry implements IService {
private providers: Map<string, IInspectorProvider> = new Map();
/**
* 注册Inspector提供器
*/
register(provider: IInspectorProvider): void {
if (this.providers.has(provider.id)) {
console.warn(`Inspector provider with id "${provider.id}" is already registered`);
return;
}
this.providers.set(provider.id, provider);
}
/**
* 注销Inspector提供器
*/
unregister(providerId: string): void {
this.providers.delete(providerId);
}
/**
* 获取指定ID的提供器
*/
getProvider(providerId: string): IInspectorProvider | undefined {
return this.providers.get(providerId);
}
/**
* 获取所有提供器
*/
getAllProviders(): IInspectorProvider[] {
return Array.from(this.providers.values());
}
/**
* 查找可以处理指定目标的提供器
* 按优先级排序,返回第一个可以处理的提供器
*/
findProvider(target: unknown): IInspectorProvider | undefined {
const providers = Array.from(this.providers.values())
.sort((a, b) => (b.priority || 0) - (a.priority || 0));
for (const provider of providers) {
if (provider.canHandle(target)) {
return provider;
}
}
return undefined;
}
/**
* 渲染Inspector内容
* 自动查找合适的提供器并渲染
*/
render(target: unknown, context: InspectorContext): React.ReactElement | null {
const provider = this.findProvider(target);
if (!provider) {
return null;
}
return provider.render(target, context);
}
clear(): void {
this.providers.clear();
}
dispose(): void {
this.clear();
}
}
// Service identifier for DI registration (用于跨包插件访问)
// 使用 Symbol.for 确保跨包共享同一个 Symbol
export const IInspectorRegistry = Symbol.for('IInspectorRegistry');

View File

@@ -1,74 +0,0 @@
import React from 'react';
import { IService, createLogger } from '@esengine/ecs-framework';
import { IPropertyRenderer, IPropertyRendererRegistry, PropertyContext } from './IPropertyRenderer';
const logger = createLogger('PropertyRendererRegistry');
export class PropertyRendererRegistry implements IPropertyRendererRegistry, IService {
private renderers: Map<string, IPropertyRenderer> = new Map();
register(renderer: IPropertyRenderer): void {
if (this.renderers.has(renderer.id)) {
logger.warn(`Overwriting existing property renderer: ${renderer.id}`);
}
this.renderers.set(renderer.id, renderer);
logger.debug(`Registered property renderer: ${renderer.name} (${renderer.id})`);
}
unregister(rendererId: string): void {
if (this.renderers.delete(rendererId)) {
logger.debug(`Unregistered property renderer: ${rendererId}`);
}
}
findRenderer(value: any, context: PropertyContext): IPropertyRenderer | undefined {
const renderers = Array.from(this.renderers.values())
.sort((a, b) => (b.priority || 0) - (a.priority || 0));
for (const renderer of renderers) {
try {
if (renderer.canHandle(value, context)) {
return renderer;
}
} catch (error) {
logger.error(`Error in canHandle for renderer ${renderer.id}:`, error);
}
}
return undefined;
}
render(value: any, context: PropertyContext): React.ReactElement | null {
const renderer = this.findRenderer(value, context);
if (!renderer) {
logger.debug(`No renderer found for value type: ${typeof value}`);
return null;
}
try {
return renderer.render(value, context);
} catch (error) {
logger.error(`Error rendering with ${renderer.id}:`, error);
return React.createElement(
'span',
{ style: { color: '#f87171', fontStyle: 'italic' } },
'[Render Error]'
);
}
}
getAllRenderers(): IPropertyRenderer[] {
return Array.from(this.renderers.values());
}
hasRenderer(value: any, context: PropertyContext): boolean {
return this.findRenderer(value, context) !== undefined;
}
dispose(): void {
this.renderers.clear();
logger.debug('PropertyRendererRegistry disposed');
}
}

View File

@@ -1,202 +0,0 @@
import type { IService } from '@esengine/ecs-framework';
import { Injectable } from '@esengine/ecs-framework';
import { createLogger } from '@esengine/ecs-framework';
import type { MenuItem, ToolbarItem, PanelDescriptor } from '../Types/UITypes';
const logger = createLogger('UIRegistry');
/**
* UI 注册表
*
* 管理所有编辑器 UI 扩展点的注册和查询。
*/
@Injectable()
export class UIRegistry implements IService {
private menus: Map<string, MenuItem> = new Map();
private toolbarItems: Map<string, ToolbarItem> = new Map();
private panels: Map<string, PanelDescriptor> = new Map();
/**
* 注册菜单项
*/
public registerMenu(item: MenuItem): void {
if (this.menus.has(item.id)) {
logger.warn(`Menu item ${item.id} is already registered`);
return;
}
this.menus.set(item.id, item);
logger.debug(`Registered menu item: ${item.id}`);
}
/**
* 批量注册菜单项
*/
public registerMenus(items: MenuItem[]): void {
for (const item of items) {
this.registerMenu(item);
}
}
/**
* 注销菜单项
*/
public unregisterMenu(id: string): boolean {
const result = this.menus.delete(id);
if (result) {
logger.debug(`Unregistered menu item: ${id}`);
}
return result;
}
/**
* 获取菜单项
*/
public getMenu(id: string): MenuItem | undefined {
return this.menus.get(id);
}
/**
* 获取所有菜单项
*/
public getAllMenus(): MenuItem[] {
return Array.from(this.menus.values()).sort((a, b) => {
return (a.order ?? 0) - (b.order ?? 0);
});
}
/**
* 获取指定父菜单的子菜单
*/
public getChildMenus(parentId: string): MenuItem[] {
return this.getAllMenus()
.filter((item) => item.parentId === parentId)
.sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
}
/**
* 注册工具栏项
*/
public registerToolbarItem(item: ToolbarItem): void {
if (this.toolbarItems.has(item.id)) {
logger.warn(`Toolbar item ${item.id} is already registered`);
return;
}
this.toolbarItems.set(item.id, item);
logger.debug(`Registered toolbar item: ${item.id}`);
}
/**
* 批量注册工具栏项
*/
public registerToolbarItems(items: ToolbarItem[]): void {
for (const item of items) {
this.registerToolbarItem(item);
}
}
/**
* 注销工具栏项
*/
public unregisterToolbarItem(id: string): boolean {
const result = this.toolbarItems.delete(id);
if (result) {
logger.debug(`Unregistered toolbar item: ${id}`);
}
return result;
}
/**
* 获取工具栏项
*/
public getToolbarItem(id: string): ToolbarItem | undefined {
return this.toolbarItems.get(id);
}
/**
* 获取所有工具栏项
*/
public getAllToolbarItems(): ToolbarItem[] {
return Array.from(this.toolbarItems.values()).sort((a, b) => {
return (a.order ?? 0) - (b.order ?? 0);
});
}
/**
* 获取指定组的工具栏项
*/
public getToolbarItemsByGroup(groupId: string): ToolbarItem[] {
return this.getAllToolbarItems()
.filter((item) => item.groupId === groupId)
.sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
}
/**
* 注册面板
*/
public registerPanel(panel: PanelDescriptor): void {
if (this.panels.has(panel.id)) {
logger.warn(`Panel ${panel.id} is already registered`);
return;
}
this.panels.set(panel.id, panel);
logger.debug(`Registered panel: ${panel.id}`);
}
/**
* 批量注册面板
*/
public registerPanels(panels: PanelDescriptor[]): void {
for (const panel of panels) {
this.registerPanel(panel);
}
}
/**
* 注销面板
*/
public unregisterPanel(id: string): boolean {
const result = this.panels.delete(id);
if (result) {
logger.debug(`Unregistered panel: ${id}`);
}
return result;
}
/**
* 获取面板
*/
public getPanel(id: string): PanelDescriptor | undefined {
return this.panels.get(id);
}
/**
* 获取所有面板
*/
public getAllPanels(): PanelDescriptor[] {
return Array.from(this.panels.values()).sort((a, b) => {
return (a.order ?? 0) - (b.order ?? 0);
});
}
/**
* 获取指定位置的面板
*/
public getPanelsByPosition(position: string): PanelDescriptor[] {
return this.getAllPanels()
.filter((panel) => panel.position === position)
.sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
}
/**
* 释放资源
*/
public dispose(): void {
this.menus.clear();
this.toolbarItems.clear();
this.panels.clear();
logger.info('UIRegistry disposed');
}
}

View File

@@ -1,177 +0,0 @@
import { IService } from '@esengine/ecs-framework';
import { ComponentType } from 'react';
/**
* 窗口描述符
*/
export interface WindowDescriptor {
/**
* 窗口唯一标识
*/
id: string;
/**
* 窗口组件
*/
component: ComponentType<any>;
/**
* 窗口标题
*/
title?: string;
/**
* 默认宽度
*/
defaultWidth?: number;
/**
* 默认高度
*/
defaultHeight?: number;
}
/**
* 窗口实例
*/
export interface WindowInstance {
/**
* 窗口描述符
*/
descriptor: WindowDescriptor;
/**
* 是否打开
*/
isOpen: boolean;
/**
* 窗口参数
*/
params?: Record<string, any>;
}
/**
* 窗口注册表服务
*
* 管理插件注册的窗口组件
*/
export class WindowRegistry implements IService {
private windows: Map<string, WindowDescriptor> = new Map();
private openWindows: Map<string, WindowInstance> = new Map();
private listeners: Set<() => void> = new Set();
/**
* 注册窗口
*/
registerWindow(descriptor: WindowDescriptor): void {
if (this.windows.has(descriptor.id)) {
console.warn(`Window ${descriptor.id} is already registered`);
return;
}
this.windows.set(descriptor.id, descriptor);
}
/**
* 取消注册窗口
*/
unregisterWindow(windowId: string): void {
this.windows.delete(windowId);
this.openWindows.delete(windowId);
this.notifyListeners();
}
/**
* 获取窗口描述符
*/
getWindow(windowId: string): WindowDescriptor | undefined {
return this.windows.get(windowId);
}
/**
* 获取所有窗口描述符
*/
getAllWindows(): WindowDescriptor[] {
return Array.from(this.windows.values());
}
/**
* 打开窗口
*/
openWindow(windowId: string, params?: Record<string, any>): void {
const descriptor = this.windows.get(windowId);
if (!descriptor) {
console.warn(`Window ${windowId} is not registered`);
return;
}
this.openWindows.set(windowId, {
descriptor,
isOpen: true,
params
});
this.notifyListeners();
}
/**
* 关闭窗口
*/
closeWindow(windowId: string): void {
this.openWindows.delete(windowId);
this.notifyListeners();
}
/**
* 获取打开的窗口实例
*/
getOpenWindow(windowId: string): WindowInstance | undefined {
return this.openWindows.get(windowId);
}
/**
* 获取所有打开的窗口
*/
getAllOpenWindows(): WindowInstance[] {
return Array.from(this.openWindows.values());
}
/**
* 检查窗口是否打开
*/
isWindowOpen(windowId: string): boolean {
return this.openWindows.has(windowId);
}
/**
* 添加变化监听器
*/
addListener(listener: () => void): () => void {
this.listeners.add(listener);
return () => {
this.listeners.delete(listener);
};
}
/**
* 通知所有监听器
*/
private notifyListeners(): void {
this.listeners.forEach((listener) => listener());
}
/**
* 清空所有窗口
*/
clear(): void {
this.windows.clear();
this.openWindows.clear();
this.listeners.clear();
}
/**
* 释放资源
*/
dispose(): void {
this.clear();
}
}

View File

@@ -1,75 +0,0 @@
/**
* ECS Framework Editor Core
*
* Plugin-based editor framework for ECS Framework
*/
// Service Tokens | 服务令牌
export * from './tokens';
// 配置 | Configuration
export * from './Config';
// 新插件系统 | New plugin system
export * from './Plugin';
export * from './Services/UIRegistry';
export * from './Services/MessageHub';
export * from './Services/SerializerRegistry';
export * from './Services/EntityStoreService';
export * from './Services/ComponentRegistry';
export * from './Services/LocaleService';
export * from './Services/PropertyMetadata';
export * from './Services/ProjectService';
export * from './Services/ComponentDiscoveryService';
export * from './Services/LogService';
export * from './Services/SettingsRegistry';
export * from './Services/SceneManagerService';
export * from './Services/SceneTemplateRegistry';
export * from './Services/FileActionRegistry';
export * from './Services/EntityCreationRegistry';
export * from './Services/CompilerRegistry';
export * from './Services/ICompiler';
export * from './Services/ICommand';
export * from './Services/BaseCommand';
export * from './Services/CommandManager';
export * from './Services/IEditorDataStore';
export * from './Services/IFileSystem';
export * from './Services/IDialog';
export * from './Services/INotification';
export * from './Services/IInspectorProvider';
export * from './Services/InspectorRegistry';
export * from './Services/IPropertyRenderer';
export * from './Services/PropertyRendererRegistry';
export * from './Services/IFieldEditor';
export * from './Services/FieldEditorRegistry';
export * from './Services/ComponentInspectorRegistry';
export * from './Services/ComponentActionRegistry';
export * from './Services/AssetRegistryService';
export * from './Services/IViewportService';
export * from './Services/PreviewSceneService';
export * from './Services/EditorViewportService';
export * from './Services/PrefabService';
export * from './Services/VirtualNodeRegistry';
export * from './Services/GizmoInteractionService';
// Build System | 构建系统
export * from './Services/Build';
// User Code System | 用户代码系统
export * from './Services/UserCode';
// Module System | 模块系统
export * from './Services/Module';
export * from './Gizmos';
export * from './Rendering';
export * from './Module/IEventBus';
export * from './Module/ICommandRegistry';
export * from './Module/IPanelRegistry';
export * from './Module/IModuleContext';
export * from './Module/IEditorModule';
export * from './Types/IFileAPI';
export * from './Types/UITypes';

View File

@@ -32,6 +32,8 @@
"@esengine/engine-core": "workspace:*",
"@esengine/material-editor": "workspace:*",
"@esengine/material-system": "workspace:*",
"@esengine/mesh-3d": "workspace:*",
"@esengine/mesh-3d-editor": "workspace:*",
"@esengine/particle": "workspace:*",
"@esengine/particle-editor": "workspace:*",
"@esengine/physics-rapier2d": "workspace:*",

View File

@@ -1104,6 +1104,7 @@ dependencies = [
"notify-debouncer-mini",
"once_cell",
"qrcode",
"regex",
"serde",
"serde_json",
"tauri",

View File

@@ -36,6 +36,7 @@ qrcode = "0.14"
image = "0.25"
notify = "7.0"
notify-debouncer-mini = "0.5"
regex = "1"
[profile.dev]
incremental = true

View File

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View File

Before

Width:  |  Height:  |  Size: 915 B

After

Width:  |  Height:  |  Size: 915 B

Some files were not shown because too many files have changed in this diff Show More