Compare commits

..

24 Commits

Author SHA1 Message Date
yhh 8662449dcf feat(ci): 改进 SignPath 代码签名集成
- 添加 SignPath 配置检查步骤
- 使用 test-signing 策略进行测试
- 即使签名跳过也能继续版本更新 PR
2025-12-16 13:09:39 +08:00
yhh 1834bc2068 fix(tests): 更新测试以使用 GlobalComponentRegistry 实例
修复多个测试文件以适配 ComponentRegistry 从静态类变为实例类的变更:
- ComponentStorage.test.ts: 使用 GlobalComponentRegistry.reset()
- EntitySerializer.test.ts: 使用 GlobalComponentRegistry 实例
- IncrementalSerialization.test.ts: 使用 GlobalComponentRegistry 实例
- SceneSerializer.test.ts: 使用 GlobalComponentRegistry 实例
- ComponentRegistry.extended.test.ts: 使用 GlobalComponentRegistry,同时注册到 scene.componentRegistry
- SystemTypes.test.ts: 在 Scene 创建前注册组件
- QuerySystem.test.ts: mockScene 添加 componentRegistry
2025-12-16 12:38:14 +08:00
yhh c23c6c21db fix(asset-system): 移除未使用的 TextureLoader 导入 2025-12-16 12:06:09 +08:00
yhh b494283e9c refactor(asset-system-editor): 资产元数据改进
- AssetMetaFile 优化
- 导出调整
2025-12-16 11:55:39 +08:00
yhh 9b334f36e1 refactor(platform): 平台适配层优化
- BrowserRuntime 改进
- 新增 RuntimeSceneManager 服务
- 导出优化
2025-12-16 11:55:06 +08:00
yhh 7f8d2eb142 refactor(particle): 粒子系统改进
- 适配新的组件注册接口
- ParticleSystem 优化
- 添加单元测试
2025-12-16 11:52:21 +08:00
yhh 9d3eeb1980 feat(tauri): 添加文件修改时间查询命令
- 新增 get_file_mtime 命令
- 支持检测文件外部修改
2025-12-16 11:51:58 +08:00
yhh 0bcb675c3b feat(i18n): 更新国际化翻译
- 添加新功能相关翻译
- 更新中文、英文、西班牙文
2025-12-16 11:29:14 +08:00
yhh 574b4d08a3 refactor(editor-app): 编辑器服务和组件优化
- EngineService 改进引擎集成
- EditorEngineSync 同步优化
- AssetFileInspector 改进
- VectorFieldEditors 优化
- InstantiatePrefabCommand 改进
2025-12-16 11:28:50 +08:00
yhh d64e463a71 feat(editor-app): 添加渲染调试面板
- 新增 RenderDebugService 和调试面板 UI
- App/ContentBrowser 添加调试日志
- TitleBar/Viewport 优化
- DialogManager 改进
2025-12-16 11:28:34 +08:00
yhh 792fd05c85 feat(editor-app): 添加外部文件修改检测
- 新增 ExternalModificationDialog 组件
- TauriFileAPI 支持 getFileMtime
- 场景文件被外部修改时提示用户
2025-12-16 11:28:08 +08:00
yhh 7814b97ace feat(ui): 添加场景切换和文本闪烁组件
新增组件:
- SceneLoadTriggerComponent: 场景切换触发器
- TextBlinkComponent: 文本闪烁效果

新增系统:
- SceneLoadTriggerSystem: 处理场景切换逻辑
- TextBlinkSystem: 处理文本闪烁动画

其他改进:
- UIRuntimeModule 适配新组件注册接口
- UI 渲染系统优化
2025-12-16 11:25:49 +08:00
yhh 75be905f14 feat(engine): 改进 Rust 纹理管理器
- 支持任意 ID 的纹理加载(非递增)
- 添加纹理状态追踪 API
- 优化纹理缓存清理机制
- 更新 TypeScript 绑定
2025-12-16 11:25:28 +08:00
yhh 01293590e8 feat(editor-core): 改进编辑器核心服务
- EntityStoreService 添加调试日志
- AssetRegistryService 优化资产注册
- PluginManager 改进插件管理
- IFileAPI 添加 getFileMtime 接口
2025-12-16 11:23:50 +08:00
yhh b236b729b4 fix(editor-app): 在编译完成后调用 signalReady()
确保用户脚本编译完成后发出就绪信号:
- 编译成功后调用 userCodeService.signalReady()
- 编译失败也要发出信号,避免阻塞场景加载
2025-12-16 11:21:57 +08:00
yhh 0170dc6e9c feat(editor-core): 添加 UserCodeService 就绪信号机制
- 新增 waitForReady()/signalReady() API
- 支持等待用户脚本编译完成
- 解决场景加载时组件未注册的时序问题
2025-12-16 11:17:19 +08:00
yhh 7834328ae0 fix(physics-rapier2d): 修复物理插件组件注册
- PhysicsEditorPlugin 添加 runtimeModule 引用
- 适配 IComponentRegistry 接口
- 修复物理组件在场景加载时未注册的问题
2025-12-16 11:12:50 +08:00
yhh 39fa797299 refactor(modules): 适配新的组件注册接口
更新各模块 RuntimeModule 使用 IComponentRegistry 接口:
- audio, behavior-tree, camera
- sprite, tilemap, world-streaming
2025-12-16 11:12:17 +08:00
yhh 03229ffb59 refactor(engine-core): 改进插件服务注册机制
- 更新 IComponentRegistry 类型引用
- 优化 PluginServiceRegistry 服务管理
2025-12-16 11:11:48 +08:00
yhh 844a770335 refactor(core): 提取 IComponentRegistry 接口
将组件注册表抽象为接口,支持场景级组件注册:
- 新增 IComponentRegistry 接口定义
- Scene 持有独立的 componentRegistry 实例
- 支持从 GlobalComponentRegistry 克隆
- 各系统支持传入自定义注册表
2025-12-16 11:11:29 +08:00
yhh c8dc9869a3 fix(runtime-core): 修复 PluginManager 组件注册类型错误
将 ComponentRegistry 类改为 GlobalComponentRegistry 实例:
- registerComponents() 期望 IComponentRegistry 接口实例
- GlobalComponentRegistry 是 ComponentRegistry 的全局实例
2025-12-16 11:08:09 +08:00
yhh 38755c9014 fix(editor-core): 修复场景切换时的资源泄漏
在 openScene() 加载新场景前先卸载旧场景资源:
- 调用 sceneResourceManager.unloadSceneResources() 释放旧资源
- 使用引用计数机制,仅卸载不再被引用的资源
- 路径稳定 ID 缓存不受影响,保持 ID 稳定性
2025-12-16 11:07:48 +08:00
yhh 5d5537e4c7 fix(runtime-core): 移除 Play/Stop 循环中的 clearTextureMappings 调用
使用路径稳定 ID 后,不再需要在快照保存/恢复时清除纹理缓存:
- saveSceneSnapshot() 移除 clearTextureMappings() 调用
- restoreSceneSnapshot() 移除 clearTextureMappings() 调用
- 组件保存的 textureId 在 Play/Stop 后仍然有效
2025-12-16 11:07:15 +08:00
yhh 9da9f5f068 feat(asset-system): 实现路径稳定 ID 生成器
使用 FNV-1a hash 算法为纹理生成稳定的运行时 ID:
- 新增 _pathIdCache 静态缓存,跨 Play/Stop 循环保持稳定
- 新增 getStableIdForPath() 方法,相同路径永远返回相同 ID
- 修改 loadTextureForComponent/loadTextureByGuid 使用稳定 ID
- clearTextureMappings() 不再清除 _pathIdCache

这解决了 Play/Stop 后纹理 ID 失效的根本问题。
2025-12-16 11:06:59 +08:00
430 changed files with 16262 additions and 54097 deletions
+13 -21
View File
@@ -156,25 +156,18 @@ jobs:
if: steps.check-signpath.outputs.enabled == 'true'
uses: actions/checkout@v4
- name: Get artifact ID
- name: Download Windows artifact
if: steps.check-signpath.outputs.enabled == 'true'
uses: actions/download-artifact@v4
with:
name: windows-unsigned
path: ./artifacts
- name: List artifacts for signing
if: steps.check-signpath.outputs.enabled == 'true'
id: get-artifact
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
# 获取 windows-unsigned artifact 的 ID
ARTIFACT_ID=$(gh api \
-H "Accept: application/vnd.github+json" \
"/repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts" \
--jq '.artifacts[] | select(.name == "windows-unsigned") | .id')
if [ -z "$ARTIFACT_ID" ]; then
echo "Error: Could not find artifact 'windows-unsigned'"
exit 1
fi
echo "artifact-id=$ARTIFACT_ID" >> $GITHUB_OUTPUT
echo "Found artifact ID: $ARTIFACT_ID"
echo "Files to be signed:"
find ./artifacts -type f \( -name "*.exe" -o -name "*.msi" \) | head -20
- name: Submit to SignPath for code signing
if: steps.check-signpath.outputs.enabled == 'true'
@@ -185,8 +178,8 @@ jobs:
organization-id: ${{ secrets.SIGNPATH_ORGANIZATION_ID }}
project-slug: 'ecs-framework'
signing-policy-slug: 'test-signing'
artifact-configuration-slug: 'initial'
github-artifact-id: ${{ steps.get-artifact.outputs.artifact-id }}
artifact-configuration-slug: 'default'
github-artifact-name: 'windows-unsigned'
wait-for-completion: true
wait-for-completion-timeout-in-seconds: 600
output-artifact-directory: './signed'
@@ -197,8 +190,7 @@ jobs:
with:
files: ./signed/*
tag_name: ${{ github.event_name == 'workflow_dispatch' && format('editor-v{0}', github.event.inputs.version) || github.ref_name }}
# 保持 Draft 状态,需要手动发布 | Keep as draft, require manual publish
draft: true
draft: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-4
View File
@@ -90,7 +90,3 @@ docs/.vitepress/dist/
# Tauri 捆绑输出
**/src-tauri/target/release/bundle/
**/src-tauri/target/debug/bundle/
# Rust 构建产物
**/engine-shared/target/
external/
@@ -1,663 +0,0 @@
# ESEngine 材质系统统一架构重构方案
## 问题概述
当前 UI 和 Scene (Sprite) 两套渲染系统存在大量代码重复:
| 重复项 | Sprite | UI | 重复度 |
|--------|--------|----|----|
| 材质属性覆盖接口 | `MaterialPropertyOverride` | `UIMaterialPropertyOverride` | 100% |
| 材质方法 (12个) | `SpriteComponent` | `UIRenderComponent` | 100% |
| ShinyEffect 组件 | `ShinyEffectComponent` | `UIShinyEffectComponent` | 99% |
| ShinyEffect 系统 | `ShinyEffectSystem` | `UIShinyEffectSystem` | 98% |
**根本原因**:缺乏统一的材质覆盖接口抽象层。
---
## 一、统一材质覆盖接口
### 1.1 定义通用接口
`@esengine/material-system` 包中定义统一接口:
```typescript
// packages/material-system/src/interfaces/IMaterialOverridable.ts
/**
* Material property override definition.
* 材质属性覆盖定义。
*/
export interface MaterialPropertyOverride {
type: 'float' | 'vec2' | 'vec3' | 'vec4' | 'color' | 'int';
value: number | number[];
}
export type MaterialOverrides = Record<string, MaterialPropertyOverride>;
/**
* Interface for components that support material property overrides.
* 支持材质属性覆盖的组件接口。
*/
export interface IMaterialOverridable {
/** Material GUID for asset reference | 材质资产引用的 GUID */
materialGuid: string;
/** Current material overrides | 当前材质覆盖 */
readonly materialOverrides: MaterialOverrides;
/** Get current material ID | 获取当前材质 ID */
getMaterialId(): number;
/** Set material ID | 设置材质 ID */
setMaterialId(id: number): void;
// Uniform setters
setOverrideFloat(name: string, value: number): this;
setOverrideVec2(name: string, x: number, y: number): this;
setOverrideVec3(name: string, x: number, y: number, z: number): this;
setOverrideVec4(name: string, x: number, y: number, z: number, w: number): this;
setOverrideColor(name: string, r: number, g: number, b: number, a?: number): this;
setOverrideInt(name: string, value: number): this;
// Uniform getters
getOverride(name: string): MaterialPropertyOverride | undefined;
removeOverride(name: string): this;
clearOverrides(): this;
hasOverrides(): boolean;
}
```
### 1.2 创建 Mixin 实现
使用 Mixin 模式避免代码重复:
```typescript
// packages/material-system/src/mixins/MaterialOverridableMixin.ts
import type { MaterialPropertyOverride, MaterialOverrides } from '../interfaces/IMaterialOverridable';
/**
* Mixin that provides material override functionality.
* 提供材质覆盖功能的 Mixin。
*/
export function MaterialOverridableMixin<TBase extends new (...args: any[]) => {}>(Base: TBase) {
return class extends Base {
materialGuid: string = '';
private _materialId: number = 0;
private _materialOverrides: MaterialOverrides = {};
get materialOverrides(): MaterialOverrides {
return this._materialOverrides;
}
getMaterialId(): number {
return this._materialId;
}
setMaterialId(id: number): void {
this._materialId = id;
}
setOverrideFloat(name: string, value: number): this {
this._materialOverrides[name] = { type: 'float', value };
return this;
}
setOverrideVec2(name: string, x: number, y: number): this {
this._materialOverrides[name] = { type: 'vec2', value: [x, y] };
return this;
}
setOverrideVec3(name: string, x: number, y: number, z: number): this {
this._materialOverrides[name] = { type: 'vec3', value: [x, y, z] };
return this;
}
setOverrideVec4(name: string, x: number, y: number, z: number, w: number): this {
this._materialOverrides[name] = { type: 'vec4', value: [x, y, z, w] };
return this;
}
setOverrideColor(name: string, r: number, g: number, b: number, a: number = 1.0): this {
this._materialOverrides[name] = { type: 'color', value: [r, g, b, a] };
return this;
}
setOverrideInt(name: string, value: number): this {
this._materialOverrides[name] = { type: 'int', value: Math.floor(value) };
return this;
}
getOverride(name: string): MaterialPropertyOverride | undefined {
return this._materialOverrides[name];
}
removeOverride(name: string): this {
delete this._materialOverrides[name];
return this;
}
clearOverrides(): this {
this._materialOverrides = {};
return this;
}
hasOverrides(): boolean {
return Object.keys(this._materialOverrides).length > 0;
}
};
}
```
---
## 二、Shader Property 元数据系统
### 2.1 定义属性元数据接口
```typescript
// packages/material-system/src/interfaces/IShaderProperty.ts
/**
* Shader property UI metadata.
* 着色器属性 UI 元数据。
*/
export interface ShaderPropertyMeta {
/** Property type | 属性类型 */
type: 'float' | 'vec2' | 'vec3' | 'vec4' | 'color' | 'int' | 'texture';
/** Display label (supports i18n key) | 显示标签(支持 i18n 键) */
label: string;
/** Property group for organization | 属性分组 */
group?: string;
/** Default value | 默认值 */
default?: number | number[] | string;
// Numeric constraints
min?: number;
max?: number;
step?: number;
/** UI hints | UI 提示 */
hint?: 'range' | 'angle' | 'hdr' | 'normal';
/** Tooltip description | 工具提示描述 */
tooltip?: string;
/** Whether to hide in inspector | 是否在检查器中隐藏 */
hidden?: boolean;
}
/**
* Extended shader definition with property metadata.
* 带属性元数据的扩展着色器定义。
*/
export interface ShaderAssetDefinition {
/** Shader name | 着色器名称 */
name: string;
/** Display name for UI | UI 显示名称 */
displayName?: string;
/** Shader description | 着色器描述 */
description?: string;
/** Vertex shader source (inline or path) | 顶点着色器源(内联或路径)*/
vertexSource: string;
/** Fragment shader source (inline or path) | 片段着色器源(内联或路径)*/
fragmentSource: string;
/** Property metadata for inspector | 检查器属性元数据 */
properties?: Record<string, ShaderPropertyMeta>;
/** Render queue / order | 渲染队列/顺序 */
renderQueue?: number;
/** Preset blend mode | 预设混合模式 */
blendMode?: 'alpha' | 'additive' | 'multiply' | 'opaque';
}
```
### 2.2 .shader 资产文件格式
```json
{
"$schema": "esengine://schemas/shader.json",
"version": 1,
"name": "Shiny",
"displayName": "闪光效果 | Shiny Effect",
"description": "扫光高亮动画着色器 | Sweeping highlight animation shader",
"vertexSource": "./shaders/sprite.vert",
"fragmentSource": "./shaders/shiny.frag",
"blendMode": "alpha",
"renderQueue": 2000,
"properties": {
"u_shinyProgress": {
"type": "float",
"label": "进度 | Progress",
"group": "Animation",
"default": 0,
"min": 0,
"max": 1,
"step": 0.01,
"hidden": true
},
"u_shinyWidth": {
"type": "float",
"label": "宽度 | Width",
"group": "Effect",
"default": 0.25,
"min": 0,
"max": 1,
"step": 0.01,
"tooltip": "闪光带宽度 | Width of the shiny band"
},
"u_shinyRotation": {
"type": "float",
"label": "角度 | Rotation",
"group": "Effect",
"default": 2.25,
"min": 0,
"max": 6.28,
"step": 0.01,
"hint": "angle"
},
"u_shinySoftness": {
"type": "float",
"label": "柔和度 | Softness",
"group": "Effect",
"default": 1.0,
"min": 0,
"max": 1,
"step": 0.01
},
"u_shinyBrightness": {
"type": "float",
"label": "亮度 | Brightness",
"group": "Effect",
"default": 1.0,
"min": 0,
"max": 2,
"step": 0.01
},
"u_shinyGloss": {
"type": "float",
"label": "光泽度 | Gloss",
"group": "Effect",
"default": 1.0,
"min": 0,
"max": 1,
"step": 0.01,
"tooltip": "0=白色高光, 1=带颜色 | 0=white shine, 1=color-tinted"
}
}
}
```
---
## 三、统一效果组件/系统架构
### 3.1 抽取通用 ShinyEffect 基类
```typescript
// packages/material-system/src/effects/BaseShinyEffect.ts
import { Component, Property, Serializable, Serialize } from '@esengine/ecs-framework';
/**
* Base shiny effect configuration (shared between UI and Sprite).
* 基础闪光效果配置(UI 和 Sprite 共享)。
*/
export abstract class BaseShinyEffect extends Component {
// ============= Effect Parameters =============
@Serialize()
@Property({ type: 'number', label: 'Width', min: 0, max: 1, step: 0.01 })
public width: number = 0.25;
@Serialize()
@Property({ type: 'number', label: 'Rotation', min: 0, max: 360, step: 1 })
public rotation: number = 129;
@Serialize()
@Property({ type: 'number', label: 'Softness', min: 0, max: 1, step: 0.01 })
public softness: number = 1.0;
@Serialize()
@Property({ type: 'number', label: 'Brightness', min: 0, max: 2, step: 0.01 })
public brightness: number = 1.0;
@Serialize()
@Property({ type: 'number', label: 'Gloss', min: 0, max: 2, step: 0.01 })
public gloss: number = 1.0;
// ============= Animation Settings =============
@Serialize()
@Property({ type: 'boolean', label: 'Play' })
public play: boolean = true;
@Serialize()
@Property({ type: 'boolean', label: 'Loop' })
public loop: boolean = true;
@Serialize()
@Property({ type: 'number', label: 'Duration', min: 0.1, step: 0.1 })
public duration: number = 2.0;
@Serialize()
@Property({ type: 'number', label: 'Loop Delay', min: 0, step: 0.1 })
public loopDelay: number = 2.0;
@Serialize()
@Property({ type: 'number', label: 'Initial Delay', min: 0, step: 0.1 })
public initialDelay: number = 0;
// ============= Runtime State =============
public progress: number = 0;
public elapsedTime: number = 0;
public inDelay: boolean = false;
public delayRemaining: number = 0;
public initialDelayProcessed: boolean = false;
reset(): void {
this.progress = 0;
this.elapsedTime = 0;
this.inDelay = false;
this.delayRemaining = 0;
this.initialDelayProcessed = false;
}
start(): void {
this.reset();
this.play = true;
}
stop(): void {
this.play = false;
}
getRotationRadians(): number {
return this.rotation * Math.PI / 180;
}
}
```
### 3.2 通用动画更新逻辑
```typescript
// packages/material-system/src/effects/ShinyEffectAnimator.ts
import type { BaseShinyEffect } from './BaseShinyEffect';
import type { IMaterialOverridable } from '../interfaces/IMaterialOverridable';
import { BuiltInShaders } from '../types';
/**
* Shared animator logic for shiny effect.
* 闪光效果共享的动画逻辑。
*/
export class ShinyEffectAnimator {
/**
* Update animation state.
* 更新动画状态。
*/
static updateAnimation(shiny: BaseShinyEffect, deltaTime: number): void {
if (!shiny.initialDelayProcessed && shiny.initialDelay > 0) {
shiny.delayRemaining = shiny.initialDelay;
shiny.inDelay = true;
shiny.initialDelayProcessed = true;
}
if (shiny.inDelay) {
shiny.delayRemaining -= deltaTime;
if (shiny.delayRemaining <= 0) {
shiny.inDelay = false;
shiny.elapsedTime = 0;
}
return;
}
shiny.elapsedTime += deltaTime;
shiny.progress = Math.min(shiny.elapsedTime / shiny.duration, 1.0);
if (shiny.progress >= 1.0) {
if (shiny.loop) {
shiny.inDelay = true;
shiny.delayRemaining = shiny.loopDelay;
shiny.progress = 0;
shiny.elapsedTime = 0;
} else {
shiny.play = false;
shiny.progress = 1.0;
}
}
}
/**
* Apply material overrides.
* 应用材质覆盖。
*/
static applyMaterialOverrides(shiny: BaseShinyEffect, target: IMaterialOverridable): void {
if (target.getMaterialId() === 0) {
target.setMaterialId(BuiltInShaders.Shiny);
}
target.setOverrideFloat('u_shinyProgress', shiny.progress);
target.setOverrideFloat('u_shinyWidth', shiny.width);
target.setOverrideFloat('u_shinyRotation', shiny.getRotationRadians());
target.setOverrideFloat('u_shinySoftness', shiny.softness);
target.setOverrideFloat('u_shinyBrightness', shiny.brightness);
target.setOverrideFloat('u_shinyGloss', shiny.gloss);
}
}
```
---
## 四、Material Inspector 设计
### 4.1 组件架构
```
MaterialPropertiesEditor (容器组件)
├── ShaderSelector (着色器选择器)
├── PropertyGroup (属性分组)
│ ├── FloatProperty (浮点属性)
│ ├── VectorProperty (向量属性)
│ ├── ColorProperty (颜色属性)
│ └── TextureProperty (纹理属性)
└── OverrideIndicator (覆盖指示器)
```
### 4.2 核心组件
```typescript
// packages/editor-app/src/components/inspectors/material/MaterialPropertiesEditor.tsx
interface MaterialPropertiesEditorProps {
/** Target component implementing IMaterialOverridable */
target: IMaterialOverridable;
/** Current shader definition with property metadata */
shaderDef?: ShaderAssetDefinition;
/** Callback when property changes */
onChange?: (name: string, value: MaterialPropertyOverride) => void;
}
export const MaterialPropertiesEditor: React.FC<MaterialPropertiesEditorProps> = ({
target,
shaderDef,
onChange
}) => {
// Group properties by their group field
const groupedProps = useMemo(() => {
if (!shaderDef?.properties) return {};
const groups: Record<string, Array<[string, ShaderPropertyMeta]>> = {};
for (const [name, meta] of Object.entries(shaderDef.properties)) {
if (meta.hidden) continue;
const group = meta.group || 'Default';
if (!groups[group]) groups[group] = [];
groups[group].push([name, meta]);
}
return groups;
}, [shaderDef]);
return (
<div className="material-properties-editor">
<ShaderSelector
currentShaderId={target.getMaterialId()}
onSelect={(id) => target.setMaterialId(id)}
/>
{Object.entries(groupedProps).map(([group, props]) => (
<PropertyGroup key={group} title={group}>
{props.map(([name, meta]) => (
<PropertyField
key={name}
name={name}
meta={meta}
value={target.getOverride(name)?.value ?? meta.default}
onChange={(value) => {
applyOverride(target, name, meta.type, value);
onChange?.(name, target.getOverride(name)!);
}}
/>
))}
</PropertyGroup>
))}
</div>
);
};
```
---
## 五、实施计划
### Phase 1: 接口层 (1-2 天)
1. **创建 IMaterialOverridable 接口** (`packages/material-system/src/interfaces/`)
2. **创建 MaterialOverridableMixin** (`packages/material-system/src/mixins/`)
3. **导出新接口** (`packages/material-system/src/index.ts`)
### Phase 2: 重构现有组件 (2-3 天)
1. **修改 SpriteComponent**:实现 `IMaterialOverridable`,使用 Mixin
2. **修改 UIRenderComponent**:实现 `IMaterialOverridable`,使用 Mixin
3. **删除重复代码**:移除各组件中的重复材质方法
### Phase 3: 统一效果系统 (2-3 天)
1. **创建 BaseShinyEffect** (`packages/material-system/src/effects/`)
2. **创建 ShinyEffectAnimator** (`packages/material-system/src/effects/`)
3. **重构 ShinyEffectComponent**:继承 BaseShinyEffect
4. **重构 UIShinyEffectComponent**:继承 BaseShinyEffect
5. **重构系统**:使用 ShinyEffectAnimator
### Phase 4: Shader Property 系统 (2-3 天)
1. **定义 ShaderPropertyMeta 接口**
2. **扩展 ShaderDefinition** 添加 properties 字段
3. **创建 ShaderLoader** 支持 .shader 文件
4. **注册内置着色器属性元数据**
### Phase 5: Material Inspector (3-4 天)
1. **创建 MaterialPropertiesEditor 组件**
2. **创建 PropertyField 组件** (Float, Vector, Color, Texture)
3. **集成到现有 Inspector 系统**
4. **支持实时预览**
---
## 六、文件修改清单
| 优先级 | 包 | 文件 | 操作 |
|--------|-----|------|------|
| P0 | material-system | `src/interfaces/IMaterialOverridable.ts` | 新建 |
| P0 | material-system | `src/mixins/MaterialOverridableMixin.ts` | 新建 |
| P0 | material-system | `src/interfaces/IShaderProperty.ts` | 新建 |
| P1 | material-system | `src/effects/BaseShinyEffect.ts` | 新建 |
| P1 | material-system | `src/effects/ShinyEffectAnimator.ts` | 新建 |
| P1 | sprite | `src/SpriteComponent.ts` | 重构 |
| P1 | ui | `src/components/UIRenderComponent.ts` | 重构 |
| P2 | sprite | `src/ShinyEffectComponent.ts` | 重构 |
| P2 | ui | `src/components/UIShinyEffectComponent.ts` | 重构 |
| P2 | sprite | `src/systems/ShinyEffectSystem.ts` | 重构 |
| P2 | ui | `src/systems/render/UIShinyEffectSystem.ts` | 重构 |
| P3 | material-system | `src/loaders/ShaderLoader.ts` | 扩展 |
| P3 | editor-app | `src/components/inspectors/material/*` | 新建 |
---
## 七、Transform 组件统一(可选)
### 7.1 现状分析
| 特性 | TransformComponent | UITransformComponent |
|------|-------------------|---------------------|
| **坐标系** | 绝对坐标 (position.x/y/z) | 相对锚点坐标 (x/y + anchor) |
| **尺寸** | ❌ 无 | ✅ width/height + 约束 |
| **锚点系统** | ❌ 无 | ✅ anchorMin/Max |
| **3D 支持** | ✅ IVector3 | ❌ 纯 2D |
| **可见性** | ❌ 无 | ✅ visible, alpha |
### 7.2 结论
**不建议完全合并**,但可提取公共基类:
```typescript
// packages/engine-core/src/interfaces/ITransformBase.ts
export interface ITransformBase {
/** 旋转角度(度) | Rotation in degrees */
rotation: number;
/** X 缩放 | Scale X */
scaleX: number;
/** Y 缩放 | Scale Y */
scaleY: number;
/** 本地到世界矩阵 | Local to world matrix */
readonly localToWorldMatrix: Matrix2D;
/** 是否需要更新 | Dirty flag */
isDirty: boolean;
/** 世界坐标 X | World position X */
readonly worldX: number;
/** 世界坐标 Y | World position Y */
readonly worldY: number;
/** 世界旋转 | World rotation */
readonly worldRotation: number;
/** 世界缩放 X | World scale X */
readonly worldScaleX: number;
/** 世界缩放 Y | World scale Y */
readonly worldScaleY: number;
}
```
### 7.3 收益
- 渲染系统可以统一处理 `ITransformBase`
- 减少 SpriteRenderSystem 和 UIRenderSystem 的重复
- Gizmo 系统可以共享变换操作逻辑
---
## 八、向后兼容性
1. **接口兼容**:现有组件的 API 保持不变
2. **序列化兼容**:不改变现有序列化格式
3. **渐进迁移**:可分阶段进行,不影响现有功能
-402
View File
@@ -1,402 +0,0 @@
# Time and Timer System
The ECS framework provides a complete time management and timer system, including time scaling, frame time calculation, and flexible timer scheduling.
## Time Class
The Time class is the core of the framework's time management, providing all game time-related functionality.
### Basic Time Properties
```typescript
import { Time } from '@esengine/ecs-framework';
class GameSystem extends EntitySystem {
protected process(entities: readonly Entity[]): void {
// Get frame time (seconds)
const deltaTime = Time.deltaTime;
// Get unscaled frame time
const unscaledDelta = Time.unscaledDeltaTime;
// Get total game time
const totalTime = Time.totalTime;
// Get current frame count
const frameCount = Time.frameCount;
console.log(`Frame ${frameCount}, delta: ${deltaTime}s, total: ${totalTime}s`);
}
}
```
### Game Pause
The framework provides two pause methods for different scenarios:
#### Core.paused (Recommended)
`Core.paused` is a **true pause** - when set, the entire game loop stops:
```typescript
import { Core } from '@esengine/ecs-framework';
class PauseMenuSystem extends EntitySystem {
public pauseGame(): void {
// True pause - all systems stop executing
Core.paused = true;
console.log('Game paused');
}
public resumeGame(): void {
// Resume game
Core.paused = false;
console.log('Game resumed');
}
public togglePause(): void {
Core.paused = !Core.paused;
console.log(Core.paused ? 'Game paused' : 'Game resumed');
}
}
```
#### Time.timeScale = 0
`Time.timeScale = 0` only makes `deltaTime` become 0, **systems still execute**:
```typescript
class SlowMotionSystem extends EntitySystem {
public freezeTime(): void {
// Time freeze - systems still execute, just deltaTime = 0
Time.timeScale = 0;
}
}
```
#### Comparison
| Feature | `Core.paused = true` | `Time.timeScale = 0` |
|---------|---------------------|---------------------|
| System Execution | Completely stopped | Still running |
| CPU Overhead | Zero | Normal overhead |
| Time Updates | Stopped | Continues (deltaTime=0) |
| Timers | Stopped | Continues (but time doesn't advance) |
| Use Cases | Pause menu, game pause | Slow motion, bullet time effects |
**Recommendations**:
- Pause menu, true game pause → Use `Core.paused = true`
- Slow motion, bullet time effects → Use `Time.timeScale`
### Time Scaling
The Time class supports time scaling for slow motion, fast forward, and other effects:
```typescript
class TimeControlSystem extends EntitySystem {
public enableSlowMotion(): void {
// Set to slow motion (50% speed)
Time.timeScale = 0.5;
console.log('Slow motion enabled');
}
public enableFastForward(): void {
// Set to fast forward (200% speed)
Time.timeScale = 2.0;
console.log('Fast forward enabled');
}
public enableBulletTime(): void {
// Bullet time effect (10% speed)
Time.timeScale = 0.1;
console.log('Bullet time enabled');
}
public resumeNormalSpeed(): void {
// Resume normal speed
Time.timeScale = 1.0;
console.log('Normal speed resumed');
}
protected process(entities: readonly Entity[]): void {
// deltaTime is affected by timeScale
const scaledDelta = Time.deltaTime; // Affected by time scale
const realDelta = Time.unscaledDeltaTime; // Not affected by time scale
for (const entity of entities) {
const movement = entity.getComponent(Movement);
if (movement) {
// Use scaled time for game logic updates
movement.update(scaledDelta);
}
const ui = entity.getComponent(UIComponent);
if (ui) {
// UI animations use real time, not affected by game time scale
ui.update(realDelta);
}
}
}
}
```
### Time Check Utilities
```typescript
class CooldownSystem extends EntitySystem {
private lastAttackTime = 0;
private lastSpawnTime = 0;
constructor() {
super(Matcher.all(Weapon));
}
protected process(entities: readonly Entity[]): void {
// Check attack cooldown
if (Time.checkEvery(1.5, this.lastAttackTime)) {
this.performAttack();
this.lastAttackTime = Time.totalTime;
}
// Check spawn interval
if (Time.checkEvery(3.0, this.lastSpawnTime)) {
this.spawnEnemy();
this.lastSpawnTime = Time.totalTime;
}
}
private performAttack(): void {
console.log('Performing attack!');
}
private spawnEnemy(): void {
console.log('Spawning enemy!');
}
}
```
## Core.schedule Timer System
Core provides powerful timer scheduling functionality for creating one-time or repeating timers.
### Basic Timer Usage
```typescript
import { Core } from '@esengine/ecs-framework';
class GameScene extends Scene {
protected initialize(): void {
// Create one-time timers
this.createOneTimeTimers();
// Create repeating timers
this.createRepeatingTimers();
// Create timers with context
this.createContextTimers();
}
private createOneTimeTimers(): void {
// Execute once after 2 seconds
Core.schedule(2.0, false, null, (timer) => {
console.log('Executed after 2 second delay');
});
// Show tip after 5 seconds
Core.schedule(5.0, false, this, (timer) => {
const scene = timer.getContext<GameScene>();
scene.showTip('Game tip: 5 seconds have passed!');
});
}
private createRepeatingTimers(): void {
// Execute every second
const heartbeatTimer = Core.schedule(1.0, true, null, (timer) => {
console.log(`Game heartbeat - Total time: ${Time.totalTime.toFixed(1)}s`);
});
// Save timer reference for later control
this.saveTimerReference(heartbeatTimer);
}
private createContextTimers(): void {
const gameData = { score: 0, level: 1 };
// Add score every 2 seconds
Core.schedule(2.0, true, gameData, (timer) => {
const data = timer.getContext<typeof gameData>();
data.score += 10;
console.log(`Score increased! Current score: ${data.score}`);
});
}
private saveTimerReference(timer: any): void {
// Can stop timer later
setTimeout(() => {
timer.stop();
console.log('Timer stopped');
}, 10000); // Stop after 10 seconds
}
private showTip(message: string): void {
console.log('Tip:', message);
}
}
```
### Timer Control
```typescript
class TimerControlExample {
private attackTimer: any;
private spawnerTimer: any;
public startCombat(): void {
// Start attack timer
this.attackTimer = Core.schedule(0.5, true, this, (timer) => {
const self = timer.getContext<TimerControlExample>();
self.performAttack();
});
// Start enemy spawn timer
this.spawnerTimer = Core.schedule(3.0, true, null, (timer) => {
this.spawnEnemy();
});
}
public stopCombat(): void {
// Stop all combat-related timers
if (this.attackTimer) {
this.attackTimer.stop();
console.log('Attack timer stopped');
}
if (this.spawnerTimer) {
this.spawnerTimer.stop();
console.log('Spawn timer stopped');
}
}
public resetAttackTimer(): void {
// Reset attack timer
if (this.attackTimer) {
this.attackTimer.reset();
console.log('Attack timer reset');
}
}
private performAttack(): void {
console.log('Performing attack');
}
private spawnEnemy(): void {
console.log('Spawning enemy');
}
}
```
## Best Practices
### 1. Use Appropriate Time Types
```typescript
class MovementSystem extends EntitySystem {
protected process(entities: readonly Entity[]): void {
for (const entity of entities) {
const movement = entity.getComponent(Movement);
// Use scaled time for game logic
movement.position.x += movement.velocity.x * Time.deltaTime;
// Use real time for UI animations (not affected by game pause)
const ui = entity.getComponent(UIAnimation);
if (ui) {
ui.update(Time.unscaledDeltaTime);
}
}
}
}
```
### 2. Timer Management
```typescript
class TimerManager {
private timers: any[] = [];
public createManagedTimer(duration: number, repeats: boolean, callback: () => void): any {
const timer = Core.schedule(duration, repeats, null, callback);
this.timers.push(timer);
return timer;
}
public stopAllTimers(): void {
for (const timer of this.timers) {
timer.stop();
}
this.timers = [];
}
public cleanupCompletedTimers(): void {
this.timers = this.timers.filter(timer => !timer.isDone);
}
}
```
### 3. Avoid Too Many Timers
```typescript
// Avoid: Creating a timer for each entity
class BadExample extends EntitySystem {
protected onAdded(entity: Entity): void {
Core.schedule(1.0, true, entity, (timer) => {
// One timer per entity - poor performance
});
}
}
// Recommended: Manage time uniformly in the system
class GoodExample extends EntitySystem {
private lastUpdateTime = 0;
protected process(entities: readonly Entity[]): void {
// Execute logic once per second
if (Time.checkEvery(1.0, this.lastUpdateTime)) {
this.processAllEntities(entities);
this.lastUpdateTime = Time.totalTime;
}
}
private processAllEntities(entities: readonly Entity[]): void {
// Batch process all entities
}
}
```
### 4. Timer Context Usage
```typescript
interface TimerContext {
entityId: number;
duration: number;
onComplete: () => void;
}
class ContextualTimerExample {
public createEntityTimer(entityId: number, duration: number, onComplete: () => void): void {
const context: TimerContext = {
entityId,
duration,
onComplete
};
Core.schedule(duration, false, context, (timer) => {
const ctx = timer.getContext<TimerContext>();
console.log(`Timer for entity ${ctx.entityId} completed`);
ctx.onComplete();
});
}
}
```
The time and timer system is an essential tool in game development. Using these features correctly will make your game logic more precise and controllable.
+4 -62
View File
@@ -30,64 +30,6 @@ class GameSystem extends EntitySystem {
}
```
### 游戏暂停
框架提供两种暂停方式,适用于不同场景:
#### Core.paused(推荐)
`Core.paused` 是**真正的暂停**,设置后整个游戏循环停止:
```typescript
import { Core } from '@esengine/ecs-framework';
class PauseMenuSystem extends EntitySystem {
public pauseGame(): void {
// 真正暂停 - 所有系统停止执行
Core.paused = true;
console.log('游戏已暂停');
}
public resumeGame(): void {
// 恢复游戏
Core.paused = false;
console.log('游戏已恢复');
}
public togglePause(): void {
Core.paused = !Core.paused;
console.log(Core.paused ? '游戏已暂停' : '游戏已恢复');
}
}
```
#### Time.timeScale = 0
`Time.timeScale = 0` 只是让 `deltaTime` 变为 0**系统仍然在执行**
```typescript
class SlowMotionSystem extends EntitySystem {
public freezeTime(): void {
// 时间冻结 - 系统仍在执行,只是 deltaTime = 0
Time.timeScale = 0;
}
}
```
#### 两种方式对比
| 特性 | `Core.paused = true` | `Time.timeScale = 0` |
|------|---------------------|---------------------|
| 系统执行 | ❌ 完全停止 | ✅ 仍在执行 |
| CPU 开销 | 零 | 正常开销 |
| Time 更新 | ❌ 停止 | ✅ 继续(deltaTime=0 |
| 定时器 | ❌ 停止 | ✅ 继续(但时间不走) |
| 适用场景 | 暂停菜单、游戏暂停 | 慢动作、时间冻结特效 |
**推荐**
- 暂停菜单、真正的游戏暂停 → 使用 `Core.paused = true`
- 慢动作、子弹时间等特效 → 使用 `Time.timeScale`
### 时间缩放
Time 类支持时间缩放功能,可以实现慢动作、快进等效果:
@@ -106,10 +48,10 @@ class TimeControlSystem extends EntitySystem {
console.log('快进模式启用');
}
public enableBulletTime(): void {
// 子弹时间效果(10%速度
Time.timeScale = 0.1;
console.log('子弹时间启用');
public pauseGame(): void {
// 暂停游戏(时间静止
Time.timeScale = 0;
console.log('游戏暂停');
}
public resumeNormalSpeed(): void {
@@ -165,10 +165,7 @@ export function inferAssetType(path: string): AssetType {
btree: 'behavior-tree',
bp: 'blueprint',
mat: 'material',
particle: 'particle',
// FairyGUI
fui: 'fui'
particle: 'particle'
};
return typeMap[ext] || 'binary';
@@ -10,47 +10,6 @@ import {
IAssetCatalogEntry
} from '../types/AssetTypes';
/**
* 纹理 Sprite 信息(从 meta 文件的 importSettings 读取)
* Texture sprite info (read from meta file's importSettings)
*/
export interface ITextureSpriteInfo {
/**
* 九宫格切片边距 [top, right, bottom, left]
* Nine-patch slice border
*/
sliceBorder?: [number, number, number, number];
/**
* Sprite 锚点 [x, y]0-1 归一化)
* Sprite pivot point (0-1 normalized)
*/
pivot?: [number, number];
/**
* 纹理宽度(可选,需要纹理已加载)
* Texture width (optional, requires texture to be loaded)
*/
width?: number;
/**
* 纹理高度(可选,需要纹理已加载)
* Texture height (optional, requires texture to be loaded)
*/
height?: number;
}
/**
* Sprite settings in import settings
* 导入设置中的 Sprite 设置
*/
interface ISpriteSettings {
sliceBorder?: [number, number, number, number];
pivot?: [number, number];
pixelsPerUnit?: number;
/** Texture width (from import settings) | 纹理宽度(来自导入设置) */
width?: number;
/** Texture height (from import settings) | 纹理高度(来自导入设置) */
height?: number;
}
/**
* Asset database implementation
* 资产数据库实现
@@ -253,41 +212,6 @@ export class AssetDatabase {
return guid ? this._metadata.get(guid) : undefined;
}
/**
* Get texture sprite info from metadata
* 从元数据获取纹理 Sprite 信息
*
* Extracts spriteSettings from importSettings if available.
* 如果可用,从 importSettings 提取 spriteSettings。
*
* @param guid - Texture asset GUID | 纹理资产 GUID
* @returns Sprite info or undefined if not found/not a texture | Sprite 信息或未找到/非纹理则为 undefined
*/
getTextureSpriteInfo(guid: AssetGUID): ITextureSpriteInfo | undefined {
const metadata = this._metadata.get(guid);
if (!metadata) return undefined;
// Check if it's a texture asset
// 检查是否是纹理资产
if (metadata.type !== AssetType.Texture) return undefined;
// Extract spriteSettings from importSettings
// 从 importSettings 提取 spriteSettings
const importSettings = metadata.importSettings as Record<string, unknown> | undefined;
const spriteSettings = importSettings?.spriteSettings as ISpriteSettings | undefined;
if (!spriteSettings) return undefined;
return {
sliceBorder: spriteSettings.sliceBorder,
pivot: spriteSettings.pivot,
// Include dimensions from import settings if available
// 如果可用,包含来自导入设置的尺寸
width: spriteSettings.width,
height: spriteSettings.height
};
}
/**
* Find assets by type
* 按类型查找资产
@@ -132,10 +132,7 @@ export class AssetManager implements IAssetManager {
labels: [],
tags: new Map(),
lastModified: Date.now(),
version: 1,
// Include importSettings for sprite slicing (nine-patch), etc.
// 包含 importSettings 以支持精灵切片(九宫格)等功能
importSettings: entry.importSettings
version: 1
};
this._database.addAsset(metadata);
+1 -13
View File
@@ -36,7 +36,6 @@ export { RuntimeCatalog, runtimeCatalog } from './runtime/RuntimeCatalog';
export * from './interfaces/IAssetLoader';
export * from './interfaces/IAssetManager';
export * from './interfaces/IAssetReader';
export * from './interfaces/IAssetFileLoader';
export * from './interfaces/IResourceComponent';
// Core
@@ -59,24 +58,13 @@ export { PrefabLoader } from './loaders/PrefabLoader';
// Integration
export { EngineIntegration } from './integration/EngineIntegration';
export type { ITextureEngineBridge, TextureLoadCallback } from './integration/EngineIntegration';
export type { ITextureEngineBridge } from './integration/EngineIntegration';
// Services
export { SceneResourceManager } from './services/SceneResourceManager';
export type { IResourceLoader } from './services/SceneResourceManager';
export { PathResolutionService } from './services/PathResolutionService';
// Asset Metadata Service (primary API for sprite info)
// 资产元数据服务(sprite 信息的主要 API)
export {
setGlobalAssetDatabase,
getGlobalAssetDatabase,
setGlobalEngineBridge,
getGlobalEngineBridge,
getTextureSpriteInfo
} from './services/AssetMetadataService';
export type { ITextureSpriteInfo } from './core/AssetDatabase';
// Utils
export { UVHelper } from './utils/UVHelper';
export {
@@ -31,6 +31,12 @@ export interface ITextureEngineBridge {
*/
unloadTexture(id: number): void;
/**
* Get texture info
* 获取纹理信息
*/
getTextureInfo(id: number): { width: number; height: number } | null;
/**
* Get or load texture by path.
* 按路径获取或加载纹理。
@@ -103,20 +109,6 @@ export interface ITextureEngineBridge {
* @returns Promise that resolves when texture is ready | 纹理就绪时解析的 Promise
*/
loadTextureAsync?(id: number, url: string): Promise<void>;
/**
* Get texture info by path.
* 通过路径获取纹理信息。
*
* This is the primary API for getting texture dimensions.
* The Rust engine is the single source of truth for texture dimensions.
* 这是获取纹理尺寸的主要 API。
* Rust 引擎是纹理尺寸的唯一事实来源。
*
* @param path Image path/URL | 图片路径/URL
* @returns Texture info or null if not loaded | 纹理信息或未加载则为 null
*/
getTextureInfoByPath?(path: string): { width: number; height: number } | null;
}
/**
@@ -139,43 +131,10 @@ interface DataAssetEntry {
path: string;
}
/**
* Texture load callback type
* 纹理加载回调类型
*/
export type TextureLoadCallback = (guid: string, path: string, textureId: number) => void;
/**
* Asset system engine integration
* 资产系统引擎集成
*/
/**
* Texture sprite info (nine-patch border, pivot, etc.)
* 纹理 Sprite 信息(九宫格边距、锚点等)
*/
export interface ITextureSpriteInfo {
/**
* 九宫格切片边距 [top, right, bottom, left]
* Nine-patch slice border
*/
sliceBorder?: [number, number, number, number];
/**
* Sprite 锚点 [x, y]0-1 归一化)
* Sprite pivot point (0-1 normalized)
*/
pivot?: [number, number];
/**
* 纹理宽度
* Texture width
*/
width: number;
/**
* 纹理高度
* Texture height
*/
height: number;
}
export class EngineIntegration {
private _assetManager: AssetManager;
private _engineBridge?: ITextureEngineBridge;
@@ -187,54 +146,6 @@ export class EngineIntegration {
// Path-stable ID cache (persists across Play/Stop cycles)
private static _pathIdCache = new Map<string, number>();
// 纹理 Sprite 信息缓存(全局静态,可供渲染系统访问)
// Texture sprite info cache (global static, accessible by render systems)
private static _textureSpriteInfoCache = new Map<AssetGUID, ITextureSpriteInfo>();
// 纹理加载回调(用于动态图集集成等)
// Texture load callback (for dynamic atlas integration, etc.)
private static _textureLoadCallbacks: TextureLoadCallback[] = [];
/**
* Register a callback to be called when textures are loaded
* 注册纹理加载时调用的回调
*
* This can be used for dynamic atlas integration.
* 可用于动态图集集成。
*
* @param callback - Callback function | 回调函数
*/
static onTextureLoad(callback: TextureLoadCallback): void {
if (!EngineIntegration._textureLoadCallbacks.includes(callback)) {
EngineIntegration._textureLoadCallbacks.push(callback);
}
}
/**
* Remove a texture load callback
* 移除纹理加载回调
*/
static removeTextureLoadCallback(callback: TextureLoadCallback): void {
const index = EngineIntegration._textureLoadCallbacks.indexOf(callback);
if (index >= 0) {
EngineIntegration._textureLoadCallbacks.splice(index, 1);
}
}
/**
* Notify all callbacks of a texture load
* 通知所有回调纹理已加载
*/
private static notifyTextureLoad(guid: string, path: string, textureId: number): void {
for (const callback of EngineIntegration._textureLoadCallbacks) {
try {
callback(guid, path, textureId);
} catch (e) {
console.error('[EngineIntegration] Error in texture load callback:', e);
}
}
}
// Audio resource mappings | 音频资源映射
private _audioIdMap = new Map<AssetGUID, number>();
private _pathToAudioId = new Map<string, number>();
@@ -368,16 +279,6 @@ export class EngineIntegration {
const result = await this._assetManager.loadAsset<ITextureAsset>(guid);
const metadata = result.metadata;
const assetPath = metadata.path;
const textureAsset = result.asset;
// 缓存 sprite 信息(九宫格边距等)到静态缓存
// Cache sprite info (slice border, etc.) to static cache
EngineIntegration._textureSpriteInfoCache.set(guid, {
sliceBorder: textureAsset.sliceBorder,
pivot: textureAsset.pivot,
width: textureAsset.width,
height: textureAsset.height
});
// 生成路径稳定 ID
// Generate path-stable ID
@@ -408,37 +309,9 @@ export class EngineIntegration {
this._textureIdMap.set(guid, stableId);
this._pathToTextureId.set(assetPath, stableId);
// 通知回调(用于动态图集等)
// Notify callbacks (for dynamic atlas, etc.)
EngineIntegration.notifyTextureLoad(guid, engineUrl, stableId);
return stableId;
}
/**
* Get texture sprite info by GUID (static method for render system access)
* 通过 GUID 获取纹理 Sprite 信息(静态方法,供渲染系统访问)
*
* Returns cached sprite info including nine-patch slice border.
* Must call loadTextureByGuid first to populate the cache.
* 返回缓存的 sprite 信息,包括九宫格边距。
* 必须先调用 loadTextureByGuid 来填充缓存。
*
* @param guid - Texture asset GUID | 纹理资产 GUID
* @returns Sprite info or undefined if not loaded | Sprite 信息或未加载则为 undefined
*/
static getTextureSpriteInfo(guid: AssetGUID): ITextureSpriteInfo | undefined {
return EngineIntegration._textureSpriteInfoCache.get(guid);
}
/**
* Clear texture sprite info cache
* 清除纹理 Sprite 信息缓存
*/
static clearTextureSpriteInfoCache(): void {
EngineIntegration._textureSpriteInfoCache.clear();
}
/**
* Batch load textures
* 批量加载纹理
@@ -1,103 +0,0 @@
/**
* Asset File Loader Interface
* 资产文件加载器接口
*
* High-level file loading abstraction that combines path resolution
* with platform-specific file reading.
* 高级文件加载抽象,结合路径解析和平台特定的文件读取。
*
* This is the unified entry point for all file loading in the engine.
* Different from IAssetLoader (which parses content), this interface
* handles the actual file fetching from asset paths.
* 这是引擎中所有文件加载的统一入口。
* 与 IAssetLoader(解析内容)不同,此接口处理从资产路径获取文件。
*/
/**
* Asset file loader interface.
* 资产文件加载器接口。
*
* Provides a unified API for loading files from asset paths (relative to project).
* Different platforms provide their own implementations.
* 提供从资产路径(相对于项目)加载文件的统一 API。
* 不同平台提供各自的实现。
*
* @example
* ```typescript
* // Get global loader
* const loader = getGlobalAssetFileLoader();
*
* // Load image from asset path (relative to project)
* const image = await loader.loadImage('assets/demo/button.png');
*
* // Load text content
* const json = await loader.loadText('assets/config.json');
* ```
*/
export interface IAssetFileLoader {
/**
* Load image from asset path.
* 从资产路径加载图片。
*
* @param assetPath - Asset path relative to project (e.g., "assets/demo/button.png").
* 相对于项目的资产路径。
* @returns Promise resolving to HTMLImageElement. | 返回 HTMLImageElement 的 Promise。
*/
loadImage(assetPath: string): Promise<HTMLImageElement>;
/**
* Load text content from asset path.
* 从资产路径加载文本内容。
*
* @param assetPath - Asset path relative to project. | 相对于项目的资产路径。
* @returns Promise resolving to text content. | 返回文本内容的 Promise。
*/
loadText(assetPath: string): Promise<string>;
/**
* Load binary data from asset path.
* 从资产路径加载二进制数据。
*
* @param assetPath - Asset path relative to project. | 相对于项目的资产路径。
* @returns Promise resolving to ArrayBuffer. | 返回 ArrayBuffer 的 Promise。
*/
loadBinary(assetPath: string): Promise<ArrayBuffer>;
/**
* Check if asset file exists.
* 检查资产文件是否存在。
*
* @param assetPath - Asset path relative to project. | 相对于项目的资产路径。
* @returns Promise resolving to boolean. | 返回布尔值的 Promise。
*/
exists(assetPath: string): Promise<boolean>;
}
/**
* Global asset file loader instance.
* 全局资产文件加载器实例。
*/
let globalAssetFileLoader: IAssetFileLoader | null = null;
/**
* Set the global asset file loader.
* 设置全局资产文件加载器。
*
* Should be called during engine initialization with platform-specific implementation.
* 应在引擎初始化期间使用平台特定的实现调用。
*
* @param loader - Asset file loader instance or null. | 资产文件加载器实例或 null。
*/
export function setGlobalAssetFileLoader(loader: IAssetFileLoader | null): void {
globalAssetFileLoader = loader;
}
/**
* Get the global asset file loader.
* 获取全局资产文件加载器。
*
* @returns Asset file loader instance or null. | 资产文件加载器实例或 null。
*/
export function getGlobalAssetFileLoader(): IAssetFileLoader | null {
return globalAssetFileLoader;
}
@@ -144,24 +144,6 @@ export interface ITextureAsset {
hasMipmaps: boolean;
/** 原始数据(如果可用) / Raw image data if available */
data?: ImageData | HTMLImageElement;
// ===== Sprite Settings =====
// ===== Sprite 设置 =====
/**
* 九宫格切片边距 [top, right, bottom, left]
* Nine-patch slice border
*
* Defines the non-stretchable borders for nine-patch rendering.
* 定义九宫格渲染时不可拉伸的边框区域。
*/
sliceBorder?: [number, number, number, number];
/**
* Sprite 锚点 [x, y]0-1 归一化)
* Sprite pivot point (0-1 normalized)
*/
pivot?: [number, number];
}
/**
@@ -201,109 +183,24 @@ export interface IAudioAsset {
channels: number;
}
/**
* Shader property type
* 着色器属性类型
*/
export type ShaderPropertyType = 'float' | 'vec2' | 'vec3' | 'vec4' | 'int' | 'sampler2D' | 'mat3' | 'mat4';
/**
* Shader property definition
* 着色器属性定义
*/
export interface IShaderProperty {
/** 属性名称(uniform 名) / Property name (uniform name) */
name: string;
/** 属性类型 / Property type */
type: ShaderPropertyType;
/** 默认值 / Default value */
default: number | number[];
/** 显示名称(编辑器用) / Display name for editor */
displayName?: string;
/** 值范围(用于 float/int / Value range for float/int */
range?: [number, number];
/** 是否隐藏(内部使用) / Hidden from inspector */
hidden?: boolean;
}
/**
* Shader asset interface
* 着色器资产接口
*
* Shader assets contain GLSL source code and property definitions.
* 着色器资产包含 GLSL 源代码和属性定义。
*/
export interface IShaderAsset {
/** 着色器名称 / Shader name (e.g., "UI/Shiny") */
name: string;
/** 顶点着色器源代码 / Vertex shader GLSL source */
vertex: string;
/** 片段着色器源代码 / Fragment shader GLSL source */
fragment: string;
/** 属性定义列表 / Property definitions */
properties: IShaderProperty[];
/** 编译后的着色器 ID(运行时填充) / Compiled shader ID (runtime) */
shaderId?: number;
}
/**
* Material property value
* 材质属性值
*/
export type MaterialPropertyValue = number | number[] | string;
/**
* Material animator configuration
* 材质动画器配置
*/
export interface IMaterialAnimator {
/** 要动画的属性名 / Property to animate */
property: string;
/** 起始值 / Start value */
from: number;
/** 结束值 / End value */
to: number;
/** 持续时间(秒) / Duration in seconds */
duration: number;
/** 是否循环 / Loop animation */
loop?: boolean;
/** 循环间隔(秒) / Delay between loops */
loopDelay?: number;
/** 缓动函数 / Easing function */
easing?: 'linear' | 'easeIn' | 'easeOut' | 'easeInOut';
/** 是否自动播放 / Auto play on start */
autoPlay?: boolean;
}
/**
* Material asset interface
* 材质资产接口
*
* Material assets reference a shader and define property values.
* 材质资产引用着色器并定义属性值。
*/
export interface IMaterialAsset {
/** 材质名称 / Material name */
name: string;
/** 着色器 GUID 或内置路径 / Shader GUID or built-in path (e.g., "builtin://shaders/Shiny") */
/** 着色器名称 / Shader name */
shader: string;
/** 材质属性 / Material property values */
properties: Record<string, MaterialPropertyValue>;
/** 纹理映射 / Texture slot mappings (property name -> texture GUID) */
textures?: Record<string, AssetGUID>;
/** 材质属性 / Material properties */
properties: Map<string, unknown>;
/** 纹理映射 / Texture slot mappings */
textures: Map<string, AssetGUID>;
/** 渲染状态 / Render states */
renderStates?: {
renderStates: {
cullMode?: 'none' | 'front' | 'back';
blendMode?: 'none' | 'alpha' | 'additive' | 'multiply' | 'screen';
blendMode?: 'none' | 'alpha' | 'additive' | 'multiply';
depthTest?: boolean;
depthWrite?: boolean;
};
/** 动画器配置(可选) / Animator configuration (optional) */
animator?: IMaterialAnimator;
/** 运行时:编译后的着色器 ID / Runtime: compiled shader ID */
_shaderId?: number;
/** 运行时:引擎材质 ID / Runtime: engine material ID */
_materialId?: number;
}
// 预制体资产接口从专用文件导出 | Prefab asset interface exported from dedicated file
@@ -46,9 +46,6 @@ export class AssetLoaderFactory implements IAssetLoaderFactory {
// 预制体加载器 / Prefab loader
this._loaders.set(AssetType.Prefab, new PrefabLoader());
// 注:Shader 和 Material 加载器由 material-system 模块注册
// Note: Shader and Material loaders are registered by material-system module
}
/**
@@ -16,16 +16,6 @@ interface IEngineBridgeGlobal {
unloadTexture?(textureId: number): void;
}
/**
* Sprite settings from texture meta
* 纹理 meta 中的 Sprite 设置
*/
interface ISpriteSettings {
sliceBorder?: [number, number, number, number];
pivot?: [number, number];
pixelsPerUnit?: number;
}
/**
* 获取全局引擎桥接
* Get global engine bridge
@@ -71,22 +61,13 @@ export class TextureLoader implements IAssetLoader<ITextureAsset> {
const image = content.image;
// Read sprite settings from import settings
// 从导入设置读取 sprite 设置
const importSettings = context.metadata.importSettings as Record<string, unknown> | undefined;
const spriteSettings = importSettings?.spriteSettings as ISpriteSettings | undefined;
const textureAsset: ITextureAsset = {
textureId: TextureLoader._nextTextureId++,
width: image.width,
height: image.height,
format: 'rgba',
hasMipmaps: false,
data: image,
// Include sprite settings if available
// 如果有则包含 sprite 设置
sliceBorder: spriteSettings?.sliceBorder,
pivot: spriteSettings?.pivot
data: image
};
// Upload to GPU if bridge exists.
@@ -1,139 +0,0 @@
/**
* Asset Metadata Service
* 资产元数据服务
*
* Provides global access to asset metadata without requiring asset loading.
* This service is independent of the texture loading path, allowing
* render systems to query sprite info regardless of how textures are loaded.
*
* 提供对资产元数据的全局访问,无需加载资产。
* 此服务独立于纹理加载路径,允许渲染系统查询 sprite 信息,
* 无论纹理是如何加载的。
*/
import { AssetDatabase, ITextureSpriteInfo } from '../core/AssetDatabase';
import type { AssetGUID } from '../types/AssetTypes';
import type { ITextureEngineBridge } from '../integration/EngineIntegration';
/**
* Global asset database instance
* 全局资产数据库实例
*/
let globalAssetDatabase: AssetDatabase | null = null;
/**
* Global engine bridge instance
* 全局引擎桥实例
*
* Used to query texture dimensions from Rust engine (single source of truth).
* 用于从 Rust 引擎查询纹理尺寸(唯一事实来源)。
*/
let globalEngineBridge: ITextureEngineBridge | null = null;
/**
* Set the global asset database
* 设置全局资产数据库
*
* Should be called during engine initialization.
* 应在引擎初始化期间调用。
*
* @param database - AssetDatabase instance | AssetDatabase 实例
*/
export function setGlobalAssetDatabase(database: AssetDatabase | null): void {
globalAssetDatabase = database;
}
/**
* Get the global asset database
* 获取全局资产数据库
*
* @returns AssetDatabase instance or null | AssetDatabase 实例或 null
*/
export function getGlobalAssetDatabase(): AssetDatabase | null {
return globalAssetDatabase;
}
/**
* Set the global engine bridge
* 设置全局引擎桥
*
* The engine bridge is used to query texture dimensions directly from Rust engine.
* This is the single source of truth for texture dimensions.
* 引擎桥用于直接从 Rust 引擎查询纹理尺寸。
* 这是纹理尺寸的唯一事实来源。
*
* @param bridge - ITextureEngineBridge instance | ITextureEngineBridge 实例
*/
export function setGlobalEngineBridge(bridge: ITextureEngineBridge | null): void {
globalEngineBridge = bridge;
}
/**
* Get the global engine bridge
* 获取全局引擎桥
*
* @returns ITextureEngineBridge instance or null | ITextureEngineBridge 实例或 null
*/
export function getGlobalEngineBridge(): ITextureEngineBridge | null {
return globalEngineBridge;
}
/**
* Get texture sprite info by GUID
* 通过 GUID 获取纹理 Sprite 信息
*
* This is the primary API for render systems to query nine-patch/sprite info.
* It combines data from:
* - Asset metadata (sliceBorder, pivot) from AssetDatabase
* - Texture dimensions (width, height) from Rust engine (single source of truth)
*
* 这是渲染系统查询九宫格/sprite 信息的主要 API。
* 它合并来自:
* - AssetDatabase 的资产元数据(sliceBorder, pivot
* - Rust 引擎的纹理尺寸(width, height)(唯一事实来源)
*
* @param guid - Texture asset GUID | 纹理资产 GUID
* @returns Sprite info or undefined | Sprite 信息或 undefined
*/
export function getTextureSpriteInfo(guid: AssetGUID): ITextureSpriteInfo | undefined {
// Get sprite settings from metadata
// 从元数据获取 sprite 设置
const metadataInfo = globalAssetDatabase?.getTextureSpriteInfo(guid);
// Get texture dimensions from Rust engine (single source of truth)
// 从 Rust 引擎获取纹理尺寸(唯一事实来源)
let dimensions: { width: number; height: number } | undefined;
if (globalEngineBridge?.getTextureInfoByPath && globalAssetDatabase) {
// Get asset path from database
// 从数据库获取资产路径
const metadata = globalAssetDatabase.getMetadata(guid);
if (metadata?.path) {
const engineInfo = globalEngineBridge.getTextureInfoByPath(metadata.path);
if (engineInfo) {
dimensions = engineInfo;
}
}
}
// If no metadata and no dimensions, return undefined
// 如果没有元数据也没有尺寸,返回 undefined
if (!metadataInfo && !dimensions) {
return undefined;
}
// Merge the two sources
// 合并两个数据源
// Prefer engine dimensions (runtime loaded), fallback to metadata dimensions (catalog stored)
// 优先使用引擎尺寸(运行时加载),后备使用元数据尺寸(目录存储)
return {
sliceBorder: metadataInfo?.sliceBorder,
pivot: metadataInfo?.pivot,
width: dimensions?.width ?? metadataInfo?.width,
height: dimensions?.height ?? metadataInfo?.height
};
}
// Re-export type for convenience
// 为方便起见重新导出类型
export type { ITextureSpriteInfo };
@@ -406,12 +406,6 @@ export interface IAssetCatalogEntry {
/** 可用变体 / Available variants (platform/quality specific) */
variants?: IAssetVariant[];
/**
* Import settings (e.g., sprite slicing for nine-patch)
* 导入设置(如九宫格切片信息)
*/
importSettings?: Record<string, unknown>;
}
/**
+1
View File
@@ -32,6 +32,7 @@
{ "path": "../core" },
{ "path": "../engine-core" },
{ "path": "../editor-core" },
{ "path": "../ui" },
{ "path": "../editor-runtime" }
]
}
+2 -2
View File
@@ -15,13 +15,13 @@ import type {
import {
EntityStoreService,
MessageHub,
EditorComponentRegistry
ComponentRegistry
} from '@esengine/editor-core';
import { CameraComponent } from '@esengine/camera';
export class CameraEditorModule implements IEditorModuleLoader {
async install(services: ServiceContainer): Promise<void> {
const componentRegistry = services.resolve(EditorComponentRegistry);
const componentRegistry = services.resolve(ComponentRegistry);
if (componentRegistry) {
componentRegistry.register({
name: 'Camera',
+6 -6
View File
@@ -1,6 +1,6 @@
import type { IComponentRegistry, IScene } from '@esengine/ecs-framework';
import type { IRuntimeModule, IRuntimePlugin, ModuleManifest, SystemContext } from '@esengine/engine-core';
import { RenderConfigServiceToken } from '@esengine/engine-core';
import { EngineBridgeToken } from '@esengine/engine-core';
import { CameraComponent } from './CameraComponent';
import { CameraSystem } from './CameraSystem';
@@ -10,15 +10,15 @@ class CameraRuntimeModule implements IRuntimeModule {
}
createSystems(scene: IScene, context: SystemContext): void {
// 从服务注册表获取渲染配置服务 | Get render config service from registry
const renderConfig = context.services.get(RenderConfigServiceToken);
if (!renderConfig) {
console.warn('[CameraPlugin] RenderConfigService not found, CameraSystem will not be created');
// 从服务注册表获取 EngineBridge | Get EngineBridge from service registry
const bridge = context.services.get(EngineBridgeToken);
if (!bridge) {
console.warn('[CameraPlugin] EngineBridge not found, CameraSystem will not be created');
return;
}
// 创建并添加 CameraSystem | Create and add CameraSystem
const cameraSystem = new CameraSystem(renderConfig);
const cameraSystem = new CameraSystem(bridge);
scene.addSystem(cameraSystem);
}
}
+5 -5
View File
@@ -4,18 +4,18 @@
*/
import { EntitySystem, Matcher, Entity, ECSSystem } from '@esengine/ecs-framework';
import type { IRenderConfigService } from '@esengine/engine-core';
import type { IEngineBridge } from '@esengine/engine-core';
import { CameraComponent } from './CameraComponent';
@ECSSystem('Camera', { updateOrder: -100 })
export class CameraSystem extends EntitySystem {
private renderConfig: IRenderConfigService;
private bridge: IEngineBridge;
private lastAppliedCameraId: number | null = null;
constructor(renderConfig: IRenderConfigService) {
constructor(bridge: IEngineBridge) {
// Match entities with CameraComponent
super(Matcher.empty().all(CameraComponent));
this.renderConfig = renderConfig;
this.bridge = bridge;
}
protected override onBegin(): void {
@@ -47,6 +47,6 @@ export class CameraSystem extends EntitySystem {
const r = parseInt(bgColor.slice(1, 3), 16) / 255;
const g = parseInt(bgColor.slice(3, 5), 16) / 255;
const b = parseInt(bgColor.slice(5, 7), 16) / 255;
this.renderConfig.setClearColor(r, g, b, 1.0);
this.bridge.setClearColor(r, g, b, 1.0);
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@esengine/ecs-framework",
"version": "2.4.1",
"version": "2.4.0",
"description": "用于Laya、Cocos Creator等JavaScript游戏引擎的高性能ECS框架",
"main": "dist/index.cjs",
"module": "dist/index.mjs",
+2 -2
View File
@@ -18,7 +18,7 @@ const updatableMetadata = new WeakMap<Constructor, UpdatableMetadata>();
/**
* 可注入元数据接口
*/
export type InjectableMetadata = {
export interface InjectableMetadata {
/**
* 是否可注入
*/
@@ -39,7 +39,7 @@ export type InjectableMetadata = {
/**
* 可更新元数据接口
*/
export type UpdatableMetadata = {
export interface UpdatableMetadata {
/**
* 是否可更新
*/
+2 -2
View File
@@ -51,7 +51,7 @@ export enum PluginState {
* }
* ```
*/
export type IPlugin = {
export interface IPlugin {
/**
* 插件唯一名称
*
@@ -96,7 +96,7 @@ export type IPlugin = {
/**
* 插件元数据
*/
export type IPluginMetadata = {
export interface IPluginMetadata {
/**
* 插件名称
*/
@@ -28,7 +28,7 @@
* Note: __phantom is a required property to ensure TypeScript preserves generic
* type information across packages.
*/
export type ServiceToken<T> = {
export interface ServiceToken<T> {
readonly id: symbol;
readonly name: string;
/**
+2 -2
View File
@@ -39,7 +39,7 @@ import { createServiceToken } from './PluginServiceRegistry';
* 运行时模式接口
* Runtime mode interface
*/
export type IRuntimeMode = {
export interface IRuntimeMode {
/**
* 是否为编辑器模式
* Whether in editor mode
@@ -110,7 +110,7 @@ type ModeChangeCallback = (mode: IRuntimeMode) => void;
* 运行时模式服务配置
* Runtime mode service configuration
*/
export type RuntimeModeConfig = {
export interface RuntimeModeConfig {
/** 是否为编辑器模式 | Whether in editor mode */
isEditor?: boolean;
/** 是否正在播放 | Whether playing */
+1 -1
View File
@@ -7,7 +7,7 @@ const logger = createLogger('ServiceContainer');
* 服务基础接口
* 所有通过 ServiceContainer 管理的服务都应该实现此接口
*/
export type IService = {
export interface IService {
/**
* 释放服务占用的资源
* 当服务被注销或容器被清空时调用
@@ -11,7 +11,7 @@ export type ArchetypeId = BitMask64Data;
/**
* 原型数据结构
*/
export type Archetype = {
export interface Archetype {
/** 原型唯一标识符 */
id: ArchetypeId;
/** 包含的组件类型 */
@@ -23,7 +23,7 @@ export type Archetype = {
/**
* 原型查询结果
*/
export type ArchetypeQueryResult = {
export interface ArchetypeQueryResult {
/** 匹配的原型列表 */
archetypes: Archetype[];
/** 所有匹配实体的总数 */
+1 -1
View File
@@ -25,7 +25,7 @@ export enum CommandType {
* 延迟命令接口
* Deferred command interface
*/
export type DeferredCommand = {
export interface DeferredCommand {
/** 命令类型 | Command type */
type: CommandType;
/** 目标实体 | Target entity */
@@ -1,7 +1,6 @@
import { Component } from '../Component';
import { BitMask64Utils, BitMask64Data } from '../Utils/BigIntCompatibility';
import { SoAStorage } from './SoAStorage';
import type { SupportedTypedArray } from './SoAStorage';
import { SoAStorage, SupportedTypedArray } from './SoAStorage';
import { createLogger } from '../../Utils/Logger';
import { getComponentTypeName, ComponentType } from '../Decorators';
import { ComponentRegistry, GlobalComponentRegistry } from './ComponentStorage/ComponentRegistry';
@@ -14,11 +14,7 @@ import type { Component } from '../../Component';
/**
* 组件类型定义
* Component type definition
*
* 注意:构造函数参数使用 any[] 是必要的,因为组件可以有各种不同签名的构造函数
* Note: Constructor args use any[] because components can have various constructor signatures
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type ComponentType<T extends Component = Component> = new (...args: any[]) => T;
/**
@@ -43,7 +39,7 @@ export const COMPONENT_EDITOR_OPTIONS = Symbol('ComponentEditorOptions');
* 组件编辑器选项
* Component editor options
*/
export type ComponentEditorOptions = {
export interface ComponentEditorOptions {
/**
* 是否在 Inspector 中隐藏此组件
* Whether to hide this component in Inspector
@@ -65,51 +61,6 @@ export type ComponentEditorOptions = {
icon?: string;
}
/**
* 组件构造函数上的元数据接口
* Metadata interface stored on component constructors
*
* 使用 Symbol 索引签名来类型安全地访问装饰器存储的元数据
* Uses Symbol index signature to safely access decorator-stored metadata
*/
export type ComponentTypeMetadata = {
readonly [COMPONENT_TYPE_NAME]?: string;
readonly [COMPONENT_DEPENDENCIES]?: string[];
readonly [COMPONENT_EDITOR_OPTIONS]?: ComponentEditorOptions;
}
/**
* 可写的组件类型元数据(用于装饰器设置)
* Writable component type metadata (for decorator setting)
*/
export type WritableComponentTypeMetadata = {
[COMPONENT_TYPE_NAME]?: string;
[COMPONENT_DEPENDENCIES]?: string[];
[COMPONENT_EDITOR_OPTIONS]?: ComponentEditorOptions;
}
/**
* 获取组件构造函数的元数据
* Get metadata from component constructor
*
* @param componentType 组件构造函数
* @returns 元数据对象
*/
export function getComponentTypeMetadata(componentType: ComponentType): ComponentTypeMetadata {
return componentType as unknown as ComponentTypeMetadata;
}
/**
* 获取可写的组件构造函数元数据(用于装饰器)
* Get writable metadata from component constructor (for decorators)
*
* @param componentType 组件构造函数
* @returns 可写的元数据对象
*/
export function getWritableComponentTypeMetadata(componentType: ComponentType): WritableComponentTypeMetadata {
return componentType as unknown as WritableComponentTypeMetadata;
}
/**
* 检查组件是否使用了 @ECSComponent 装饰器
* Check if component has @ECSComponent decorator
@@ -118,8 +69,7 @@ export function getWritableComponentTypeMetadata(componentType: ComponentType):
* @returns 是否有装饰器
*/
export function hasECSComponentDecorator(componentType: ComponentType): boolean {
const metadata = getComponentTypeMetadata(componentType);
return metadata[COMPONENT_TYPE_NAME] !== undefined;
return !!(componentType as any)[COMPONENT_TYPE_NAME];
}
/**
@@ -132,8 +82,7 @@ export function hasECSComponentDecorator(componentType: ComponentType): boolean
export function getComponentTypeName(componentType: ComponentType): string {
// 优先使用装饰器指定的名称
// Prefer decorator-specified name
const metadata = getComponentTypeMetadata(componentType);
const decoratorName = metadata[COMPONENT_TYPE_NAME];
const decoratorName = (componentType as any)[COMPONENT_TYPE_NAME];
if (decoratorName) {
return decoratorName;
}
@@ -162,8 +111,7 @@ export function getComponentInstanceTypeName(component: Component): string {
* @returns 依赖的组件名称列表
*/
export function getComponentDependencies(componentType: ComponentType): string[] | undefined {
const metadata = getComponentTypeMetadata(componentType);
return metadata[COMPONENT_DEPENDENCIES];
return (componentType as any)[COMPONENT_DEPENDENCIES];
}
/**
@@ -174,8 +122,7 @@ export function getComponentDependencies(componentType: ComponentType): string[]
* @returns 编辑器选项
*/
export function getComponentEditorOptions(componentType: ComponentType): ComponentEditorOptions | undefined {
const metadata = getComponentTypeMetadata(componentType);
return metadata[COMPONENT_EDITOR_OPTIONS];
return (componentType as any)[COMPONENT_EDITOR_OPTIONS];
}
/**
@@ -16,7 +16,7 @@ import type { ComponentType } from './ComponentTypeUtils';
* Component Registry Interface.
* 组件注册表接口。
*/
export type IComponentRegistry = {
export interface IComponentRegistry {
/**
* Register component type and allocate bitmask.
* 注册组件类型并分配位掩码。
+3 -3
View File
@@ -13,7 +13,7 @@ export type AsyncEventHandler<T> = (event: T) => Promise<void>;
/**
* 事件监听器配置
*/
export type EventListenerConfig = {
export interface EventListenerConfig {
/** 是否只执行一次 */
once?: boolean;
/** 优先级(数字越大优先级越高) */
@@ -41,7 +41,7 @@ interface InternalEventListener {
/**
* 事件统计信息
*/
export type EventStats = {
export interface EventStats {
/** 事件类型 */
eventType: string;
/** 监听器数量 */
@@ -59,7 +59,7 @@ export type EventStats = {
/**
* 事件批处理配置
*/
export type EventBatchConfig = {
export interface EventBatchConfig {
/** 批处理大小 */
batchSize: number;
/** 批处理延迟(毫秒) */
+1 -2
View File
@@ -1,3 +1,2 @@
export { EventBus, GlobalEventBus } from '../EventBus';
export { TypeSafeEventSystem } from '../EventSystem';
export type { EventListenerConfig, EventStats } from '../EventSystem';
export { TypeSafeEventSystem, EventListenerConfig, EventStats } from '../EventSystem';
+2 -4
View File
@@ -6,12 +6,10 @@ import { createLogger } from '../../Utils/Logger';
import { getComponentTypeName } from '../Decorators';
import { Archetype, ArchetypeSystem } from './ArchetypeSystem';
import { ReactiveQuery, ReactiveQueryConfig } from './ReactiveQuery';
import type { QueryCondition, QueryResult } from './QueryTypes';
import { QueryConditionType } from './QueryTypes';
import { QueryCondition, QueryConditionType, QueryResult } from './QueryTypes';
import { CompiledQuery } from './Query/CompiledQuery';
export { QueryConditionType };
export type { QueryCondition, QueryResult };
export { QueryCondition, QueryConditionType, QueryResult };
export { CompiledQuery };
/**
+2 -2
View File
@@ -17,7 +17,7 @@ export enum QueryConditionType {
/**
* 查询条件接口
*/
export type QueryCondition = {
export interface QueryCondition {
type: QueryConditionType;
componentTypes: ComponentType[];
mask: BitMask64Data;
@@ -26,7 +26,7 @@ export type QueryCondition = {
/**
* 实体查询结果接口
*/
export type QueryResult = {
export interface QueryResult {
entities: readonly Entity[];
count: number;
/** 查询执行时间(毫秒) */
+3 -4
View File
@@ -1,6 +1,5 @@
import { Entity } from '../Entity';
import type { QueryCondition } from './QueryTypes';
import { QueryConditionType } from './QueryTypes';
import { QueryCondition, QueryConditionType } from './QueryTypes';
import { BitMask64Utils } from '../Utils/BigIntCompatibility';
import { createLogger } from '../../Utils/Logger';
@@ -21,7 +20,7 @@ export enum ReactiveQueryChangeType {
/**
* 响应式查询变化事件
*/
export type ReactiveQueryChange = {
export interface ReactiveQueryChange {
/** 变化类型 */
type: ReactiveQueryChangeType;
/** 变化的实体 */
@@ -42,7 +41,7 @@ export type ReactiveQueryListener = (change: ReactiveQueryChange) => void;
/**
* 响应式查询配置
*/
export type ReactiveQueryConfig = {
export interface ReactiveQueryConfig {
/** 是否启用批量模式(减少通知频率) */
enableBatchMode?: boolean;
/** 批量模式的延迟时间(毫秒) */
@@ -54,7 +54,7 @@ const WeakRefImpl: IWeakRefConstructor = (
/**
* Entity引用记录
*/
export type EntityRefRecord = {
export interface EntityRefRecord {
component: IWeakRef<Component>;
propertyKey: string;
}
+1 -1
View File
@@ -8,7 +8,7 @@ import {
import { SoASerializer } from './SoASerializer';
// 重新导出类型,保持向后兼容
export type { SupportedTypedArray, TypedArrayTypeName } from './SoATypeRegistry';
export { SupportedTypedArray, TypedArrayTypeName } from './SoATypeRegistry';
export { SoATypeRegistry } from './SoATypeRegistry';
export { SoASerializer } from './SoASerializer';
@@ -40,7 +40,7 @@ export type TypedArrayTypeName =
/**
* 字段元数据
*/
export type FieldMetadata = {
export interface FieldMetadata {
name: string;
type: 'number' | 'boolean' | 'string' | 'object';
arrayType?: TypedArrayTypeName;
+1 -2
View File
@@ -1,5 +1,4 @@
export { ComponentPoolManager } from '../ComponentPool';
export type { ComponentPool } from '../ComponentPool';
export { ComponentPool, ComponentPoolManager } from '../ComponentPool';
export { ComponentStorage, ComponentRegistry, GlobalComponentRegistry } from '../ComponentStorage';
export type { IComponentRegistry } from '../ComponentStorage';
export { EnableSoA, Float64, Float32, Int32, SerializeMap, SoAStorage } from '../SoAStorage';
@@ -17,8 +17,12 @@
* ```
*/
// 从SoAStorage导入所有装饰器和类型
export {
// 启用装饰器
EnableSoA,
// 数值类型装饰器
Float64,
Float32,
Int32,
@@ -28,10 +32,13 @@ export {
Int8,
Uint8,
Uint8Clamped,
// 序列化装饰器
SerializeMap,
SerializeSet,
SerializeArray,
DeepCopy
} from './SoAStorage';
DeepCopy,
export type { SupportedTypedArray } from './SoAStorage';
// 类型定义
SupportedTypedArray
} from './SoAStorage';
@@ -48,7 +48,7 @@ interface GraphNode {
* 系统依赖信息
* System dependency info
*/
export type SystemDependencyInfo = {
export interface SystemDependencyInfo {
/** 系统名称 | System name */
name: string;
/** 在这些系统之前执行 | Execute before these systems */
@@ -53,7 +53,7 @@ export const DEFAULT_STAGE_ORDER: readonly SystemStage[] = [
* 系统调度元数据
* System scheduling metadata
*/
export type SystemSchedulingMetadata = {
export interface SystemSchedulingMetadata {
/** 执行阶段 | Execution stage */
stage: SystemStage;
/** 在这些系统之前执行 | Execute before these systems */
@@ -18,7 +18,7 @@ const ENTITY_REF_VALUES = Symbol('EntityRefValues');
/**
* EntityRef元数据
*/
export type EntityRefMetadata = {
export interface EntityRefMetadata {
properties: Set<string>;
}
@@ -1,10 +1,6 @@
/**
* 属性元数据存储
* Property metadata storage
*/
const metadataStorage = new WeakMap<Function, Record<string, unknown>>();
import 'reflect-metadata';
export type PropertyType = 'number' | 'integer' | 'string' | 'boolean' | 'color' | 'vector2' | 'vector3' | 'vector4' | 'enum' | 'asset' | 'array' | 'animationClips' | 'collisionLayer' | 'collisionMask' | 'entityRef';
export type PropertyType = 'number' | 'integer' | 'string' | 'boolean' | 'color' | 'vector2' | 'vector3' | 'vector4' | 'enum' | 'asset' | 'array' | 'animationClips' | 'collisionLayer' | 'collisionMask';
/**
* 属性资源类型
@@ -25,7 +21,7 @@ export type EnumOption = string | { label: string; value: any };
* Action button configuration for property fields
* 属性字段的操作按钮配置
*/
export type PropertyAction = {
export interface PropertyAction {
/** Action identifier | 操作标识符 */
id: string;
/** Button label | 按钮标签 */
@@ -40,7 +36,7 @@ export type PropertyAction = {
* 控制关系声明
* Control relationship declaration
*/
export type PropertyControl = {
export interface PropertyControl {
/** 被控制的组件名称 | Target component name */
component: string;
/** 被控制的属性名称 | Target property name */
@@ -56,16 +52,6 @@ interface PropertyOptionsBase {
label?: string;
/** 是否只读 | Read-only flag */
readOnly?: boolean;
/**
* 是否在 Inspector 中隐藏
* Whether to hide this property in Inspector
*
* Hidden properties are still serialized but not shown in the default PropertyInspector.
* Useful when a custom Inspector handles the property.
* 隐藏的属性仍然会被序列化,但不会在默认的 PropertyInspector 中显示。
* 适用于自定义 Inspector 处理该属性的情况。
*/
hidden?: boolean;
/** Action buttons | 操作按钮 */
actions?: PropertyAction[];
/** 此属性控制的其他组件属性 | Properties this field controls */
@@ -207,17 +193,6 @@ interface CollisionMaskPropertyOptions extends PropertyOptionsBase {
type: 'collisionMask';
}
/**
* 实体引用属性选项
* Entity reference property options
*
* Used for properties that store entity IDs and support drag-and-drop from SceneHierarchy.
* 用于存储实体 ID 的属性,支持从场景层级面板拖放。
*/
interface EntityRefPropertyOptions extends PropertyOptionsBase {
type: 'entityRef';
}
/**
* 属性选项联合类型
* Property options union type
@@ -233,8 +208,7 @@ export type PropertyOptions =
| ArrayPropertyOptions
| AnimationClipsPropertyOptions
| CollisionLayerPropertyOptions
| CollisionMaskPropertyOptions
| EntityRefPropertyOptions;
| CollisionMaskPropertyOptions;
// 使用 Symbol.for 创建全局 Symbol,确保跨包共享元数据
// Use Symbol.for to create a global Symbol to ensure metadata sharing across packages
@@ -257,27 +231,25 @@ export const PROPERTY_METADATA = Symbol.for('@esengine/property:metadata');
*/
export function Property(options: PropertyOptions): PropertyDecorator {
return (target: object, propertyKey: string | symbol) => {
const constructor = target.constructor as Function;
const existingMetadata = metadataStorage.get(constructor) || {};
const constructor = target.constructor;
const existingMetadata = Reflect.getMetadata(PROPERTY_METADATA, constructor) || {};
existingMetadata[propertyKey as string] = options;
metadataStorage.set(constructor, existingMetadata);
Reflect.defineMetadata(PROPERTY_METADATA, existingMetadata, constructor);
};
}
/**
* 获取组件类的所有属性元数据
* Get all property metadata for a component class
*/
export function getPropertyMetadata(target: Function): Record<string, PropertyOptions> | undefined {
return metadataStorage.get(target) as Record<string, PropertyOptions> | undefined;
return Reflect.getMetadata(PROPERTY_METADATA, target);
}
/**
* 检查组件类是否有属性元数据
* Check if a component class has property metadata
*/
export function hasPropertyMetadata(target: Function): boolean {
return metadataStorage.has(target);
return Reflect.hasMetadata(PROPERTY_METADATA, target);
}
@@ -15,9 +15,7 @@ import {
COMPONENT_TYPE_NAME,
COMPONENT_DEPENDENCIES,
COMPONENT_EDITOR_OPTIONS,
getWritableComponentTypeMetadata,
type ComponentEditorOptions,
type ComponentType
type ComponentEditorOptions
} from '../Core/ComponentStorage/ComponentTypeUtils';
/**
@@ -26,55 +24,11 @@ import {
*/
export const SYSTEM_TYPE_NAME = Symbol('SystemTypeName');
/**
* 系统类型元数据接口
* System type metadata interface
*/
export type SystemTypeMetadata = {
readonly [SYSTEM_TYPE_NAME]?: string;
readonly __systemMetadata__?: SystemMetadata;
}
/**
* 可写的系统类型元数据
* Writable system type metadata
*/
interface WritableSystemTypeMetadata {
[SYSTEM_TYPE_NAME]?: string;
__systemMetadata__?: SystemMetadata;
}
/**
* 获取系统类型元数据
* Get system type metadata
*/
function getSystemTypeMetadata(systemType: SystemConstructor): SystemTypeMetadata {
return systemType as unknown as SystemTypeMetadata;
}
/**
* 获取可写的系统类型元数据
* Get writable system type metadata
*/
function getWritableSystemTypeMetadata(systemType: SystemConstructor): WritableSystemTypeMetadata {
return systemType as unknown as WritableSystemTypeMetadata;
}
/**
* 系统构造函数类型
* System constructor type
*
* 注意:构造函数参数使用 any[] 是必要的,因为系统可以有各种不同签名的构造函数
* Note: Constructor args use any[] because systems can have various constructor signatures
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type SystemConstructor<T extends EntitySystem = EntitySystem> = new (...args: any[]) => T;
/**
* 组件装饰器配置选项
* Component decorator options
*/
export type ComponentOptions = {
export interface ComponentOptions {
/** 依赖的其他组件名称列表 | List of required component names */
requires?: string[];
@@ -113,29 +67,25 @@ export type ComponentOptions = {
* ```
*/
export function ECSComponent(typeName: string, options?: ComponentOptions) {
return function <T extends ComponentType<Component>>(target: T): T {
return function <T extends new (...args: any[]) => Component>(target: T): T {
if (!typeName || typeof typeName !== 'string') {
throw new Error('ECSComponent装饰器必须提供有效的类型名称');
}
// 获取可写的元数据对象
// Get writable metadata object
const metadata = getWritableComponentTypeMetadata(target);
// 在构造函数上存储类型名称
// Store type name on constructor
metadata[COMPONENT_TYPE_NAME] = typeName;
(target as any)[COMPONENT_TYPE_NAME] = typeName;
// 存储依赖关系
// Store dependencies
if (options?.requires) {
metadata[COMPONENT_DEPENDENCIES] = options.requires;
(target as any)[COMPONENT_DEPENDENCIES] = options.requires;
}
// 存储编辑器选项
// Store editor options
if (options?.editor) {
metadata[COMPONENT_EDITOR_OPTIONS] = options.editor;
(target as any)[COMPONENT_EDITOR_OPTIONS] = options.editor;
}
// 自动注册到全局 ComponentRegistry,使组件可以通过名称查找
@@ -150,7 +100,7 @@ export function ECSComponent(typeName: string, options?: ComponentOptions) {
* System 元数据配置
* System metadata configuration
*/
export type SystemMetadata = {
export interface SystemMetadata {
/**
* 更新顺序(数值越小越先执行,默认0)
* Update order (lower values execute first, default 0)
@@ -162,21 +112,6 @@ export type SystemMetadata = {
* Whether enabled by default (default true)
*/
enabled?: boolean;
/**
* 是否在编辑模式下运行(默认 true)
* Whether to run in edit mode (default true)
*
* 默认情况下,所有系统在编辑模式下都会运行。
* 当设置为 false 时,此系统在编辑模式(非 Play 状态)下不会执行。
* 适用于物理系统、AI 系统等只应在游戏运行时执行的系统。
*
* By default, all systems run in edit mode.
* When set to false, this system will NOT execute during edit mode
* (when not playing). Useful for physics, AI, and other systems
* that should only run during gameplay.
*/
runInEditMode?: boolean;
}
/**
@@ -204,23 +139,19 @@ export type SystemMetadata = {
* ```
*/
export function ECSSystem(typeName: string, metadata?: SystemMetadata) {
return function <T extends SystemConstructor>(target: T): T {
return function <T extends new (...args: any[]) => EntitySystem>(target: T): T {
if (!typeName || typeof typeName !== 'string') {
throw new Error('ECSSystem装饰器必须提供有效的类型名称');
}
// 获取可写的元数据对象
// Get writable metadata object
const meta = getWritableSystemTypeMetadata(target);
// 在构造函数上存储类型名称
// Store type name on constructor
meta[SYSTEM_TYPE_NAME] = typeName;
(target as any)[SYSTEM_TYPE_NAME] = typeName;
// 存储元数据
// Store metadata
if (metadata) {
meta.__systemMetadata__ = metadata;
(target as any).__systemMetadata__ = metadata;
}
return target;
@@ -231,20 +162,8 @@ export function ECSSystem(typeName: string, metadata?: SystemMetadata) {
* 获取 System 的元数据
* Get System metadata
*/
export function getSystemMetadata(systemType: SystemConstructor): SystemMetadata | undefined {
const meta = getSystemTypeMetadata(systemType);
return meta.__systemMetadata__;
}
/**
* 从系统实例获取元数据
* Get metadata from system instance
*
* @param system 系统实例 | System instance
* @returns 系统元数据 | System metadata
*/
export function getSystemInstanceMetadata(system: EntitySystem): SystemMetadata | undefined {
return getSystemMetadata(system.constructor as SystemConstructor);
export function getSystemMetadata(systemType: new (...args: any[]) => EntitySystem): SystemMetadata | undefined {
return (systemType as any).__systemMetadata__;
}
/**
@@ -255,10 +174,9 @@ export function getSystemInstanceMetadata(system: EntitySystem): SystemMetadata
* @returns 系统类型名称 | System type name
*/
export function getSystemTypeName<T extends EntitySystem>(
systemType: SystemConstructor<T>
systemType: new (...args: any[]) => T
): string {
const meta = getSystemTypeMetadata(systemType);
const decoratorName = meta[SYSTEM_TYPE_NAME];
const decoratorName = (systemType as any)[SYSTEM_TYPE_NAME];
if (decoratorName) {
return decoratorName;
}
@@ -273,5 +191,5 @@ export function getSystemTypeName<T extends EntitySystem>(
* @returns 系统类型名称 | System type name
*/
export function getSystemInstanceTypeName(system: EntitySystem): string {
return getSystemTypeName(system.constructor as SystemConstructor);
return getSystemTypeName(system.constructor as new (...args: any[]) => EntitySystem);
}
@@ -28,7 +28,6 @@ export {
getSystemTypeName,
getSystemInstanceTypeName,
getSystemMetadata,
getSystemInstanceMetadata,
SYSTEM_TYPE_NAME
} from './TypeDecorators';
+3 -3
View File
@@ -18,7 +18,7 @@ import type { IncrementalSnapshot, IncrementalSerializationOptions } from './Ser
*
* 定义场景应该实现的核心功能和属性,使用接口而非继承提供更灵活的实现方式。
*/
export type IScene = {
export interface IScene {
/**
* 场景名称
*/
@@ -362,7 +362,7 @@ export type IScene = {
/**
* 场景工厂接口
*/
export type ISceneFactory<T extends IScene> = {
export interface ISceneFactory<T extends IScene> {
/**
* 创建场景实例
*/
@@ -373,7 +373,7 @@ export type ISceneFactory<T extends IScene> = {
* 场景配置接口
* Scene configuration interface
*/
export type ISceneConfig = {
export interface ISceneConfig {
/**
* 场景名称
* Scene name
+3 -31
View File
@@ -13,7 +13,7 @@ import { QuerySystem } from './Core/QuerySystem';
import { TypeSafeEventSystem } from './Core/EventSystem';
import { ReferenceTracker } from './Core/ReferenceTracker';
import { IScene, ISceneConfig } from './IScene';
import { getComponentInstanceTypeName, getSystemInstanceTypeName, getSystemMetadata, getSystemInstanceMetadata } from './Decorators';
import { getComponentInstanceTypeName, getSystemInstanceTypeName, getSystemMetadata } from './Decorators';
import { TypedQueryBuilder } from './Core/Query/TypedQuery';
import {
SceneSerializer,
@@ -558,7 +558,7 @@ export class Scene implements IScene {
const updateHandle = ProfilerSDK.beginSample('Systems.update', ProfileCategory.ECS);
try {
for (const system of systems) {
if (this._shouldSystemRun(system)) {
if (system.enabled) {
const systemHandle = ProfilerSDK.beginSample(system.systemName, ProfileCategory.ECS);
try {
system.update();
@@ -577,7 +577,7 @@ export class Scene implements IScene {
const lateUpdateHandle = ProfilerSDK.beginSample('Systems.lateUpdate', ProfileCategory.ECS);
try {
for (const system of systems) {
if (this._shouldSystemRun(system)) {
if (system.enabled) {
const systemHandle = ProfilerSDK.beginSample(`${system.systemName}.late`, ProfileCategory.ECS);
try {
system.lateUpdate();
@@ -602,34 +602,6 @@ export class Scene implements IScene {
}
}
/**
*
* Check if a system should run
*
* @param system | System to check
* @returns | Whether it should run
*/
private _shouldSystemRun(system: EntitySystem): boolean {
// 系统必须启用
// System must be enabled
if (!system.enabled) {
return false;
}
// 非编辑模式下,所有启用的系统都运行
// In non-edit mode, all enabled systems run
if (!this.isEditorMode) {
return true;
}
// 编辑模式下,默认所有系统都运行
// 只有明确标记 runInEditMode: false 的系统不运行
// In edit mode, all systems run by default
// Only systems explicitly marked runInEditMode: false are skipped
const metadata = getSystemInstanceMetadata(system);
return metadata?.runInEditMode !== false;
}
/**
*
* Flush all systems' deferred commands
@@ -1,130 +1,363 @@
/**
*
*
* Component serializer for ECS components.
*
*/
import { Component } from '../Component';
import { ComponentType } from '../Core/ComponentStorage';
import { getComponentTypeName, isEntityRefProperty } from '../Decorators';
import { getSerializationMetadata } from './SerializationDecorators';
import { ValueSerializer, SerializableValue } from './ValueSerializer';
import {
getSerializationMetadata
} from './SerializationDecorators';
import type { Entity } from '../Entity';
import type { SerializationContext, SerializedEntityRef } from './SerializationContext';
export type { SerializableValue } from './ValueSerializer';
/**
*
*/
export type SerializableValue =
| string
| number
| boolean
| null
| undefined
| SerializableValue[]
| { [key: string]: SerializableValue }
| { __type: 'Date'; value: string }
| { __type: 'Map'; value: Array<[SerializableValue, SerializableValue]> }
| { __type: 'Set'; value: SerializableValue[] }
| { __entityRef: SerializedEntityRef };
export type SerializedComponent = {
/**
*
*/
export interface SerializedComponent {
/**
*
*/
type: string;
/**
*
*/
version: number;
/**
*
*/
data: Record<string, SerializableValue>;
}
/**
*
*/
export class ComponentSerializer {
static serialize(component: Component): SerializedComponent | null {
/**
*
*
* @param component
* @returns null
*/
public static serialize(component: Component): SerializedComponent | null {
const metadata = getSerializationMetadata(component);
if (!metadata) return null;
if (!metadata) {
// 组件没有使用@Serializable装饰器,不可序列化
return null;
}
const componentType = component.constructor as ComponentType;
const typeName = metadata.options.typeId || getComponentTypeName(componentType);
const data: Record<string, SerializableValue> = {};
// 序列化标记的字段
for (const [fieldName, options] of metadata.fields) {
if (metadata.ignoredFields.has(fieldName)) continue;
const fieldKey = typeof fieldName === 'symbol' ? fieldName.toString() : fieldName;
const value = (component as unknown as Record<string | symbol, unknown>)[fieldName];
// 跳过忽略的字段
if (metadata.ignoredFields.has(fieldName)) {
continue;
}
let serializedValue: SerializableValue;
// 检查是否为 EntityRef 属性
if (isEntityRefProperty(component, fieldKey)) {
serializedValue = this.serializeEntityRef(value as Entity | null);
} else if (options.serializer) {
// 使用自定义序列化器
serializedValue = options.serializer(value);
} else {
serializedValue = ValueSerializer.serialize(value);
// 使用默认序列化
serializedValue = this.serializeValue(value as SerializableValue);
}
data[options.alias || fieldKey] = serializedValue;
// 使用别名或原始字段名
const key = options.alias || fieldKey;
data[key] = serializedValue;
}
return { type: typeName, version: metadata.options.version, data };
return {
type: typeName,
version: metadata.options.version,
data
};
}
static deserialize(
/**
*
*
* @param serializedData
* @param componentRegistry ( -> )
* @param context EntityRef
* @returns null
*/
public static deserialize(
serializedData: SerializedComponent,
componentRegistry: Map<string, ComponentType>,
context?: SerializationContext
): Component | null {
const componentClass = componentRegistry.get(serializedData.type);
if (!componentClass) {
console.warn(`Component type not found: ${serializedData.type}`);
console.warn(`未找到组件类型: ${serializedData.type}`);
return null;
}
const metadata = getSerializationMetadata(componentClass);
if (!metadata) {
console.warn(`Component ${serializedData.type} is not serializable`);
console.warn(`组件 ${serializedData.type} 不可序列化`);
return null;
}
// 创建组件实例
const component = new componentClass();
// 反序列化字段
for (const [fieldName, options] of metadata.fields) {
const fieldKey = typeof fieldName === 'symbol' ? fieldName.toString() : fieldName;
const key = options.alias || fieldKey;
const serializedValue = serializedData.data[key];
if (serializedValue === undefined) continue;
if (serializedValue === undefined) {
continue; // 字段不存在于序列化数据中
}
// 检查是否为序列化的 EntityRef
if (this.isSerializedEntityRef(serializedValue)) {
// EntityRef 需要延迟解析
if (context) {
const ref = serializedValue.__entityRef;
context.registerPendingRef(component, fieldKey, ref.id, ref.guid);
}
// 暂时设为 null,后续由 context.resolveAllReferences() 填充
(component as unknown as Record<string | symbol, unknown>)[fieldName] = null;
continue;
}
// 使用自定义反序列化器或默认反序列化
const value = options.deserializer
? options.deserializer(serializedValue)
: ValueSerializer.deserialize(serializedValue);
: this.deserializeValue(serializedValue);
(component as unknown as Record<string | symbol, unknown>)[fieldName] = value;
(component as unknown as Record<string | symbol, SerializableValue>)[fieldName] = value;
}
return component;
}
static serializeComponents(components: Component[]): SerializedComponent[] {
return components
.map(c => this.serialize(c))
.filter((s): s is SerializedComponent => s !== null);
/**
*
*
* @param components
* @returns
*/
public static serializeComponents(components: Component[]): SerializedComponent[] {
const result: SerializedComponent[] = [];
for (const component of components) {
const serialized = this.serialize(component);
if (serialized) {
result.push(serialized);
}
}
return result;
}
static deserializeComponents(
/**
*
*
* @param serializedComponents
* @param componentRegistry
* @param context EntityRef
* @returns
*/
public static deserializeComponents(
serializedComponents: SerializedComponent[],
componentRegistry: Map<string, ComponentType>,
context?: SerializationContext
): Component[] {
return serializedComponents
.map(s => this.deserialize(s, componentRegistry, context))
.filter((c): c is Component => c !== null);
const result: Component[] = [];
for (const serialized of serializedComponents) {
const component = this.deserialize(serialized, componentRegistry, context);
if (component) {
result.push(component);
}
}
return result;
}
static validateVersion(serializedData: SerializedComponent, expectedVersion: number): boolean {
/**
*
*
*
*/
private static serializeValue(value: SerializableValue): SerializableValue {
if (value === null || value === undefined) {
return value;
}
// 基本类型
const type = typeof value;
if (type === 'string' || type === 'number' || type === 'boolean') {
return value;
}
// 日期
if (value instanceof Date) {
return {
__type: 'Date',
value: value.toISOString()
};
}
// 数组
if (Array.isArray(value)) {
return value.map((item) => this.serializeValue(item));
}
// Map (如果没有使用@SerializeMap装饰器)
if (value instanceof Map) {
return {
__type: 'Map',
value: Array.from(value.entries())
};
}
// Set
if (value instanceof Set) {
return {
__type: 'Set',
value: Array.from(value)
};
}
// 普通对象
if (type === 'object' && typeof value === 'object' && !Array.isArray(value)) {
const result: Record<string, SerializableValue> = {};
const obj = value as Record<string, SerializableValue>;
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
result[key] = this.serializeValue(obj[key]);
}
}
return result;
}
// 其他类型(函数等)不序列化
return undefined;
}
/**
*
*/
private static deserializeValue(value: SerializableValue): SerializableValue {
if (value === null || value === undefined) {
return value;
}
// 基本类型直接返回
const type = typeof value;
if (type === 'string' || type === 'number' || type === 'boolean') {
return value;
}
// 处理特殊类型标记
if (type === 'object' && typeof value === 'object' && '__type' in value) {
const typedValue = value as { __type: string; value: SerializableValue };
switch (typedValue.__type) {
case 'Date':
return { __type: 'Date', value: typeof typedValue.value === 'string' ? typedValue.value : String(typedValue.value) };
case 'Map':
return { __type: 'Map', value: typedValue.value as Array<[SerializableValue, SerializableValue]> };
case 'Set':
return { __type: 'Set', value: typedValue.value as SerializableValue[] };
}
}
// 数组
if (Array.isArray(value)) {
return value.map((item) => this.deserializeValue(item));
}
// 普通对象
if (type === 'object' && typeof value === 'object' && !Array.isArray(value)) {
const result: Record<string, SerializableValue> = {};
const obj = value as Record<string, SerializableValue>;
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
result[key] = this.deserializeValue(obj[key]);
}
}
return result;
}
return value;
}
/**
*
*
* @param serializedData
* @param expectedVersion
* @returns
*/
public static validateVersion(
serializedData: SerializedComponent,
expectedVersion: number
): boolean {
return serializedData.version === expectedVersion;
}
static getSerializationInfo(component: Component | ComponentType): {
/**
*
*
* @param component
* @returns
*/
public static getSerializationInfo(component: Component | ComponentType): {
type: string;
version: number;
fields: string[];
ignoredFields: string[];
isSerializable: boolean;
} {
} | null {
const metadata = getSerializationMetadata(component);
if (!metadata) {
return { type: 'unknown', version: 0, fields: [], ignoredFields: [], isSerializable: false };
return {
type: 'unknown',
version: 0,
fields: [],
ignoredFields: [],
isSerializable: false
};
}
const componentType = typeof component === 'function'
@@ -134,18 +367,50 @@ export class ComponentSerializer {
return {
type: metadata.options.typeId || getComponentTypeName(componentType),
version: metadata.options.version,
fields: Array.from(metadata.fields.keys()).map(k => typeof k === 'symbol' ? k.toString() : k),
ignoredFields: Array.from(metadata.ignoredFields).map(k => typeof k === 'symbol' ? k.toString() : k),
fields: Array.from(metadata.fields.keys()).map((k) =>
typeof k === 'symbol' ? k.toString() : k
),
ignoredFields: Array.from(metadata.ignoredFields).map((k) =>
typeof k === 'symbol' ? k.toString() : k
),
isSerializable: true
};
}
static serializeEntityRef(entity: Entity | null): SerializableValue {
if (!entity) return null;
return { __entityRef: { id: entity.id, guid: entity.persistentId } };
/**
* Entity
*
* Serialize an Entity reference to a portable format.
*
* @param entity Entity null
* @returns
*/
public static serializeEntityRef(entity: Entity | null): SerializableValue {
if (!entity) {
return null;
}
return {
__entityRef: {
id: entity.id,
guid: entity.persistentId
}
};
}
static isSerializedEntityRef(value: unknown): value is { __entityRef: SerializedEntityRef } {
return typeof value === 'object' && value !== null && '__entityRef' in value;
/**
* EntityRef
*
* Check if a value is a serialized EntityRef.
*
* @param value
* @returns EntityRef true
*/
public static isSerializedEntityRef(value: unknown): value is { __entityRef: SerializedEntityRef } {
return (
typeof value === 'object' &&
value !== null &&
'__entityRef' in value
);
}
}
@@ -15,7 +15,7 @@ import { SerializationContext } from './SerializationContext';
/**
*
*/
export type SerializedEntity = {
export interface SerializedEntity {
/**
* IDID
*
@@ -37,7 +37,7 @@ export enum ChangeOperation {
/**
*
*/
export type EntityChange = {
export interface EntityChange {
/** 操作类型 */
operation: ChangeOperation;
/** 实体ID */
@@ -51,7 +51,7 @@ export type EntityChange = {
/**
*
*/
export type ComponentChange = {
export interface ComponentChange {
/** 操作类型 */
operation: ChangeOperation;
/** 实体ID */
@@ -65,7 +65,7 @@ export type ComponentChange = {
/**
*
*/
export type SceneDataChange = {
export interface SceneDataChange {
/** 操作类型 */
operation: ChangeOperation;
/** 变更的键 */
@@ -79,7 +79,7 @@ export type SceneDataChange = {
/**
*
*/
export type IncrementalSnapshot = {
export interface IncrementalSnapshot {
/** 快照版本号 */
version: number;
/** 时间戳 */
@@ -127,7 +127,7 @@ export type IncrementalSerializationFormat = 'json' | 'binary';
/**
*
*/
export type IncrementalSerializationOptions = {
export interface IncrementalSerializationOptions {
/**
*
* truefalse可提升性能但可能漏掉组件内部字段变更
@@ -35,7 +35,7 @@ export interface SerializedPrefabEntity extends SerializedEntity {
*
* Prefab metadata
*/
export type PrefabMetadata = {
export interface PrefabMetadata {
/** 预制体名称 | Prefab name */
name: string;
/** 资产 GUID | Asset GUID */
@@ -58,7 +58,7 @@ export type PrefabMetadata = {
*
* Component type registry entry
*/
export type PrefabComponentTypeEntry = {
export interface PrefabComponentTypeEntry {
/** 组件类型名称 | Component type name */
typeName: string;
/** 组件版本号 | Component version number */
@@ -69,7 +69,7 @@ export type PrefabComponentTypeEntry = {
*
* Prefab data format
*/
export type PrefabData = {
export interface PrefabData {
/** 预制体格式版本号 | Prefab format version number */
version: number;
/** 预制体元数据 | Prefab metadata */
@@ -84,7 +84,7 @@ export type PrefabData = {
*
* Prefab creation options
*/
export type PrefabCreateOptions = {
export interface PrefabCreateOptions {
/** 预制体名称 | Prefab name */
name: string;
/** 预制体描述 | Prefab description */
@@ -99,7 +99,7 @@ export type PrefabCreateOptions = {
*
* Prefab instantiation options
*/
export type PrefabInstantiateOptions = {
export interface PrefabInstantiateOptions {
/** 父实体 ID | Parent entity ID */
parentId?: number;
/** 位置覆盖 | Position override */
@@ -14,7 +14,6 @@ import { BinarySerializer } from '../../Utils/BinarySerializer';
import { HierarchySystem } from '../Systems/HierarchySystem';
import { HierarchyComponent } from '../Components/HierarchyComponent';
import { SerializationContext } from './SerializationContext';
import { ValueSerializer, SerializableValue } from './ValueSerializer';
/**
*
@@ -38,7 +37,7 @@ export type MigrationFunction = (
/**
*
*/
export type SceneSerializationOptions = {
export interface SceneSerializationOptions {
/**
*
*
@@ -69,7 +68,7 @@ export type SceneSerializationOptions = {
/**
*
*/
export type SceneDeserializationOptions = {
export interface SceneDeserializationOptions {
/**
*
* - 'merge':
@@ -97,7 +96,7 @@ export type SceneDeserializationOptions = {
/**
*
*/
export type SerializedScene = {
export interface SerializedScene {
/**
*
*/
@@ -388,21 +387,131 @@ export class SceneSerializer {
}
}
private static serializeSceneData(sceneData: Map<string, unknown>): Record<string, unknown> {
const result: Record<string, unknown> = {};
/**
*
*
* Map<string, any>
*/
private static serializeSceneData(sceneData: Map<string, any>): Record<string, any> {
const result: Record<string, any> = {};
for (const [key, value] of sceneData) {
result[key] = ValueSerializer.serialize(value);
result[key] = this.serializeValue(value);
}
return result;
}
private static deserializeSceneData(data: Record<string, unknown>, targetMap: Map<string, unknown>): void {
/**
*
*
* Map<string, any>
*/
private static deserializeSceneData(
data: Record<string, any>,
targetMap: Map<string, any>
): void {
targetMap.clear();
for (const [key, value] of Object.entries(data)) {
targetMap.set(key, ValueSerializer.deserialize(value as SerializableValue));
targetMap.set(key, this.deserializeValue(value));
}
}
/**
*
*/
private static serializeValue(value: any): any {
if (value === null || value === undefined) {
return value;
}
// 基本类型
const type = typeof value;
if (type === 'string' || type === 'number' || type === 'boolean') {
return value;
}
// Date
if (value instanceof Date) {
return { __type: 'Date', value: value.toISOString() };
}
// Map
if (value instanceof Map) {
return { __type: 'Map', value: Array.from(value.entries()) };
}
// Set
if (value instanceof Set) {
return { __type: 'Set', value: Array.from(value) };
}
// 数组
if (Array.isArray(value)) {
return value.map((item) => this.serializeValue(item));
}
// 普通对象
if (type === 'object') {
const result: Record<string, any> = {};
for (const key in value) {
if (value.hasOwnProperty(key)) {
result[key] = this.serializeValue(value[key]);
}
}
return result;
}
// 其他类型不序列化
return undefined;
}
/**
*
*/
private static deserializeValue(value: any): any {
if (value === null || value === undefined) {
return value;
}
// 基本类型
const type = typeof value;
if (type === 'string' || type === 'number' || type === 'boolean') {
return value;
}
// 处理特殊类型标记
if (type === 'object' && value.__type) {
switch (value.__type) {
case 'Date':
return new Date(value.value);
case 'Map':
return new Map(value.value);
case 'Set':
return new Set(value.value);
}
}
// 数组
if (Array.isArray(value)) {
return value.map((item) => this.deserializeValue(item));
}
// 普通对象
if (type === 'object') {
const result: Record<string, any> = {};
for (const key in value) {
if (value.hasOwnProperty(key)) {
result[key] = this.deserializeValue(value[key]);
}
}
return result;
}
return value;
}
/**
*
*/
@@ -6,7 +6,7 @@ import type { Component } from '../Component';
*
* Serialized entity reference format.
*/
export type SerializedEntityRef = {
export interface SerializedEntityRef {
/**
* ID
*
@@ -16,7 +16,7 @@ export const SERIALIZE_OPTIONS = Symbol('SerializeOptions');
/**
*
*/
export type SerializableOptions = {
export interface SerializableOptions {
/**
*
*/
@@ -31,7 +31,7 @@ export type SerializableOptions = {
/**
*
*/
export type FieldSerializeOptions = {
export interface FieldSerializeOptions {
/**
*
*/
@@ -51,7 +51,7 @@ export type FieldSerializeOptions = {
/**
*
*/
export type SerializationMetadata = {
export interface SerializationMetadata {
options: SerializableOptions;
fields: Map<string | symbol, FieldSerializeOptions>;
ignoredFields: Set<string | symbol>;
@@ -1,113 +0,0 @@
/**
*
*
* Value serializer with circular reference detection and extensible type handlers.
*/
export type PrimitiveValue = string | number | boolean | null | undefined;
export type SerializableValue =
| PrimitiveValue
| SerializableValue[]
| { readonly [key: string]: SerializableValue }
| { readonly __type: string; readonly value: unknown };
type Serializer<T> = (value: T, serialize: (v: unknown) => SerializableValue) => SerializableValue;
type Deserializer<T> = (data: { __type: string; value: unknown }) => T;
interface TypeDef<T = unknown> {
check: (value: unknown) => value is T;
serialize: Serializer<T>;
deserialize: Deserializer<T>;
}
const types = new Map<string, TypeDef>();
function registerType<T>(name: string, def: TypeDef<T>): void {
types.set(name, def as TypeDef);
}
// 内置类型
registerType<Date>('Date', {
check: (v): v is Date => v instanceof Date,
serialize: (v) => ({ __type: 'Date', value: v.toISOString() }),
deserialize: (d) => new Date(d.value as string)
});
registerType<Map<unknown, unknown>>('Map', {
check: (v): v is Map<unknown, unknown> => v instanceof Map,
serialize: (v, ser) => ({ __type: 'Map', value: [...v].map(([k, val]) => [ser(k), ser(val)]) }),
deserialize: (d) => new Map(d.value as Array<[unknown, unknown]>)
});
registerType<Set<unknown>>('Set', {
check: (v): v is Set<unknown> => v instanceof Set,
serialize: (v, ser) => ({ __type: 'Set', value: [...v].map(ser) }),
deserialize: (d) => new Set(d.value as unknown[])
});
function serialize(value: unknown, seen = new WeakSet<object>()): SerializableValue {
if (value == null) return value as null | undefined;
const t = typeof value;
if (t === 'string' || t === 'number' || t === 'boolean') return value as PrimitiveValue;
if (t === 'function') return undefined;
const obj = value as object;
if (seen.has(obj)) return undefined;
seen.add(obj);
for (const [, def] of types) {
if (def.check(value)) {
return def.serialize(value, (v) => serialize(v, seen));
}
}
if (Array.isArray(value)) {
return value.map((v) => serialize(v, seen));
}
const result: Record<string, SerializableValue> = {};
for (const k of Object.keys(value as object)) {
result[k] = serialize((value as Record<string, unknown>)[k], seen);
}
return result;
}
function deserialize(value: SerializableValue): unknown {
if (value == null) return value;
const t = typeof value;
if (t === 'string' || t === 'number' || t === 'boolean') return value;
if (isTypedValue(value)) {
const def = types.get(value.__type);
return def ? def.deserialize(value) : value;
}
if (Array.isArray(value)) {
return value.map(deserialize);
}
const result: Record<string, unknown> = {};
for (const k of Object.keys(value)) {
result[k] = deserialize((value as Record<string, SerializableValue>)[k]);
}
return result;
}
function isTypedValue(v: unknown): v is { __type: string; value: unknown } {
if (v === null || typeof v !== 'object') {
return false;
}
return '__type' in v;
}
export const ValueSerializer = {
serialize,
deserialize,
register: registerType
} as const;
export type { TypeDef as TypeHandler };
export type TypedValue = { readonly __type: string; readonly value: unknown };
@@ -24,10 +24,6 @@ export type {
SerializationMetadata
} from './SerializationDecorators';
// 值序列化器
export { ValueSerializer } from './ValueSerializer';
export type { SerializableValue, TypeHandler, TypedValue } from './ValueSerializer';
// 组件序列化器
export { ComponentSerializer } from './ComponentSerializer';
export type { SerializedComponent } from './ComponentSerializer';
+2 -27
View File
@@ -135,19 +135,6 @@ export abstract class EntitySystem implements ISystemBase, IService {
*/
private _lastProcessEpoch: number = 0;
/**
*
* Whether this frame should be processed
*
* update() onCheckProcessing() lateUpdate()
* onCheckProcessing() IntervalSystem
*
* Determined by onCheckProcessing() in update(), reused by lateUpdate().
* Prevents onCheckProcessing() being called multiple times causing side effects
* to execute repeatedly (e.g., IntervalSystem's time accumulation).
*/
private _shouldProcessThisFrame: boolean = false;
/**
*
*/
@@ -787,11 +774,7 @@ export abstract class EntitySystem implements ISystemBase, IService {
*
*/
public update(): void {
// 检查是否应该处理,并缓存结果供 lateUpdate 使用
// Check if should process and cache result for lateUpdate
this._shouldProcessThisFrame = this._enabled && this.onCheckProcessing();
if (!this._shouldProcessThisFrame) {
if (!this._enabled || !this.onCheckProcessing()) {
return;
}
@@ -816,17 +799,9 @@ export abstract class EntitySystem implements ISystemBase, IService {
/**
*
*
* lateUpdate update onCheckProcessing()
* IntervalSystem
*
* lateUpdate reuses the onCheckProcessing() result from update,
* preventing side effects in subclasses like IntervalSystem from executing repeatedly.
*/
public lateUpdate(): void {
// 复用 update() 中的检查结果,不再调用 onCheckProcessing()
// Reuse check result from update(), don't call onCheckProcessing() again
if (!this._shouldProcessThisFrame) {
if (!this._enabled || !this.onCheckProcessing()) {
return;
}
@@ -19,7 +19,7 @@ export type WorkerProcessFunction<T extends Record<string, any> = any> = (
/**
* Worker配置接口
*/
export type WorkerSystemConfig = {
export interface WorkerSystemConfig {
/** 是否启用Worker并行处理 */
enableWorker?: boolean;
/** Worker数量,默认为CPU核心数,自动限制在系统最大值内 */
@@ -17,7 +17,7 @@ export type BitMask64Segment = [number,number]
* 128+base[lo , hi] segments 64
* segments[0] bit 64-127segments[1] bit 128-191
*/
export type BitMask64Data = {
export interface BitMask64Data {
base: BitMask64Segment;
/** 扩展段数组,每个元素是一个 64 位段,用于超过 64 位的场景 */
segments?: BitMask64Segment[];
+2 -15
View File
@@ -80,30 +80,17 @@ export class EntityList {
/**
*
* Remove all entities
*
* buffer
* Includes entities in buffer and entities in pending add queue.
*/
public removeAllEntities(): void {
// 收集所有实体ID用于回收
const idsToRecycle: number[] = [];
// 销毁 buffer 中的实体
// Destroy entities in buffer
for (let i = this.buffer.length - 1; i >= 0; i--) {
idsToRecycle.push(this.buffer[i]!.id);
this.buffer[i]!.destroy();
}
// 销毁待添加队列中的实体(这些实体已创建但尚未加入 buffer)
// Destroy entities in pending add queue (created but not yet in buffer)
for (const entity of this._entitiesToAdd) {
idsToRecycle.push(entity.id);
entity.destroy();
}
// 批量回收 ID
// Recycle IDs in batch
// 批量回收ID
if (this._scene && this._scene.identifierPool) {
for (const id of idsToRecycle) {
this._scene.identifierPool.checkIn(id);
+1 -1
View File
@@ -4,7 +4,7 @@ import { getComponentTypeName } from '../Decorators';
/**
*
*/
export type QueryCondition = {
export interface QueryCondition {
all: ComponentType[];
any: ComponentType[];
none: ComponentType[];
+1 -2
View File
@@ -4,7 +4,6 @@ export { EntityProcessorList } from './EntityProcessorList';
export { IdentifierPool } from './IdentifierPool';
export { Matcher } from './Matcher';
export { Bits } from './Bits';
export { BitMask64Utils } from './BigIntCompatibility';
export type { BitMask64Data } from './BigIntCompatibility';
export { BitMask64Utils, BitMask64Data } from './BigIntCompatibility';
export { SparseSet } from './SparseSet';
export { ComponentSparseSet } from './ComponentSparseSet';
+2 -2
View File
@@ -10,7 +10,7 @@ const logger = createLogger('World');
*
* World级别运行的系统Scene
*/
export type IGlobalSystem = {
export interface IGlobalSystem {
/**
*
*/
@@ -40,7 +40,7 @@ export type IGlobalSystem = {
/**
* World配置接口
*/
export type IWorldConfig = {
export interface IWorldConfig {
/**
* World名称
*/
+1 -1
View File
@@ -7,7 +7,7 @@ const logger = createLogger('WorldManager');
/**
* WorldManager配置接口
*/
export type IWorldManagerConfig = {
export interface IWorldManagerConfig {
/**
* World数量
*/
+3 -5
View File
@@ -7,12 +7,10 @@ export * from './Utils';
export * from './Decorators';
export * from './Components';
export { Scene } from './Scene';
export type { IScene, ISceneFactory, ISceneConfig } from './IScene';
export { IScene, ISceneFactory, ISceneConfig } from './IScene';
export { SceneManager } from './SceneManager';
export { World } from './World';
export type { IWorldConfig } from './World';
export { WorldManager } from './WorldManager';
export type { IWorldManagerConfig } from './WorldManager';
export { World, IWorldConfig } from './World';
export { WorldManager, IWorldManagerConfig } from './WorldManager';
export * from './Core/Events';
export * from './Core/Query';
export * from './Core/Storage';
@@ -2,7 +2,7 @@
*
*
*/
export type IPlatformAdapter = {
export interface IPlatformAdapter {
/**
*
*/
@@ -60,7 +60,7 @@ export type IPlatformAdapter = {
/**
* Worker创建选项
*/
export type WorkerCreationOptions = {
export interface WorkerCreationOptions {
/**
* Worker类型
*/
@@ -80,7 +80,7 @@ export type WorkerCreationOptions = {
/**
* Worker接口
*/
export type PlatformWorker = {
export interface PlatformWorker {
/**
* Worker
*/
@@ -110,7 +110,7 @@ export type PlatformWorker = {
/**
*
*/
export type PlatformConfig = {
export interface PlatformConfig {
/**
* Worker数量限制
*/
@@ -175,7 +175,7 @@ export type PlatformConfig = {
/**
*
*/
export type PlatformDetectionResult = {
export interface PlatformDetectionResult {
/**
*
*/
+5 -5
View File
@@ -14,7 +14,7 @@ const logger = createLogger('DebugPlugin');
/**
* ECS
*/
export type ECSDebugStats = {
export interface ECSDebugStats {
scenes: SceneDebugInfo[];
totalEntities: number;
totalSystems: number;
@@ -24,7 +24,7 @@ export type ECSDebugStats = {
/**
*
*/
export type SceneDebugInfo = {
export interface SceneDebugInfo {
name: string;
entityCount: number;
systems: SystemDebugInfo[];
@@ -34,7 +34,7 @@ export type SceneDebugInfo = {
/**
*
*/
export type SystemDebugInfo = {
export interface SystemDebugInfo {
name: string;
enabled: boolean;
updateOrder: number;
@@ -49,7 +49,7 @@ export type SystemDebugInfo = {
/**
*
*/
export type EntityDebugInfo = {
export interface EntityDebugInfo {
id: number;
name: string;
enabled: boolean;
@@ -61,7 +61,7 @@ export type EntityDebugInfo = {
/**
*
*/
export type ComponentDebugInfo = {
export interface ComponentDebugInfo {
type: string;
data: any;
}
+1 -1
View File
@@ -3,7 +3,7 @@
*
* Core自动调用update方法
*/
export type IUpdatable = {
export interface IUpdatable {
/**
*
*
+6 -6
View File
@@ -48,7 +48,7 @@ export type ComponentTypeMap<T extends readonly ComponentConstructor[]> = {
* with组件的类型
*
*/
export type EntityWithComponents<T extends readonly ComponentConstructor[]> = {
export interface EntityWithComponents<T extends readonly ComponentConstructor[]> {
readonly id: number;
readonly name: string;
@@ -163,7 +163,7 @@ export type TypedEventHandler<T> = (data: T) => void | Promise<void>;
/**
*
*/
export type SystemLifecycleHooks<T extends readonly ComponentConstructor[]> = {
export interface SystemLifecycleHooks<T extends readonly ComponentConstructor[]> {
/**
*
*/
@@ -188,7 +188,7 @@ export type SystemLifecycleHooks<T extends readonly ComponentConstructor[]> = {
/**
* Fluent API构建器类型
*/
export type TypeSafeBuilder<T> = {
export interface TypeSafeBuilder<T> {
/**
*
*/
@@ -198,7 +198,7 @@ export type TypeSafeBuilder<T> = {
/**
*
*/
export type ComponentPool<T extends IComponent> = {
export interface ComponentPool<T extends IComponent> {
/**
*
*/
@@ -223,11 +223,11 @@ export type ComponentPool<T extends IComponent> = {
/**
*
*/
export type TypedQueryCondition<
export interface TypedQueryCondition<
All extends readonly ComponentConstructor[] = [],
Any extends readonly ComponentConstructor[] = [],
None extends readonly ComponentConstructor[] = []
> = {
> {
all: All;
any: Any;
none: None;
+15 -15
View File
@@ -14,7 +14,7 @@ export * from './IUpdatable';
*
* ECS
*/
export type IComponent = {
export interface IComponent {
/** 组件唯一标识符 */
readonly id: number;
/** 组件所属的实体ID */
@@ -49,7 +49,7 @@ export type IComponent = {
*
* EntitySystem类提供类型定义
*/
export type ISystemBase = {
export interface ISystemBase {
/** 系统名称 */
readonly systemName: string;
/** 更新顺序/优先级 */
@@ -69,7 +69,7 @@ export type ISystemBase = {
* 线
*
*/
export type IEventBus = {
export interface IEventBus {
/**
*
* @param eventType
@@ -145,7 +145,7 @@ export type IEventBus = {
/**
*
*/
export type IEventListenerConfig = {
export interface IEventListenerConfig {
/** 是否只执行一次 */
once?: boolean;
/** 优先级(数字越大优先级越高) */
@@ -159,7 +159,7 @@ export type IEventListenerConfig = {
/**
*
*/
export type IEventStats = {
export interface IEventStats {
/** 事件类型 */
eventType: string;
/** 监听器数量 */
@@ -177,7 +177,7 @@ export type IEventStats = {
/**
*
*/
export type IEventData = {
export interface IEventData {
/** 事件时间戳 */
timestamp: number;
/** 事件来源 */
@@ -245,7 +245,7 @@ export interface IPerformanceEventData extends IEventData {
/**
* ECS调试配置接口
*/
export type IECSDebugConfig = {
export interface IECSDebugConfig {
/** 是否启用调试 */
enabled: boolean;
/** WebSocket服务器URL */
@@ -269,7 +269,7 @@ export type IECSDebugConfig = {
/**
* Core配置接口
*/
export type ICoreConfig = {
export interface ICoreConfig {
/** 是否启用调试模式 */
debug?: boolean;
/** 调试配置 */
@@ -281,7 +281,7 @@ export type ICoreConfig = {
/**
* ECS调试数据接口
*/
export type IECSDebugData = {
export interface IECSDebugData {
/** 时间戳 */
timestamp: number;
/** 框架版本 */
@@ -307,7 +307,7 @@ export type IECSDebugData = {
/**
*
*/
export type IEntityHierarchyNode = {
export interface IEntityHierarchyNode {
id: number;
name: string;
active: boolean;
@@ -325,7 +325,7 @@ export type IEntityHierarchyNode = {
/**
*
*/
export type IEntityDebugData = {
export interface IEntityDebugData {
/** 总实体数 */
totalEntities: number;
/** 激活实体数 */
@@ -399,7 +399,7 @@ export type IEntityDebugData = {
/**
*
*/
export type ISystemDebugData = {
export interface ISystemDebugData {
/** 总系统数 */
totalSystems: number;
/** 系统信息列表 */
@@ -422,7 +422,7 @@ export type ISystemDebugData = {
/**
*
*/
export type IPerformanceDebugData = {
export interface IPerformanceDebugData {
/** ECS框架执行时间(毫秒) */
frameTime: number;
/** 引擎总帧时间(毫秒) */
@@ -472,7 +472,7 @@ export type IPerformanceDebugData = {
/**
*
*/
export type IComponentDebugData = {
export interface IComponentDebugData {
/** 组件类型数 */
componentTypes: number;
/** 组件实例总数 */
@@ -492,7 +492,7 @@ export type IComponentDebugData = {
/**
*
*/
export type ISceneDebugData = {
export interface ISceneDebugData {
/** 当前场景名称 */
currentSceneName: string;
/** 场景是否已初始化 */
@@ -17,7 +17,7 @@ import { Time } from '../Time';
/**
* PerformanceMonitor ()
*/
export type ILegacyPerformanceMonitor = {
export interface ILegacyPerformanceMonitor {
getAllSystemStats?: () => Map<string, {
averageTime: number;
minTime?: number;
@@ -33,7 +33,7 @@ export type ILegacyPerformanceMonitor = {
/**
*
*/
export type IHotspotItem = {
export interface IHotspotItem {
name: string;
category: string;
inclusiveTime: number;
@@ -51,7 +51,7 @@ export type IHotspotItem = {
/**
*
*/
export type IAdvancedProfilerData = {
export interface IAdvancedProfilerData {
/** 当前帧信息 */
currentFrame: {
frameNumber: number;
+3 -3
View File
@@ -3,7 +3,7 @@ import type { LogLevel } from './Constants';
/**
*
*/
export type ILogger = {
export interface ILogger {
debug(...args: unknown[]): void;
info(...args: unknown[]): void;
warn(...args: unknown[]): void;
@@ -14,7 +14,7 @@ export type ILogger = {
/**
*
*/
export type LoggerColorConfig = {
export interface LoggerColorConfig {
debug?: string;
info?: string;
warn?: string;
@@ -26,7 +26,7 @@ export type LoggerColorConfig = {
/**
*
*/
export type LoggerConfig = {
export interface LoggerConfig {
/** 日志级别 */
level: LogLevel;
/** 是否启用时间戳 */
@@ -1,7 +1,7 @@
/**
*
*/
export type PerformanceData = {
export interface PerformanceData {
/** 系统名称 */
name: string;
/** 执行时间(毫秒) */
@@ -21,7 +21,7 @@ export type PerformanceData = {
/**
*
*/
export type PerformanceStats = {
export interface PerformanceStats {
/** 总执行时间 */
totalTime: number;
/** 平均执行时间 */
@@ -57,7 +57,7 @@ export enum PerformanceWarningType {
/**
*
*/
export type PerformanceWarning = {
export interface PerformanceWarning {
type: PerformanceWarningType;
systemName: string;
message: string;
@@ -71,7 +71,7 @@ export type PerformanceWarning = {
/**
*
*/
export type PerformanceThresholds = {
export interface PerformanceThresholds {
/** 执行时间阈值(毫秒) */
executionTime: {
warning: number;
+2 -2
View File
@@ -1,7 +1,7 @@
/**
*
*/
export type IPoolable = {
export interface IPoolable {
/**
*
*/
@@ -11,7 +11,7 @@ export type IPoolable = {
/**
*
*/
export type PoolStats = {
export interface PoolStats {
/** 池中对象数量 */
size: number;
/** 池的最大大小 */
@@ -15,7 +15,7 @@ import { ProfileCategory } from './ProfilerTypes';
/**
*
*/
export type AutoProfilerConfig = {
export interface AutoProfilerConfig {
/** 是否启用自动包装 */
enabled: boolean;
/** 采样间隔(毫秒),用于采样分析器 */
@@ -35,7 +35,7 @@ export enum ProfileCategory {
/**
*
*/
export type SampleHandle = {
export interface SampleHandle {
id: string;
name: string;
category: ProfileCategory;
@@ -47,7 +47,7 @@ export type SampleHandle = {
/**
*
*/
export type ProfileSample = {
export interface ProfileSample {
id: string;
name: string;
category: ProfileCategory;
@@ -66,7 +66,7 @@ export type ProfileSample = {
/**
*
*/
export type ProfileSampleStats = {
export interface ProfileSampleStats {
name: string;
category: ProfileCategory;
/** 包含时间(包含子调用) */
@@ -94,7 +94,7 @@ export type ProfileSampleStats = {
/**
*
*/
export type MemorySnapshot = {
export interface MemorySnapshot {
timestamp: number;
/** 已使用堆内存 (bytes) */
usedHeapSize: number;
@@ -111,7 +111,7 @@ export type MemorySnapshot = {
/**
*
*/
export type ProfileCounter = {
export interface ProfileCounter {
name: string;
category: ProfileCategory;
value: number;
@@ -122,7 +122,7 @@ export type ProfileCounter = {
/**
*
*/
export type ProfileFrame = {
export interface ProfileFrame {
frameNumber: number;
startTime: number;
endTime: number;
@@ -142,7 +142,7 @@ export type ProfileFrame = {
/**
*
*/
export type ProfilerConfig = {
export interface ProfilerConfig {
/** 是否启用 */
enabled: boolean;
/** 最大历史帧数 */
@@ -164,7 +164,7 @@ export type ProfilerConfig = {
/**
*
*/
export type LongTaskInfo = {
export interface LongTaskInfo {
startTime: number;
duration: number;
attribution: string[];
@@ -173,7 +173,7 @@ export type LongTaskInfo = {
/**
*
*/
export type CallGraphNode = {
export interface CallGraphNode {
name: string;
category: ProfileCategory;
/** 被调用次数 */
@@ -189,7 +189,7 @@ export type CallGraphNode = {
/**
*
*/
export type ProfileReport = {
export interface ProfileReport {
startTime: number;
endTime: number;
totalFrames: number;
+1 -1
View File
@@ -1,4 +1,4 @@
export type ITimer<TContext = unknown> = {
export interface ITimer<TContext = unknown> {
context: TContext;
/**
+2 -3
View File
@@ -48,7 +48,7 @@ export type { InjectableMetadata, UpdatableMetadata } from './Core/DI';
export { Emitter, FuncPack } from './Utils/Emitter';
export { GlobalManager } from './Utils/GlobalManager';
export { TimerManager } from './Utils/Timers/TimerManager';
export type { ITimer } from './Utils/Timers/ITimer';
export { ITimer } from './Utils/Timers/ITimer';
export { Timer } from './Utils/Timers/Timer';
// 日志系统
@@ -77,8 +77,7 @@ export * from './Utils';
export * from './Types';
// 显式导出ComponentPool类(解决与Types中ComponentPool接口的命名冲突)
export { ComponentPoolManager } from './ECS/Core/Storage';
export type { ComponentPool } from './ECS/Core/Storage';
export { ComponentPool, ComponentPoolManager } from './ECS/Core/Storage';
// 平台适配
export * from './Platform';
@@ -60,32 +60,6 @@ class ConcreteIntervalSystem extends IntervalSystem {
}
}
// 用于独立测试的间隔系统(避免 Scene 的类型去重)
class IndependentIntervalSystem extends IntervalSystem {
public processCallCount = 0;
constructor(interval: number) {
super(interval, Matcher.all(TestComponent));
}
protected override process(entities: Entity[]): void {
this.processCallCount++;
}
}
// 用于长时间运行测试的间隔系统
class LongRunIntervalSystem extends IntervalSystem {
public processCallCount = 0;
constructor(interval: number) {
super(interval, Matcher.all(TestComponent));
}
protected override process(entities: Entity[]): void {
this.processCallCount++;
}
}
// 具体的处理系统实现
class ConcreteProcessingSystem extends ProcessingSystem {
public processSystemCallCount = 0;
@@ -233,108 +207,16 @@ describe('System Types - 系统类型测试', () => {
Time.update(testInterval);
intervalSystem.update();
expect(intervalSystem.processCallCount).toBe(1);
// 再次触发需要等待完整间隔
Time.update(testInterval / 2);
intervalSystem.update();
expect(intervalSystem.processCallCount).toBe(1);
Time.update(testInterval / 2);
intervalSystem.update();
expect(intervalSystem.processCallCount).toBe(2);
});
test('update和lateUpdate同时调用时,onCheckProcessing只应执行一次副作用', () => {
// 这个测试验证修复:onCheckProcessing() 的副作用(时间累加)不应该在 lateUpdate 中重复执行
// 模拟一帧内的完整调用流程
const initialProcessCount = intervalSystem.processCallCount;
// 第一帧:时间不足,不应触发
Time.update(testInterval / 2);
intervalSystem.update();
intervalSystem.lateUpdate();
expect(intervalSystem.processCallCount).toBe(initialProcessCount);
// 第二帧:累计时间刚好达到间隔,应该触发一次
Time.update(testInterval / 2);
intervalSystem.update();
intervalSystem.lateUpdate();
expect(intervalSystem.processCallCount).toBe(initialProcessCount + 1);
// 第三帧:需要再等待完整间隔
Time.update(testInterval / 2);
intervalSystem.update();
intervalSystem.lateUpdate();
expect(intervalSystem.processCallCount).toBe(initialProcessCount + 1);
// 第四帧:再次达到间隔
Time.update(testInterval / 2);
intervalSystem.update();
intervalSystem.lateUpdate();
expect(intervalSystem.processCallCount).toBe(initialProcessCount + 2);
});
test('lateUpdate应复用update的检查结果,不重复累加时间', () => {
// 这是用户报告的 bug 场景:5秒间隔,但实际触发不规律
// 原因是 onCheckProcessing() 在 update 和 lateUpdate 中都被调用,导致时间累加了两次
// 核心验证:在 N*interval 时间内,触发次数应该接近 N 次
// 如果 bug 存在(时间累加两次),触发次数会接近 2N 次
const initialCount = intervalSystem.processCallCount;
// 模拟 50 帧,每帧 testInterval/5 (总共 10*testInterval)
for (let frame = 0; frame < 50; frame++) {
Time.update(testInterval / 5);
intervalSystem.update();
intervalSystem.lateUpdate();
}
// 50帧 * (testInterval/5) = 10*testInterval,应该触发约 10 次
// 如果 bug 存在(时间累加两次),会触发约 20 次
const triggers = intervalSystem.processCallCount - initialCount;
// 正常情况:触发 9-11 次
// Bug 情况:触发 18-22 次
expect(triggers).toBeGreaterThanOrEqual(9);
expect(triggers).toBeLessThanOrEqual(12);
});
test('精确间隔测试 - 验证触发间隔的一致性', () => {
// 验证触发间隔是稳定的,而不是忽大忽小
const initialCount = intervalSystem.processCallCount;
const frameTimes: number[] = [];
let framesSinceLastTrigger = 0;
// 模拟 100 帧,每帧 testInterval/5
for (let frame = 0; frame < 100; frame++) {
Time.update(testInterval / 5);
framesSinceLastTrigger++;
const beforeCount = intervalSystem.processCallCount;
intervalSystem.update();
intervalSystem.lateUpdate();
if (intervalSystem.processCallCount > beforeCount) {
frameTimes.push(framesSinceLastTrigger);
framesSinceLastTrigger = 0;
}
}
// 100帧 * (testInterval/5) = 20*testInterval,应该触发约 20 次
const totalTriggers = intervalSystem.processCallCount - initialCount;
expect(totalTriggers).toBeGreaterThanOrEqual(19);
expect(totalTriggers).toBeLessThanOrEqual(21);
// 验证每次触发的帧间隔都接近 5 帧(因为 5 * testInterval/5 = testInterval
// 如果 bug 存在,帧间隔会不稳定(有的 2-3 帧,有的 7-8 帧)
for (const frames of frameTimes) {
expect(frames).toBeGreaterThanOrEqual(4);
expect(frames).toBeLessThanOrEqual(6);
}
});
});
describe('ProcessingSystem - 处理系统', () => {
@@ -3,15 +3,9 @@
* TypeScript ECS与Rust引擎之间的主桥接层
*/
import type { EngineStats, CameraConfig } from '../types';
import type { SpriteRenderData, TextureLoadRequest, EngineStats, CameraConfig } from '../types';
import type { ITextureEngineBridge } from '@esengine/asset-system';
import type { GameEngine } from '../wasm/es_engine';
import type {
ITextureService,
IDynamicAtlasService,
ICoordinateService,
IRenderConfigService
} from '@esengine/engine-core';
/**
* Engine bridge configuration.
@@ -49,7 +43,7 @@ export interface EngineBridgeConfig {
* bridge.render();
* ```
*/
export class EngineBridge implements ITextureEngineBridge, ITextureService, IDynamicAtlasService, ICoordinateService, IRenderConfigService {
export class EngineBridge implements ITextureEngineBridge {
private engine: GameEngine | null = null;
private config: Required<EngineBridgeConfig>;
private initialized = false;
@@ -58,6 +52,14 @@ export class EngineBridge implements ITextureEngineBridge, ITextureService, IDyn
// 用于将文件路径转换为URL的路径解析器
private pathResolver: ((path: string) => string) | null = null;
// Pre-allocated typed arrays for batch submission
// 预分配的类型数组用于批量提交
private transformBuffer: Float32Array;
private textureIdBuffer: Uint32Array;
private uvBuffer: Float32Array;
private colorBuffer: Uint32Array;
private materialIdBuffer: Uint32Array;
// Statistics | 统计信息
private stats: EngineStats = {
fps: 0,
@@ -84,6 +86,14 @@ export class EngineBridge implements ITextureEngineBridge, ITextureService, IDyn
maxSprites: config.maxSprites ?? 10000,
debug: config.debug ?? false
};
// Pre-allocate buffers | 预分配缓冲区
const maxSprites = this.config.maxSprites;
this.transformBuffer = new Float32Array(maxSprites * 7); // x, y, rot, sx, sy, ox, oy
this.textureIdBuffer = new Uint32Array(maxSprites);
this.uvBuffer = new Float32Array(maxSprites * 4); // u0, v0, u1, v1
this.colorBuffer = new Uint32Array(maxSprites);
this.materialIdBuffer = new Uint32Array(maxSprites);
}
/**
@@ -194,52 +204,56 @@ export class EngineBridge implements ITextureEngineBridge, ITextureService, IDyn
* Submit sprite data for rendering.
*
*
* @param transforms - Transform data [x,y,rot,sx,sy,ox,oy] * count
* @param textureIds - Texture IDs
* @param uvs - UV coordinates [u0,v0,u1,v1] * count
* @param colors - Packed RGBA colors
* @param materialIds - Material IDs
* @param count - Number of sprites
* @param sprites - Array of sprite render data |
*/
submitSprites(
transforms: Float32Array,
textureIds: Uint32Array,
uvs: Float32Array,
colors: Uint32Array,
materialIds: Uint32Array,
count: number
): void {
if (!this.initialized || count === 0) return;
submitSprites(sprites: SpriteRenderData[]): void {
if (!this.initialized || sprites.length === 0) return;
const count = Math.min(sprites.length, this.config.maxSprites);
// Fill typed arrays | 填充类型数组
for (let i = 0; i < count; i++) {
const sprite = sprites[i];
const tOffset = i * 7;
const uvOffset = i * 4;
// Transform data | 变换数据
this.transformBuffer[tOffset] = sprite.x;
this.transformBuffer[tOffset + 1] = sprite.y;
this.transformBuffer[tOffset + 2] = sprite.rotation;
this.transformBuffer[tOffset + 3] = sprite.scaleX;
this.transformBuffer[tOffset + 4] = sprite.scaleY;
this.transformBuffer[tOffset + 5] = sprite.originX;
this.transformBuffer[tOffset + 6] = sprite.originY;
// Texture ID | 纹理ID
this.textureIdBuffer[i] = sprite.textureId;
// UV coordinates | UV坐标
this.uvBuffer[uvOffset] = sprite.uv[0];
this.uvBuffer[uvOffset + 1] = sprite.uv[1];
this.uvBuffer[uvOffset + 2] = sprite.uv[2];
this.uvBuffer[uvOffset + 3] = sprite.uv[3];
// Color | 颜色
this.colorBuffer[i] = sprite.color;
// Material ID (0 = default) | 材质ID0 = 默认)
this.materialIdBuffer[i] = sprite.materialId ?? 0;
}
// Submit to engine (single WASM call) | 提交到引擎(单次WASM调用)
this.getEngine().submitSpriteBatch(
this.transformBuffer.subarray(0, count * 7),
this.textureIdBuffer.subarray(0, count),
this.uvBuffer.subarray(0, count * 4),
this.colorBuffer.subarray(0, count),
this.materialIdBuffer.subarray(0, count)
);
this.getEngine().submitSpriteBatch(transforms, textureIds, uvs, colors, materialIds);
this.stats.spriteCount = count;
}
/**
* Submit mesh batch for rendering arbitrary 2D geometry.
* 2D
*
* Used for rendering ellipses, polygons, and other complex shapes.
*
*
* @param positions - Vertex positions [x, y, ...]
* @param uvs - Texture coordinates [u, v, ...]
* @param colors - Packed RGBA colors (one per vertex)
* @param indices - Triangle indices
* @param textureId - Texture ID (0 = white pixel)
*/
submitMeshBatch(
positions: Float32Array,
uvs: Float32Array,
colors: Uint32Array,
indices: Uint16Array,
textureId: number
): void {
if (!this.initialized || positions.length === 0) return;
this.getEngine().submitMeshBatch(positions, uvs, colors, indices, textureId);
}
/**
* Render the current frame.
*
@@ -279,32 +293,6 @@ export class EngineBridge implements ITextureEngineBridge, ITextureService, IDyn
this.getEngine().renderOverlay();
}
/**
* Set scissor rect for clipping (screen coordinates, Y-down).
* Y
*
* Content outside this rect will be clipped.
*
*
* @param x - Left edge in screen coordinates |
* @param y - Top edge in screen coordinates (Y-down) | Y
* @param width - Rect width |
* @param height - Rect height |
*/
setScissorRect(x: number, y: number, width: number, height: number): void {
if (!this.initialized) return;
this.getEngine().setScissorRect(x, y, width, height);
}
/**
* Clear scissor rect (disable clipping).
*
*/
clearScissorRect(): void {
if (!this.initialized) return;
this.getEngine().clearScissorRect();
}
/**
* Load a texture.
*
@@ -396,33 +384,17 @@ export class EngineBridge implements ITextureEngineBridge, ITextureService, IDyn
}
/**
* Get texture info by path.
*
* Get texture information.
*
*
* This is the primary API for getting texture dimensions.
* The Rust engine is the single source of truth for texture dimensions.
* API
* Rust
*
* @param path - Image path/URL | /URL
* @returns Texture info or null if not loaded | null
* @param id - Texture ID | ID
*/
getTextureInfoByPath(path: string): { width: number; height: number } | null {
getTextureInfo(id: number): { width: number; height: number } | null {
if (!this.initialized) return null;
// Resolve path if resolver is set
// 如果设置了解析器,则解析路径
const resolvedPath = this.pathResolver ? this.pathResolver(path) : path;
// Query Rust engine for texture size
// 向 Rust 引擎查询纹理尺寸
const result = this.getEngine().getTextureSizeByPath(resolvedPath);
if (!result) return null;
return {
width: result[0],
height: result[1]
};
// TODO: Implement in Rust engine
// TODO: 在Rust引擎中实现
// Return default values for now / 暂时返回默认值
return { width: 64, height: 64 };
}
/**
@@ -652,24 +624,6 @@ export class EngineBridge implements ITextureEngineBridge, ITextureService, IDyn
this.getEngine().setTransformMode(mode);
}
/**
* Render a 3D gizmo at the specified world position.
* 3D Gizmo
*
* Only works in 3D render mode. The gizmo will be rendered
* with the current transform mode (move/rotate/scale).
* 3D Gizmo 使
*
* @param x - World X position | X
* @param y - World Y position | Y
* @param z - World Z position | Z
* @param scale - Gizmo scale multiplier | Gizmo
*/
render3DGizmo(x: number, y: number, z: number, scale: number = 1.0): void {
if (!this.initialized) return;
this.getEngine().render3DGizmo(x, y, z, scale);
}
/**
* Set gizmo visibility.
*
@@ -1056,491 +1010,6 @@ export class EngineBridge implements ITextureEngineBridge, ITextureService, IDyn
});
}
// ===== Shader API =====
// ===== 着色器 API =====
/**
* Compile and register a custom shader program.
*
*
* @param vertexSource - Vertex shader GLSL source | GLSL
* @param fragmentSource - Fragment shader GLSL source | GLSL
* @returns Promise resolving to shader ID | ID Promise
*/
async compileShader(vertexSource: string, fragmentSource: string): Promise<number> {
if (!this.initialized) throw new Error('Engine not initialized');
return this.getEngine().compileShader(vertexSource, fragmentSource);
}
/**
* Compile and register a shader with a specific ID.
* 使 ID
*
* @param shaderId - Desired shader ID | ID
* @param vertexSource - Vertex shader GLSL source | GLSL
* @param fragmentSource - Fragment shader GLSL source | GLSL
*/
async compileShaderWithId(shaderId: number, vertexSource: string, fragmentSource: string): Promise<void> {
if (!this.initialized) throw new Error('Engine not initialized');
this.getEngine().compileShaderWithId(shaderId, vertexSource, fragmentSource);
}
/**
* Check if a shader exists.
*
*
* @param shaderId - Shader ID to check | ID
*/
hasShader(shaderId: number): boolean {
if (!this.initialized) return false;
return this.getEngine().hasShader(shaderId);
}
/**
* Remove a shader.
*
*
* @param shaderId - Shader ID to remove | ID
*/
removeShader(shaderId: number): boolean {
if (!this.initialized) return false;
return this.getEngine().removeShader(shaderId);
}
// ===== Material Management API =====
// ===== 材质管理 API =====
/**
* Create a new material.
*
*
* @param name - Material name |
* @param shaderId - Shader ID to use | 使 ID
* @param blendMode - Blend mode |
* @returns Material ID | ID
*/
createMaterial(name: string, shaderId: number, blendMode: number): number {
if (!this.initialized) return -1;
return this.getEngine().createMaterial(name, shaderId, blendMode);
}
/**
* Create a material with a specific ID.
* 使 ID
*
* @param materialId - Desired material ID | ID
* @param name - Material name |
* @param shaderId - Shader ID to use | 使 ID
* @param blendMode - Blend mode |
*/
createMaterialWithId(materialId: number, name: string, shaderId: number, blendMode: number): void {
if (!this.initialized) return;
this.getEngine().createMaterialWithId(materialId, name, shaderId, blendMode);
}
/**
* Check if a material exists.
*
*
* @param materialId - Material ID to check | ID
*/
hasMaterial(materialId: number): boolean {
if (!this.initialized) return false;
return this.getEngine().hasMaterial(materialId);
}
/**
* Remove a material.
*
*
* @param materialId - Material ID to remove | ID
*/
removeMaterial(materialId: number): boolean {
if (!this.initialized) return false;
return this.getEngine().removeMaterial(materialId);
}
// ===== Material Uniform API =====
// ===== 材质 Uniform API =====
/**
* Set a float uniform on a material.
* uniform
*
* @param materialId - Material ID | ID
* @param name - Uniform name | Uniform
* @param value - Float value |
* @returns Whether the operation succeeded |
*/
setMaterialFloat(materialId: number, name: string, value: number): boolean {
if (!this.initialized) return false;
return this.getEngine().setMaterialFloat(materialId, name, value);
}
/**
* Set a vec2 uniform on a material.
* vec2 uniform
*
* @param materialId - Material ID | ID
* @param name - Uniform name | Uniform
* @param x - X component | X
* @param y - Y component | Y
* @returns Whether the operation succeeded |
*/
setMaterialVec2(materialId: number, name: string, x: number, y: number): boolean {
if (!this.initialized) return false;
return this.getEngine().setMaterialVec2(materialId, name, x, y);
}
/**
* Set a vec3 uniform on a material.
* vec3 uniform
*
* @param materialId - Material ID | ID
* @param name - Uniform name | Uniform
* @param x - X component | X
* @param y - Y component | Y
* @param z - Z component | Z
* @returns Whether the operation succeeded |
*/
setMaterialVec3(materialId: number, name: string, x: number, y: number, z: number): boolean {
if (!this.initialized) return false;
return this.getEngine().setMaterialVec3(materialId, name, x, y, z);
}
/**
* Set a vec4 uniform on a material.
* vec4 uniform
*
* @param materialId - Material ID | ID
* @param name - Uniform name | Uniform
* @param x - X component | X
* @param y - Y component | Y
* @param z - Z component | Z
* @param w - W component | W
* @returns Whether the operation succeeded |
*/
setMaterialVec4(materialId: number, name: string, x: number, y: number, z: number, w: number): boolean {
if (!this.initialized) return false;
return this.getEngine().setMaterialVec4(materialId, name, x, y, z, w);
}
/**
* Set a color uniform on a material.
* uniform
*
* @param materialId - Material ID | ID
* @param name - Uniform name | Uniform
* @param r - Red component (0-1) | (0-1)
* @param g - Green component (0-1) | 绿 (0-1)
* @param b - Blue component (0-1) | (0-1)
* @param a - Alpha component (0-1) | Alpha (0-1)
* @returns Whether the operation succeeded |
*/
setMaterialColor(materialId: number, name: string, r: number, g: number, b: number, a: number): boolean {
if (!this.initialized) return false;
return this.getEngine().setMaterialColor(materialId, name, r, g, b, a);
}
/**
* Set a material's blend mode.
*
*
* @param materialId - Material ID | ID
* @param blendMode - Blend mode (0=None, 1=Alpha, 2=Additive, 3=Multiply, 4=Screen, 5=PremultipliedAlpha)
* (0=, 1=Alpha, 2=, 3=, 4=, 5=Alpha)
* @returns Whether the operation succeeded |
*/
setMaterialBlendMode(materialId: number, blendMode: number): boolean {
if (!this.initialized) return false;
return this.getEngine().setMaterialBlendMode(materialId, blendMode);
}
// ===== Dynamic Atlas API =====
// ===== 动态图集 API =====
/**
* Create a blank texture for dynamic atlas.
*
*
* This creates a texture that can be filled later using `updateTextureRegion`.
* Used for runtime atlas generation to batch UI elements with different textures.
* 使 `updateTextureRegion`
* 使 UI
*
* @param width - Texture width in pixels (recommended: 2048) | 2048
* @param height - Texture height in pixels (recommended: 2048) | 2048
* @returns Texture ID for the created blank texture | ID
*/
createBlankTexture(width: number, height: number): number {
if (!this.initialized) return -1;
return this.getEngine().createBlankTexture(width, height);
}
/**
* Update a region of an existing texture with pixel data.
* 使
*
* This is used for dynamic atlas to copy individual textures into the atlas.
*
*
* @param id - The texture ID to update | ID
* @param x - X offset in the texture | X偏移
* @param y - Y offset in the texture | Y偏移
* @param width - Width of the region to update |
* @param height - Height of the region to update |
* @param pixels - RGBA pixel data (Uint8Array, 4 bytes per pixel) | RGBA像素数据4
*/
updateTextureRegion(
id: number,
x: number,
y: number,
width: number,
height: number,
pixels: Uint8Array
): void {
if (!this.initialized) return;
this.getEngine().updateTextureRegion(id, x, y, width, height, pixels);
}
/**
* Apply material overrides to a material.
*
*
* @param materialId - Material ID | ID
* @param overrides - Material property overrides |
*/
applyMaterialOverrides(materialId: number, overrides: Record<string, { type: string; value: number | number[] }>): void {
if (!this.initialized || !overrides) return;
for (const [name, override] of Object.entries(overrides)) {
const { type, value } = override;
switch (type) {
case 'float':
this.setMaterialFloat(materialId, name, value as number);
break;
case 'vec2':
{
const v = value as number[];
this.setMaterialVec2(materialId, name, v[0], v[1]);
}
break;
case 'vec3':
{
const v = value as number[];
this.setMaterialVec3(materialId, name, v[0], v[1], v[2]);
}
break;
case 'vec4':
{
const v = value as number[];
this.setMaterialVec4(materialId, name, v[0], v[1], v[2], v[3]);
}
break;
case 'color':
{
const v = value as number[];
this.setMaterialColor(materialId, name, v[0], v[1], v[2], v[3] ?? 1.0);
}
break;
case 'int':
// Int is passed as float | Int 作为 float 传递
this.setMaterialFloat(materialId, name, value as number);
break;
}
}
}
// ===== 3D Rendering API =====
// ===== 3D 渲染 API =====
/**
* Render mode enumeration.
*
*/
static readonly RenderMode = {
Mode2D: 0,
Mode3D: 1,
} as const;
/**
* Get current render mode.
*
*
* @returns 0 for 2D mode, 1 for 3D mode | 0 2D 1 3D
*/
getRenderMode(): number {
if (!this.initialized) return 0;
return this.getEngine().getRenderMode();
}
/**
* Set render mode.
*
*
* When switching to 3D mode for the first time, the 3D renderer
* will be lazily initialized.
* 3D 3D
*
* @param mode - 0 for 2D, 1 for 3D | 0 2D1 3D
*/
setRenderMode(mode: number): void {
if (!this.initialized) return;
this.getEngine().setRenderMode(mode);
}
/**
* Check if 3D renderer is initialized.
* 3D
*/
has3DRenderer(): boolean {
if (!this.initialized) return false;
return this.getEngine().has3DRenderer();
}
/**
* Get 3D camera position.
* 3D
*
* @returns { x, y, z } or null if 3D renderer is not initialized
* { x, y, z } 3D null
*/
getCamera3DPosition(): { x: number; y: number; z: number } | null {
if (!this.initialized) return null;
const result = this.getEngine().getCamera3DPosition();
if (!result) return null;
return { x: result[0], y: result[1], z: result[2] };
}
/**
* Set 3D camera position.
* 3D
*
* @param x - X coordinate | X
* @param y - Y coordinate | Y
* @param z - Z coordinate | Z
*/
setCamera3DPosition(x: number, y: number, z: number): void {
if (!this.initialized) return;
this.getEngine().setCamera3DPosition(x, y, z);
}
/**
* Get 3D camera rotation as Euler angles (in degrees).
* 3D
*
* @returns { pitch, yaw, roll } in degrees or null
* { pitch, yaw, roll } null
*/
getCamera3DRotation(): { pitch: number; yaw: number; roll: number } | null {
if (!this.initialized) return null;
const result = this.getEngine().getCamera3DRotation();
if (!result) return null;
return { pitch: result[0], yaw: result[1], roll: result[2] };
}
/**
* Set 3D camera rotation from Euler angles (in degrees).
* 使 3D
*
* @param pitch - Pitch angle in degrees |
* @param yaw - Yaw angle in degrees |
* @param roll - Roll angle in degrees |
*/
setCamera3DRotation(pitch: number, yaw: number, roll: number): void {
if (!this.initialized) return;
this.getEngine().setCamera3DRotation(pitch, yaw, roll);
}
/**
* Get 3D camera field of view (in degrees).
* 3D
*
* @returns FOV in degrees or null if 3D renderer not initialized
* FOV null
*/
getCamera3DFov(): number | null {
if (!this.initialized) return null;
const result = this.getEngine().getCamera3DFov();
return result ?? null;
}
/**
* Set 3D camera field of view (in degrees).
* 3D
*
* @param fov - Field of view in degrees (typical: 45-90)
* 45-90
*/
setCamera3DFov(fov: number): void {
if (!this.initialized) return;
this.getEngine().setCamera3DFov(fov);
}
/**
* Set 3D camera projection type.
* 3D
*
* @param type - 0 for perspective, 1 for orthographic
* 0 1
* @param orthoSize - Half-height of orthographic view (only used when type = 1)
* type = 1 使
*/
setCamera3DProjection(type: number, orthoSize: number = 5.0): void {
if (!this.initialized) return;
this.getEngine().setCamera3DProjection(type, orthoSize);
}
/**
* Make 3D camera look at a target position.
* 使 3D
*
* @param targetX - Target X coordinate | X
* @param targetY - Target Y coordinate | Y
* @param targetZ - Target Z coordinate | Z
*/
camera3DLookAt(targetX: number, targetY: number, targetZ: number): void {
if (!this.initialized) return;
this.getEngine().camera3DLookAt(targetX, targetY, targetZ);
}
/**
* Set 3D camera near and far clip planes.
* 3D
*
* @param near - Near clip plane distance |
* @param far - Far clip plane distance |
*/
setCamera3DClipPlanes(near: number, far: number): void {
if (!this.initialized) return;
this.getEngine().setCamera3DClipPlanes(near, far);
}
/**
* Resize 3D viewport.
* 3D
*
* @param width - New width |
* @param height - New height |
*/
resize3D(width: number, height: number): void {
if (!this.initialized) return;
this.getEngine().resize3D(width, height);
}
/**
* Render using 3D mode.
* 使 3D
*
* This renders all submitted 3D meshes with the current 3D camera.
* 使 3D 3D
*/
render3D(): void {
if (!this.initialized) return;
this.getEngine().render3D();
}
/**
* Dispose the bridge and release resources.
*
@@ -1,386 +1,105 @@
/**
* High-performance render batcher using Structure of Arrays (SoA) pattern.
* 使 (SoA)
* Render batcher for collecting sprite data.
*
*/
import type { SpriteRenderData } from '../types';
/**
* Collects and sorts sprite render data for batch submission.
*
*
* Optimizations:
*
* - Pre-allocated typed arrays to avoid per-frame GC
* GC
* - SoA layout for cache-friendly access
* SoA 访
* - Object pool for SpriteRenderData when interface compatibility is needed
* 使 SpriteRenderData
*/
import type { SpriteRenderData, MaterialOverrides } from '../types';
/**
* Default maximum sprites per batch.
*
*/
const DEFAULT_MAX_SPRITES = 10000;
/**
* High-performance render batcher with SoA storage.
* 使 SoA
* This class is used to collect sprites during the ECS update loop
* and then submit them all at once to the engine.
* ECS更新循环中收集精灵
*
* @example
* ```typescript
* const batcher = new RenderBatcher(10000);
* const batcher = new RenderBatcher();
*
* // Add sprites using SoA API (fastest) | 使用 SoA API 添加精灵(最快)
* batcher.addSpriteSoA(x, y, rot, sx, sy, ox, oy, texId, u0, v0, u1, v1, color, matId);
* // During ECS update | 在ECS更新期间
* batcher.addSprite({
* x: 100, y: 200,
* rotation: 0,
* scaleX: 1, scaleY: 1,
* originX: 0.5, originY: 0.5,
* textureId: 1,
* uv: [0, 0, 1, 1],
* color: 0xFFFFFFFF
* });
*
* // Or use object API for compatibility | 或使用对象 API 以保持兼容性
* batcher.addSprite(spriteData);
*
* // Get typed arrays for submission | 获取类型数组用于提交
* const { transforms, textureIds, uvs, colors, materialIds, count } = batcher.getBuffers();
*
* // At end of frame | 帧结束时
* // At end of frame | 在帧结束时
* bridge.submitSprites(batcher.getSprites());
* batcher.clear();
* ```
*/
export class RenderBatcher {
// ===== SoA Buffers (pre-allocated) =====
// ===== SoA 缓冲区(预分配)=====
/** Transform data: [x, y, rotation, scaleX, scaleY, originX, originY] per sprite */
private _transforms: Float32Array;
/** Texture IDs | 纹理 ID */
private _textureIds: Uint32Array;
/** UV coordinates: [u0, v0, u1, v1] per sprite */
private _uvs: Float32Array;
/** Packed RGBA colors | 打包的 RGBA 颜色 */
private _colors: Uint32Array;
/** Material IDs | 材质 ID */
private _materialIds: Uint32Array;
/** Current sprite count | 当前精灵数量 */
private _count: number = 0;
/** Maximum sprites capacity | 最大精灵容量 */
private _capacity: number;
// ===== Object Pool for SpriteRenderData compatibility =====
// ===== SpriteRenderData 对象池用于兼容性 =====
/** Pool of reusable SpriteRenderData objects | 可复用的 SpriteRenderData 对象池 */
private _spritePool: SpriteRenderData[] = [];
/** Current pool index | 当前池索引 */
private _poolIndex: number = 0;
// ===== Material Overrides Storage =====
// ===== 材质覆盖存储 =====
/** Material overrides by sprite index | 按精灵索引存储的材质覆盖 */
private _materialOverrides: Map<number, MaterialOverrides> = new Map();
/** Clip rects by sprite index | 按精灵索引存储的裁剪矩形 */
private _clipRects: Map<number, { x: number; y: number; width: number; height: number }> = new Map();
private sprites: SpriteRenderData[] = [];
private sortByZ = false;
/**
* Create a new render batcher with pre-allocated buffers.
*
* Create a new render batcher.
*
*
* @param capacity - Maximum sprites (default: 10000) | 10000
* @param sortByZ - Whether to sort sprites by Z order | Z顺序排序精灵
*/
constructor(capacity: number = DEFAULT_MAX_SPRITES) {
this._capacity = capacity;
// Pre-allocate all buffers | 预分配所有缓冲区
this._transforms = new Float32Array(capacity * 7);
this._textureIds = new Uint32Array(capacity);
this._uvs = new Float32Array(capacity * 4);
this._colors = new Uint32Array(capacity);
this._materialIds = new Uint32Array(capacity);
// Pre-populate object pool | 预填充对象池
this._initPool();
constructor(sortByZ = false) {
this.sortByZ = sortByZ;
}
/**
* Initialize the object pool with reusable objects.
* 使
*/
private _initPool(): void {
// Create a smaller initial pool, expand on demand
// 创建较小的初始池,按需扩展
const initialPoolSize = Math.min(1000, this._capacity);
for (let i = 0; i < initialPoolSize; i++) {
this._spritePool.push(this._createSpriteData());
}
}
/**
* Create a new SpriteRenderData object.
* SpriteRenderData
*/
private _createSpriteData(): SpriteRenderData {
return {
x: 0,
y: 0,
rotation: 0,
scaleX: 1,
scaleY: 1,
originX: 0.5,
originY: 0.5,
textureId: 0,
uv: [0, 0, 1, 1],
color: 0xFFFFFFFF,
materialId: 0
};
}
/**
* Get a SpriteRenderData object from pool.
* SpriteRenderData
*/
private _getFromPool(): SpriteRenderData {
if (this._poolIndex >= this._spritePool.length) {
// Expand pool | 扩展池
this._spritePool.push(this._createSpriteData());
}
return this._spritePool[this._poolIndex++];
}
// ===== High-Performance SoA API =====
// ===== 高性能 SoA API =====
/**
* Add a sprite using direct SoA parameters (fastest method).
* 使 SoA
*
* @returns Sprite index for additional data (overrides, clipRect) |
*/
addSpriteSoA(
x: number,
y: number,
rotation: number,
scaleX: number,
scaleY: number,
originX: number,
originY: number,
textureId: number,
u0: number,
v0: number,
u1: number,
v1: number,
color: number,
materialId: number = 0
): number {
if (this._count >= this._capacity) {
console.warn('RenderBatcher capacity exceeded | RenderBatcher 容量已满');
return -1;
}
const i = this._count;
const tOffset = i * 7;
const uvOffset = i * 4;
// Write transform data | 写入变换数据
this._transforms[tOffset] = x;
this._transforms[tOffset + 1] = y;
this._transforms[tOffset + 2] = rotation;
this._transforms[tOffset + 3] = scaleX;
this._transforms[tOffset + 4] = scaleY;
this._transforms[tOffset + 5] = originX;
this._transforms[tOffset + 6] = originY;
// Write texture ID | 写入纹理 ID
this._textureIds[i] = textureId;
// Write UV coordinates | 写入 UV 坐标
this._uvs[uvOffset] = u0;
this._uvs[uvOffset + 1] = v0;
this._uvs[uvOffset + 2] = u1;
this._uvs[uvOffset + 3] = v1;
// Write color and material | 写入颜色和材质
this._colors[i] = color;
this._materialIds[i] = materialId;
this._count++;
return i;
}
/**
* Set material overrides for a sprite.
*
*
* @param index - Sprite index from addSpriteSoA | addSpriteSoA
* @param overrides - Material overrides |
*/
setMaterialOverrides(index: number, overrides: MaterialOverrides): void {
if (index >= 0 && index < this._count) {
this._materialOverrides.set(index, overrides);
}
}
/**
* Set clip rect for a sprite.
*
*
* @param index - Sprite index from addSpriteSoA | addSpriteSoA
* @param clipRect - Clip rectangle |
*/
setClipRect(index: number, clipRect: { x: number; y: number; width: number; height: number }): void {
if (index >= 0 && index < this._count) {
this._clipRects.set(index, clipRect);
}
}
// ===== Object API (for compatibility) =====
// ===== 对象 API(用于兼容性)=====
/**
* Add a sprite using SpriteRenderData object.
* 使 SpriteRenderData
*
* This method is kept for backward compatibility but internally uses SoA storage.
* 使 SoA
* Add a sprite to the batch.
*
*
* @param sprite - Sprite render data |
*/
addSprite(sprite: SpriteRenderData): void {
const index = this.addSpriteSoA(
sprite.x,
sprite.y,
sprite.rotation,
sprite.scaleX,
sprite.scaleY,
sprite.originX,
sprite.originY,
sprite.textureId,
sprite.uv[0],
sprite.uv[1],
sprite.uv[2],
sprite.uv[3],
sprite.color,
sprite.materialId ?? 0
);
// Store optional data | 存储可选数据
if (index >= 0) {
if (sprite.materialOverrides) {
this._materialOverrides.set(index, sprite.materialOverrides);
}
if (sprite.clipRect) {
this._clipRects.set(index, sprite.clipRect);
}
}
this.sprites.push(sprite);
}
/**
* Add multiple sprites.
*
* Add multiple sprites to the batch.
*
*
* @param sprites - Array of sprite render data |
*/
addSprites(sprites: SpriteRenderData[]): void {
for (let i = 0; i < sprites.length; i++) {
this.addSprite(sprites[i]);
}
}
// ===== Buffer Access =====
// ===== 缓冲区访问 =====
/**
* Get raw typed array buffers for direct submission to engine.
*
*
* Returns subarray views (zero-copy) for only the used portion.
* 使 subarray
*/
getBuffers(): {
transforms: Float32Array;
textureIds: Uint32Array;
uvs: Float32Array;
colors: Uint32Array;
materialIds: Uint32Array;
count: number;
} {
return {
transforms: this._transforms.subarray(0, this._count * 7),
textureIds: this._textureIds.subarray(0, this._count),
uvs: this._uvs.subarray(0, this._count * 4),
colors: this._colors.subarray(0, this._count),
materialIds: this._materialIds.subarray(0, this._count),
count: this._count
};
this.sprites.push(...sprites);
}
/**
* Get sprites as SpriteRenderData array (for legacy compatibility).
* SpriteRenderData
* Get all sprites in the batch.
*
*
* Uses object pool to avoid allocations.
* 使
*
* @returns Array of sprites from pool |
* @returns Sorted array of sprites |
*/
getSprites(): SpriteRenderData[] {
// Reset pool index to reuse objects | 重置池索引以复用对象
this._poolIndex = 0;
const result: SpriteRenderData[] = [];
for (let i = 0; i < this._count; i++) {
const sprite = this._getFromPool();
const tOffset = i * 7;
const uvOffset = i * 4;
// Fill from SoA buffers | 从 SoA 缓冲区填充
sprite.x = this._transforms[tOffset];
sprite.y = this._transforms[tOffset + 1];
sprite.rotation = this._transforms[tOffset + 2];
sprite.scaleX = this._transforms[tOffset + 3];
sprite.scaleY = this._transforms[tOffset + 4];
sprite.originX = this._transforms[tOffset + 5];
sprite.originY = this._transforms[tOffset + 6];
sprite.textureId = this._textureIds[i];
sprite.uv[0] = this._uvs[uvOffset];
sprite.uv[1] = this._uvs[uvOffset + 1];
sprite.uv[2] = this._uvs[uvOffset + 2];
sprite.uv[3] = this._uvs[uvOffset + 3];
sprite.color = this._colors[i];
sprite.materialId = this._materialIds[i];
// Attach optional data | 附加可选数据
sprite.materialOverrides = this._materialOverrides.get(i);
sprite.clipRect = this._clipRects.get(i);
result.push(sprite);
// Sort by material ID first, then texture ID for better batching
// 先按材质ID排序,再按纹理ID排序以获得更好的批处理效果
if (!this.sortByZ) {
this.sprites.sort((a, b) => {
const materialDiff = (a.materialId || 0) - (b.materialId || 0);
if (materialDiff !== 0) return materialDiff;
return a.textureId - b.textureId;
});
}
return result;
return this.sprites;
}
// ===== State =====
// ===== 状态 =====
/**
* Get sprite count.
*
*/
get count(): number {
return this._count;
return this.sprites.length;
}
/**
* Get capacity.
*
* Clear all sprites from the batch.
*
*/
get capacity(): number {
return this._capacity;
clear(): void {
this.sprites.length = 0;
}
/**
@@ -388,52 +107,6 @@ export class RenderBatcher {
*
*/
get isEmpty(): boolean {
return this._count === 0;
}
/**
* Clear all sprites from the batch.
*
*
* Does NOT deallocate buffers - they are reused next frame.
* -
*/
clear(): void {
this._count = 0;
this._poolIndex = 0;
this._materialOverrides.clear();
this._clipRects.clear();
}
/**
* Check if material overrides exist for any sprite.
*
*/
hasMaterialOverrides(): boolean {
return this._materialOverrides.size > 0;
}
/**
* Get material overrides for a sprite index.
*
*/
getMaterialOverrides(index: number): MaterialOverrides | undefined {
return this._materialOverrides.get(index);
}
/**
* Check if clip rects exist for any sprite.
*
*/
hasClipRects(): boolean {
return this._clipRects.size > 0;
}
/**
* Get clip rect for a sprite index.
*
*/
getClipRect(index: number): { x: number; y: number; width: number; height: number } | undefined {
return this._clipRects.get(index);
return this.sprites.length === 0;
}
}
@@ -143,11 +143,7 @@ export class SpriteRenderHelper {
*/
render(): void {
if (!this.batcher.isEmpty) {
const buffers = this.batcher.getBuffers();
this.bridge.submitSprites(
buffers.transforms, buffers.textureIds, buffers.uvs,
buffers.colors, buffers.materialIds, buffers.count
);
this.bridge.submitSprites(this.batcher.getSprites());
}
this.bridge.render();
}
+3 -11
View File
@@ -8,20 +8,12 @@
// Service tokens and interfaces (谁定义接口,谁导出 Token)
export {
RenderSystemToken,
EngineBridgeToken,
EngineIntegrationToken,
// 新的单一职责服务令牌 | New single-responsibility service tokens
TextureServiceToken,
DynamicAtlasServiceToken,
CoordinateServiceToken,
RenderConfigServiceToken,
// 接口类型 | Interface types
type IRenderSystem,
type IEngineBridge,
type IEngineIntegration,
type IRenderDataProvider,
type ITextureService,
type IDynamicAtlasService,
type ICoordinateService,
type IRenderConfigService
type IRenderDataProvider
} from './tokens';
export { EngineBridge } from './core/EngineBridge';
@@ -12,7 +12,7 @@ import { SpriteComponent } from '@esengine/sprite';
import type { EngineBridge } from '../core/EngineBridge';
import { RenderBatcher } from '../core/RenderBatcher';
import type { ITransformComponent } from '../core/SpriteRenderHelper';
import type { SpriteRenderData, MaterialOverrides } from '../types';
import type { SpriteRenderData } from '../types';
/**
* Render data from a provider
@@ -47,17 +47,6 @@ export interface ProviderRenderData {
* Overrides sortingLayer's bScreenSpace setting, for particles that need dynamic render space.
*/
bScreenSpace?: boolean;
/** Material IDs for each primitive. | 每个原语的材质 ID。 */
materialIds?: Uint32Array;
/** Material overrides (per-group). | 材质覆盖(按组)。 */
materialOverrides?: MaterialOverrides;
/**
* Clip rectangle for scissor test (screen coordinates).
* All primitives in this batch will be clipped to this rect.
* scissor test
*
*/
clipRect?: { x: number; y: number; width: number; height: number };
}
/**
@@ -68,23 +57,6 @@ export interface IRenderDataProvider {
getRenderData(): readonly ProviderRenderData[];
}
/**
* Mesh render data for arbitrary 2D geometry
* 2D
*/
export interface MeshRenderData {
/** Vertex positions [x, y, ...] | 顶点位置 */
positions: Float32Array;
/** Texture coordinates [u, v, ...] | 纹理坐标 */
uvs: Float32Array;
/** Vertex colors (packed RGBA) | 顶点颜色 */
colors: Uint32Array;
/** Triangle indices | 三角形索引 */
indices: Uint16Array;
/** Texture ID (0 = white pixel) | 纹理 ID */
textureId: number;
}
/**
* Interface for UI render data providers
* UI
@@ -95,8 +67,6 @@ export interface MeshRenderData {
export interface IUIRenderDataProvider extends IRenderDataProvider {
/** Get UI render data | 获取 UI 渲染数据 */
getRenderData(): readonly ProviderRenderData[];
/** Get mesh render data for complex shapes | 获取复杂形状的网格渲染数据 */
getMeshRenderData?(): readonly MeshRenderData[];
/** @deprecated Use getRenderData() instead */
getScreenSpaceRenderData?(): readonly ProviderRenderData[];
/** @deprecated World space UI is no longer supported */
@@ -162,24 +132,6 @@ export type GizmoDataProviderFn = (
*/
export type HasGizmoProviderFn = (component: Component) => boolean;
/**
* Function type for getting highlight color for gizmo.
* Used to inject GizmoInteractionService functionality from editor layer.
* gizmo
* GizmoInteractionService
*/
export type GizmoHighlightColorFn = (
entityId: number,
baseColor: GizmoColorInternal,
isSelected: boolean
) => GizmoColorInternal;
/**
* Function type for getting hovered entity ID.
* ID
*/
export type GetHoveredEntityIdFn = () => number | null;
/**
* Type for transform component constructor.
*
@@ -198,28 +150,28 @@ export type TransformComponentType = ComponentType & (new (...args: any[]) => Co
*/
export type AssetPathResolverFn = (guidOrPath: string) => string;
/**
* Render item for sorting.
*
* @internal
*/
interface RenderItem {
sortKey: number;
addIndex: number;
/** Start index in batcher | 在批处理器中的起始索引 */
startIndex: number;
/** Number of sprites | 精灵数量 */
count: number;
}
/**
* ECS System for rendering sprites using the Rust engine.
* 使Rust引擎渲染精灵的ECS系统
*
* This system extends EntitySystem and integrates with the ECS lifecycle.
* EntitySystem并与ECS生命周期集成
*
* @example
* ```typescript
* // Create transform component | 创建变换组件
* @ECSComponent('Transform')
* class Transform extends Component implements ITransformComponent {
* position = { x: 0, y: 0 };
* rotation = 0;
* scale = { x: 1, y: 1 };
* }
*
* // Initialize bridge | 初始化桥接
* const bridge = new EngineBridge({ canvasId: 'canvas' });
* await bridge.initialize();
*
* // Add system to scene | 将系统添加到场景
* const renderSystem = new EngineRenderSystem(bridge, Transform);
* scene.addSystem(renderSystem);
* ```
@@ -227,8 +179,7 @@ interface RenderItem {
@ECSSystem('EngineRender', { updateOrder: 1000 }) // Render system executes last | 渲染系统最后执行
export class EngineRenderSystem extends EntitySystem {
private bridge: EngineBridge;
private worldBatcher: RenderBatcher;
private screenBatcher: RenderBatcher;
private batcher: RenderBatcher;
private transformType: TransformComponentType;
private showGizmos = true;
private selectedEntityIds: Set<number> = new Set();
@@ -238,17 +189,6 @@ export class EngineRenderSystem extends EntitySystem {
// 可重用的映射以避免每帧分配
private entityRenderMap: Map<number, SpriteRenderData> = new Map();
// ===== Pre-allocated arrays for render items =====
// ===== 渲染项的预分配数组 =====
private worldSpaceItems: RenderItem[] = [];
private screenSpaceItems: RenderItem[] = [];
private worldSpaceItemCount = 0;
private screenSpaceItemCount = 0;
// Frame counter for dirty tracking
// 用于脏标记追踪的帧计数器
private frameNumber = 0;
// Additional render data providers (e.g., tilemap)
// 额外的渲染数据提供者(如瓦片地图)
private renderDataProviders: IRenderDataProvider[] = [];
@@ -258,11 +198,6 @@ export class EngineRenderSystem extends EntitySystem {
private gizmoDataProvider: GizmoDataProviderFn | null = null;
private hasGizmoProvider: HasGizmoProviderFn | null = null;
// Gizmo interaction functions (injected from editor layer)
// Gizmo 交互函数(从编辑器层注入)
private gizmoHighlightColorFn: GizmoHighlightColorFn | null = null;
private getHoveredEntityIdFn: GetHoveredEntityIdFn | null = null;
// UI Canvas boundary settings
// UI 画布边界设置
private uiCanvasWidth: number = 0;
@@ -296,8 +231,7 @@ export class EngineRenderSystem extends EntitySystem {
super(Matcher.empty().all(SpriteComponent, transformType));
this.bridge = bridge;
this.worldBatcher = new RenderBatcher();
this.screenBatcher = new RenderBatcher();
this.batcher = new RenderBatcher();
this.transformType = transformType;
}
@@ -315,16 +249,8 @@ export class EngineRenderSystem extends EntitySystem {
*
*/
protected override onBegin(): void {
// Increment frame counter | 递增帧计数器
this.frameNumber++;
// Clear the batches | 清空批处理
this.worldBatcher.clear();
this.screenBatcher.clear();
// Reset render item counts (reuse arrays) | 重置渲染项计数(复用数组)
this.worldSpaceItemCount = 0;
this.screenSpaceItemCount = 0;
// Clear the batch | 清空批处理
this.batcher.clear();
// Clear screen with dark background | 用深色背景清屏
this.bridge.clear(0.1, 0.1, 0.12, 1);
@@ -353,319 +279,320 @@ export class EngineRenderSystem extends EntitySystem {
// 清空并重用映射用于绘制 gizmo
this.entityRenderMap.clear();
this.collectEntitySprites(entities);
this.collectProviderRenderData();
this.collectUIRenderData();
// Collect all render items separated by render space
// 按渲染空间分离收集所有渲染项
const worldSpaceItems: Array<{ sortKey: number; sprites: SpriteRenderData[] }> = [];
const screenSpaceItems: Array<{ sortKey: number; sprites: SpriteRenderData[] }> = [];
this.renderWorldSpace();
if (this.previewMode && this.screenSpaceItemCount > 0) {
this.renderScreenSpace();
}
}
// Collect sprites from entities (all in world space)
// 收集实体的 sprites(都在世界空间)
this.collectEntitySprites(entities, worldSpaceItems);
/** @internal */
private addRenderItem(
items: RenderItem[],
itemCount: number,
sortKey: number,
addIndex: number,
startIndex: number,
count: number
): number {
if (itemCount >= items.length) {
items.push({ sortKey: 0, addIndex: 0, startIndex: 0, count: 0 });
// Collect render data from providers (e.g., tilemap, particle)
// 收集渲染数据提供者的数据(如瓦片地图、粒子)
this.collectProviderRenderData(worldSpaceItems, screenSpaceItems);
// Collect UI render data
// 收集 UI 渲染数据
if (this.uiRenderDataProvider) {
const uiRenderData = this.uiRenderDataProvider.getRenderData();
for (const data of uiRenderData) {
const uiSprites = this.convertProviderDataToSprites(data);
if (uiSprites.length > 0) {
const sortKey = sortingLayerManager.getSortKey(data.sortingLayer, data.orderInLayer);
// UI always goes to screen space in preview mode, world space in editor mode
// UI 在预览模式下始终在屏幕空间,编辑器模式下在世界空间
if (this.previewMode) {
screenSpaceItems.push({ sortKey, sprites: uiSprites });
} else {
worldSpaceItems.push({ sortKey, sprites: uiSprites });
}
}
}
}
// ===== Pass 1: World Space Rendering =====
// ===== 阶段 1:世界空间渲染 =====
this.renderWorldSpacePass(worldSpaceItems);
// ===== Pass 2: Screen Space Rendering (Preview Mode Only) =====
// ===== 阶段 2:屏幕空间渲染(仅预览模式)=====
if (this.previewMode && screenSpaceItems.length > 0) {
this.renderScreenSpacePass(screenSpaceItems);
}
const item = items[itemCount];
item.sortKey = sortKey;
item.addIndex = addIndex;
item.startIndex = startIndex;
item.count = count;
return itemCount + 1;
}
/**
* Collect sprites from matched entities.
* sprites
*/
private collectEntitySprites(entities: readonly Entity[]): void {
private collectEntitySprites(
entities: readonly Entity[],
worldSpaceItems: Array<{ sortKey: number; sprites: SpriteRenderData[] }>
): void {
for (const entity of entities) {
const sprite = entity.getComponent(SpriteComponent);
const transform = entity.getComponent(this.transformType) as unknown as ITransformComponent | null;
if (!sprite || !transform) continue;
let u0 = sprite.uv[0], v0 = sprite.uv[1], u1 = sprite.uv[2], v1 = sprite.uv[3];
if (sprite.flipX) { [u0, u1] = [u1, u0]; }
if (sprite.flipY) { [v0, v1] = [v1, v0]; }
if (!sprite || !transform) {
continue;
}
// Calculate UV with flip | 计算带翻转的 UV
const uv: [number, number, number, number] = [...sprite.uv];
if (sprite.flipX) {
[uv[0], uv[2]] = [uv[2], uv[0]];
}
if (sprite.flipY) {
[uv[1], uv[3]] = [uv[3], uv[1]];
}
// 使用世界变换(由 TransformSystem 计算,考虑父级变换),回退到本地变换
const pos = transform.worldPosition ?? transform.position;
const scl = transform.worldScale ?? transform.scale;
const rot = transform.worldRotation
? transform.worldRotation.z
: (typeof transform.rotation === 'number' ? transform.rotation : transform.rotation.z);
// Convert hex color string to packed RGBA | 将十六进制颜色字符串转换为打包的 RGBA
const color = Color.packHexAlpha(sprite.color, sprite.alpha);
// Get texture ID from sprite component
// 从精灵组件获取纹理 ID
let textureId = 0;
const textureSource = sprite.getTextureSource();
if (textureSource) {
textureId = this.bridge.getOrLoadTextureByPath(this.resolveAssetPath(textureSource));
const texturePath = this.resolveAssetPath(textureSource);
textureId = this.bridge.getOrLoadTextureByPath(texturePath);
}
// Get material ID from GUID
// 从 GUID 获取材质 ID
const materialGuidOrPath = sprite.materialGuid;
const materialPath = materialGuidOrPath ? this.resolveAssetPath(materialGuidOrPath) : materialGuidOrPath;
const materialId = materialPath ? getMaterialManager().getMaterialIdByPath(materialPath) : 0;
const materialPath = materialGuidOrPath
? this.resolveAssetPath(materialGuidOrPath)
: materialGuidOrPath;
const materialId = materialPath
? getMaterialManager().getMaterialIdByPath(materialPath)
: 0;
// Determine if this sprite should render in screen space
// 确定此精灵是否应在屏幕空间渲染
const bScreenSpace = this.previewMode && sortingLayerManager.isScreenSpace(sprite.sortingLayer);
const batcher = bScreenSpace ? this.screenBatcher : this.worldBatcher;
const hasOverrides = sprite.hasOverrides();
const startIndex = batcher.count;
const index = batcher.addSpriteSoA(
pos.x, pos.y, rot,
sprite.width * scl.x, sprite.height * scl.y,
sprite.anchorX, sprite.anchorY,
textureId, u0, v0, u1, v1, color, materialId
);
if (index >= 0 && sprite.hasOverrides()) {
batcher.setMaterialOverrides(index, sprite.materialOverrides!);
}
const renderData: SpriteRenderData = {
x: pos.x,
y: pos.y,
rotation: rot,
scaleX: sprite.width * scl.x,
scaleY: sprite.height * scl.y,
originX: sprite.anchorX,
originY: sprite.anchorY,
textureId,
uv,
color,
materialId,
...(hasOverrides ? { materialOverrides: sprite.materialOverrides } : {})
};
const sortKey = sortingLayerManager.getSortKey(sprite.sortingLayer, sprite.orderInLayer);
if (bScreenSpace) {
this.screenSpaceItemCount = this.addRenderItem(
this.screenSpaceItems, this.screenSpaceItemCount,
sortKey, this.screenSpaceItemCount, startIndex, 1
);
} else {
this.worldSpaceItemCount = this.addRenderItem(
this.worldSpaceItems, this.worldSpaceItemCount,
sortKey, this.worldSpaceItemCount, startIndex, 1
);
}
worldSpaceItems.push({ sortKey, sprites: [renderData] });
this.entityRenderMap.set(entity.id, renderData);
}
}
/**
* Collect render data from providers (tilemap, particle, etc.).
*
*
*/
private collectProviderRenderData(): void {
private collectProviderRenderData(
worldSpaceItems: Array<{ sortKey: number; sprites: SpriteRenderData[] }>,
screenSpaceItems: Array<{ sortKey: number; sprites: SpriteRenderData[] }>
): void {
for (const provider of this.renderDataProviders) {
for (const data of provider.getRenderData()) {
const bScreenSpace = this.previewMode && (data.bScreenSpace ?? sortingLayerManager.isScreenSpace(data.sortingLayer));
const batcher = bScreenSpace ? this.screenBatcher : this.worldBatcher;
const renderDataList = provider.getRenderData();
for (const data of renderDataList) {
// Determine render space: explicit flag > layer config
// 确定渲染空间:显式标志 > 层配置
const bScreenSpace = data.bScreenSpace ?? sortingLayerManager.isScreenSpace(data.sortingLayer);
// Get texture ID - load from GUID if needed
// 获取纹理 ID - 如果需要从 GUID 加载
let textureId = data.textureIds[0] || 0;
if (textureId === 0 && data.textureGuid) {
textureId = this.bridge.getOrLoadTextureByPath(this.resolveAssetPath(data.textureGuid));
const resolvedPath = this.resolveAssetPath(data.textureGuid);
textureId = this.bridge.getOrLoadTextureByPath(resolvedPath);
}
const startIndex = batcher.count;
const sortKey = sortingLayerManager.getSortKey(data.sortingLayer, data.orderInLayer);
// Convert render data to sprites
// 转换渲染数据为 sprites
const sprites: SpriteRenderData[] = [];
for (let i = 0; i < data.tileCount; i++) {
const tOffset = i * 7;
const uvOffset = i * 4;
batcher.addSpriteSoA(
data.transforms[tOffset], data.transforms[tOffset + 1], data.transforms[tOffset + 2],
data.transforms[tOffset + 3], data.transforms[tOffset + 4],
data.transforms[tOffset + 5], data.transforms[tOffset + 6],
const renderData: SpriteRenderData = {
x: data.transforms[tOffset],
y: data.transforms[tOffset + 1],
rotation: data.transforms[tOffset + 2],
scaleX: data.transforms[tOffset + 3],
scaleY: data.transforms[tOffset + 4],
originX: data.transforms[tOffset + 5],
originY: data.transforms[tOffset + 6],
textureId,
data.uvs[uvOffset], data.uvs[uvOffset + 1], data.uvs[uvOffset + 2], data.uvs[uvOffset + 3],
data.colors[i], data.materialIds?.[i] ?? 0
);
uv: [data.uvs[uvOffset], data.uvs[uvOffset + 1], data.uvs[uvOffset + 2], data.uvs[uvOffset + 3]],
color: data.colors[i]
};
sprites.push(renderData);
}
if (bScreenSpace) {
this.screenSpaceItemCount = this.addRenderItem(
this.screenSpaceItems, this.screenSpaceItemCount,
sortKey, this.screenSpaceItemCount, startIndex, data.tileCount
);
} else {
this.worldSpaceItemCount = this.addRenderItem(
this.worldSpaceItems, this.worldSpaceItemCount,
sortKey, this.worldSpaceItemCount, startIndex, data.tileCount
);
if (sprites.length > 0) {
const sortKey = sortingLayerManager.getSortKey(data.sortingLayer, data.orderInLayer);
// Route to appropriate render space
// 路由到适当的渲染空间
if (this.previewMode && bScreenSpace) {
screenSpaceItems.push({ sortKey, sprites });
} else {
worldSpaceItems.push({ sortKey, sprites });
}
}
}
}
}
/**
* Collect UI render data.
* UI
*/
private collectUIRenderData(): void {
if (!this.uiRenderDataProvider) return;
const uiRenderData = this.uiRenderDataProvider.getRenderData();
if ((globalThis as any).__UI_RENDER_DEBUG__) {
console.log('[EngineRenderSystem] UI render batches:', uiRenderData.map((data, i) => ({
index: i, orderInLayer: data.orderInLayer, sortingLayer: data.sortingLayer,
tileCount: data.tileCount,
sortKey: sortingLayerManager.getSortKey(data.sortingLayer, data.orderInLayer),
textureIds: Array.from(data.textureIds).slice(0, 3), textureGuid: data.textureGuid
})));
}
for (const data of uiRenderData) {
if (data.tileCount === 0) continue;
// UI goes to screen space in preview mode, world space in editor mode
// UI 在预览模式下进入屏幕空间,在编辑器模式下进入世界空间
const bScreenSpace = this.previewMode;
const batcher = bScreenSpace ? this.screenBatcher : this.worldBatcher;
let textureId = data.textureIds[0] || 0;
if (textureId === 0 && data.textureGuid) {
textureId = this.bridge.getOrLoadTextureByPath(this.resolveAssetPath(data.textureGuid));
}
const startIndex = batcher.count;
const sortKey = sortingLayerManager.getSortKey(data.sortingLayer, data.orderInLayer);
for (let i = 0; i < data.tileCount; i++) {
const tOffset = i * 7;
const uvOffset = i * 4;
const index = batcher.addSpriteSoA(
data.transforms[tOffset], data.transforms[tOffset + 1], data.transforms[tOffset + 2],
data.transforms[tOffset + 3], data.transforms[tOffset + 4],
data.transforms[tOffset + 5], data.transforms[tOffset + 6],
textureId,
data.uvs[uvOffset], data.uvs[uvOffset + 1], data.uvs[uvOffset + 2], data.uvs[uvOffset + 3],
data.colors[i], data.materialIds?.[i] ?? 0
);
if (index >= 0) {
if (data.clipRect) batcher.setClipRect(index, data.clipRect);
if (data.materialOverrides) batcher.setMaterialOverrides(index, data.materialOverrides);
}
}
if (bScreenSpace) {
this.screenSpaceItemCount = this.addRenderItem(
this.screenSpaceItems, this.screenSpaceItemCount,
sortKey, this.screenSpaceItemCount, startIndex, data.tileCount
);
} else {
this.worldSpaceItemCount = this.addRenderItem(
this.worldSpaceItems, this.worldSpaceItemCount,
sortKey, this.worldSpaceItemCount, startIndex, data.tileCount
);
}
}
// Collect mesh render data for complex shapes (ellipses, polygons, etc.)
// 收集复杂形状(椭圆、多边形等)的网格渲染数据
if (this.uiRenderDataProvider.getMeshRenderData) {
const meshRenderData = this.uiRenderDataProvider.getMeshRenderData();
if (meshRenderData.length > 0) {
console.log(`[EngineRenderSystem] Submitting ${meshRenderData.length} mesh batches`);
}
for (const mesh of meshRenderData) {
if (mesh.positions.length === 0) continue;
console.log('[EngineRenderSystem] Mesh batch:', {
vertices: mesh.positions.length / 2,
indices: mesh.indices.length,
textureId: mesh.textureId
});
// Submit mesh data directly to the engine
// 直接将网格数据提交到引擎
this.bridge.submitMeshBatch(
mesh.positions,
mesh.uvs,
mesh.colors,
mesh.indices,
mesh.textureId
);
}
}
}
/**
* Render world space content.
*
*/
private renderWorldSpace(): void {
const items = this.worldSpaceItems;
const count = this.worldSpaceItemCount;
private renderWorldSpacePass(
worldSpaceItems: Array<{ sortKey: number; sprites: SpriteRenderData[] }>
): void {
// Sort by sortKey (lower values render first, appear behind)
// 按 sortKey 排序(值越小越先渲染,显示在后面)
worldSpaceItems.sort((a, b) => a.sortKey - b.sortKey);
// Sort by sortKey, use addIndex for stable sorting
if (count > 1) {
for (let i = 0; i < count - 1; i++) {
for (let j = i + 1; j < count; j++) {
const a = items[i], b = items[j];
const diff = a.sortKey - b.sortKey;
if (diff > 0 || (diff === 0 && a.addIndex > b.addIndex)) {
items[i] = b;
items[j] = a;
}
}
// Submit all sprites in sorted order
// 按排序顺序提交所有 sprites
for (const item of worldSpaceItems) {
for (const sprite of item.sprites) {
this.batcher.addSprite(sprite);
}
}
if (!this.worldBatcher.isEmpty) {
const buffers = this.worldBatcher.getBuffers();
this.bridge.submitSprites(
buffers.transforms, buffers.textureIds, buffers.uvs,
buffers.colors, buffers.materialIds, buffers.count
);
if (!this.batcher.isEmpty) {
const sprites = this.batcher.getSprites();
this.bridge.submitSprites(sprites);
}
// Draw gizmos
// 绘制 Gizmo
if (this.showGizmos) {
this.drawComponentGizmos();
}
if (this.showGizmos && this.selectedEntityIds.size > 0) {
this.drawSelectedEntityGizmos();
}
if (this.showGizmos) {
this.drawComponentGizmos();
if (this.selectedEntityIds.size > 0) this.drawSelectedEntityGizmos();
this.drawCameraFrustums();
if (this.showUICanvasBoundary && this.uiCanvasWidth > 0 && this.uiCanvasHeight > 0) {
this.drawUICanvasBoundary();
}
}
if (this.showGizmos && this.showUICanvasBoundary && this.uiCanvasWidth > 0 && this.uiCanvasHeight > 0) {
this.drawUICanvasBoundary();
}
// Render world content
// 渲染世界内容
this.bridge.render();
}
/**
* Render screen space content (UI overlay).
* UI
* Render screen space content (UI, ScreenOverlay, Modal).
* UI
*/
private renderScreenSpace(): void {
const items = this.screenSpaceItems;
const count = this.screenSpaceItemCount;
private renderScreenSpacePass(
screenSpaceItems: Array<{ sortKey: number; sprites: SpriteRenderData[] }>
): void {
// Sort by sortKey
// 按 sortKey 排序
screenSpaceItems.sort((a, b) => a.sortKey - b.sortKey);
if (count > 1) {
for (let i = 0; i < count - 1; i++) {
for (let j = i + 1; j < count; j++) {
const a = items[i], b = items[j];
const diff = a.sortKey - b.sortKey;
if (diff > 0 || (diff === 0 && a.addIndex > b.addIndex)) {
items[i] = b;
items[j] = a;
}
}
// Switch to screen space projection
// 切换到屏幕空间投影
const canvasWidth = this.uiCanvasWidth > 0 ? this.uiCanvasWidth : 1920;
const canvasHeight = this.uiCanvasHeight > 0 ? this.uiCanvasHeight : 1080;
this.bridge.pushScreenSpaceMode(canvasWidth, canvasHeight);
// Clear batcher for screen space content
// 清空批处理器用于屏幕空间内容
this.batcher.clear();
// Submit screen space sprites
// 提交屏幕空间 sprites
for (const item of screenSpaceItems) {
for (const sprite of item.sprites) {
this.batcher.addSprite(sprite);
}
}
const canvasWidth = this.uiCanvasWidth > 0 ? this.uiCanvasWidth : 1920;
const canvasHeight = this.uiCanvasHeight > 0 ? this.uiCanvasHeight : 1080;
this.bridge.pushScreenSpaceMode(canvasWidth, canvasHeight);
if (!this.screenBatcher.isEmpty) {
const buffers = this.screenBatcher.getBuffers();
this.bridge.submitSprites(
buffers.transforms, buffers.textureIds, buffers.uvs,
buffers.colors, buffers.materialIds, buffers.count
);
if (!this.batcher.isEmpty) {
const sprites = this.batcher.getSprites();
this.bridge.submitSprites(sprites);
// Render overlay (without clearing screen)
// 渲染叠加层(不清屏)
this.bridge.renderOverlay();
}
this.bridge.clearScissorRect();
// Restore world space camera
// 恢复世界空间相机
this.bridge.popScreenSpaceMode();
}
/**
* Convert provider render data to sprite render data array.
* Sprite
*/
private convertProviderDataToSprites(data: ProviderRenderData): SpriteRenderData[] {
// Get texture ID - load from GUID if needed
// 获取纹理 ID - 如果需要从 GUID 加载
let textureId = data.textureIds[0] || 0;
if (textureId === 0 && data.textureGuid) {
textureId = this.bridge.getOrLoadTextureByPath(this.resolveAssetPath(data.textureGuid));
}
const sprites: SpriteRenderData[] = [];
for (let i = 0; i < data.tileCount; i++) {
const tOffset = i * 7;
const uvOffset = i * 4;
const uv: [number, number, number, number] = [
data.uvs[uvOffset],
data.uvs[uvOffset + 1],
data.uvs[uvOffset + 2],
data.uvs[uvOffset + 3]
];
const renderData: SpriteRenderData = {
x: data.transforms[tOffset],
y: data.transforms[tOffset + 1],
rotation: data.transforms[tOffset + 2],
scaleX: data.transforms[tOffset + 3],
scaleY: data.transforms[tOffset + 4],
originX: data.transforms[tOffset + 5],
originY: data.transforms[tOffset + 6],
textureId,
uv,
color: data.colors[i]
};
sprites.push(renderData);
}
return sprites;
}
/**
* Draw gizmos from components that have registered gizmo providers.
* gizmo gizmo
@@ -674,15 +601,10 @@ export class EngineRenderSystem extends EntitySystem {
const scene = Core.scene;
if (!scene || !this.gizmoDataProvider || !this.hasGizmoProvider) return;
// Get hovered entity ID for highlight
// 获取悬停的实体 ID 用于高亮
const hoveredEntityId = this.getHoveredEntityIdFn?.() ?? null;
// Iterate all entities in the scene
// 遍历场景中的所有实体
for (const entity of scene.entities.buffer) {
const isSelected = this.selectedEntityIds.has(entity.id);
const isHovered = entity.id === hoveredEntityId;
// Check each component for gizmo provider
// 检查每个组件是否有 gizmo 提供者
@@ -691,15 +613,6 @@ export class EngineRenderSystem extends EntitySystem {
try {
const gizmoDataArray = this.gizmoDataProvider(component, entity, isSelected);
for (const gizmoData of gizmoDataArray) {
// Apply hover highlight color if applicable
// 如果适用,应用悬停高亮颜色
if (isHovered && this.gizmoHighlightColorFn) {
gizmoData.color = this.gizmoHighlightColorFn(
entity.id,
gizmoData.color,
isSelected
);
}
this.renderGizmoData(gizmoData);
}
} catch (e) {
@@ -1124,26 +1037,6 @@ export class EngineRenderSystem extends EntitySystem {
this.hasGizmoProvider = hasProvider;
}
/**
* Set gizmo interaction functions.
* gizmo
*
* This allows the editor layer to inject GizmoInteractionService functionality
* for hover highlighting and click selection.
* GizmoInteractionService
*
*
* @param highlightColorFn - Function to get highlight color for gizmo
* @param getHoveredEntityIdFn - Function to get currently hovered entity ID
*/
setGizmoInteraction(
highlightColorFn: GizmoHighlightColorFn,
getHoveredEntityIdFn: GetHoveredEntityIdFn
): void {
this.gizmoHighlightColorFn = highlightColorFn;
this.getHoveredEntityIdFn = getHoveredEntityIdFn;
}
/**
* Set gizmo visibility.
* Gizmo可见性
@@ -1308,7 +1201,7 @@ export class EngineRenderSystem extends EntitySystem {
*
*/
get spriteCount(): number {
return this.worldBatcher.count + this.screenBatcher.count;
return this.batcher.count;
}
/**
+3 -22
View File
@@ -4,31 +4,12 @@
*/
import { createServiceToken } from '@esengine/ecs-framework';
import {
TextureServiceToken,
DynamicAtlasServiceToken,
CoordinateServiceToken,
RenderConfigServiceToken,
type ITextureService,
type IDynamicAtlasService,
type ICoordinateService,
type IRenderConfigService
} from '@esengine/engine-core';
import { EngineBridgeToken as CoreEngineBridgeToken, type IEngineBridge as CoreIEngineBridge } from '@esengine/engine-core';
import type { IRenderDataProvider as InternalIRenderDataProvider } from './systems/EngineRenderSystem';
// 从 engine-core 重新导出 | Re-export from engine-core
export {
TextureServiceToken,
DynamicAtlasServiceToken,
CoordinateServiceToken,
RenderConfigServiceToken
};
export type {
ITextureService,
IDynamicAtlasService,
ICoordinateService,
IRenderConfigService
};
export { CoreEngineBridgeToken as EngineBridgeToken };
export type { CoreIEngineBridge as IEngineBridge };
export type IRenderDataProvider = InternalIRenderDataProvider;
+2 -57
View File
@@ -52,13 +52,6 @@ export interface SpriteRenderData {
*
*/
materialOverrides?: MaterialOverrides;
/**
* Clip rectangle for scissor test (screen coordinates).
* Content outside this rect will be clipped.
* scissor test
*
*/
clipRect?: { x: number; y: number; width: number; height: number };
}
/**
@@ -88,8 +81,8 @@ export interface EngineStats {
}
/**
* Camera configuration (2D).
* 2D
* Camera configuration.
*
*/
export interface CameraConfig {
/** Camera X position. | 相机X位置。 */
@@ -101,51 +94,3 @@ export interface CameraConfig {
/** Rotation in radians. | 旋转角度(弧度)。 */
rotation: number;
}
/**
* 3D Camera position.
* 3D
*/
export interface Camera3DPosition {
/** X coordinate. | X 坐标。 */
x: number;
/** Y coordinate. | Y 坐标。 */
y: number;
/** Z coordinate. | Z 坐标。 */
z: number;
}
/**
* 3D Camera rotation (Euler angles in degrees).
* 3D
*/
export interface Camera3DRotation {
/** Pitch angle in degrees. | 俯仰角(度)。 */
pitch: number;
/** Yaw angle in degrees. | 偏航角(度)。 */
yaw: number;
/** Roll angle in degrees. | 滚转角(度)。 */
roll: number;
}
/**
* Projection type for 3D camera.
* 3D
*/
export enum ProjectionType {
/** Perspective projection. | 透视投影。 */
Perspective = 0,
/** Orthographic projection. | 正交投影。 */
Orthographic = 1,
}
/**
* Render mode.
*
*/
export enum RenderMode {
/** 2D rendering mode. | 2D 渲染模式。 */
Mode2D = 0,
/** 3D rendering mode. | 3D 渲染模式。 */
Mode3D = 1,
}
+2 -264
View File
@@ -5,22 +5,6 @@
* panic hook以在控制台显示更好的错误信息
*/
export function init(): void;
/**
* Render mode enumeration.
*
*/
export enum RenderMode {
/**
* 2D rendering mode (orthographic camera, no depth test).
* 2D渲染模式
*/
Mode2D = 0,
/**
* 3D rendering mode (perspective/orthographic camera, depth test enabled).
* 3D渲染模式/
*/
Mode3D = 1,
}
/**
* Game engine main interface exposed to JavaScript.
* JavaScript的游戏引擎主接口
@@ -159,37 +143,11 @@ export class GameEngine {
* The material ID for referencing this material | ID
*/
createMaterial(name: string, shader_id: number, blend_mode: number): number;
/**
* Get current render mode.
*
*
* Returns 0 for 2D mode, 1 for 3D mode.
* 0 2D 1 3D
*/
getRenderMode(): number;
/**
* Check if 3D renderer is initialized.
* 3D
*/
has3DRenderer(): boolean;
/**
* Remove a material.
*
*/
removeMaterial(material_id: number): boolean;
/**
* Render a 3D gizmo at the specified world position.
* 3D Gizmo
*
* Only works in 3D render mode. The gizmo will be rendered
* with the current transform mode (move/rotate/scale).
* 3D Gizmo 使
*
* # Arguments |
* * `x`, `y`, `z` - World position |
* * `scale` - Gizmo scale multiplier | Gizmo
*/
render3DGizmo(x: number, y: number, z: number, scale: number): void;
/**
* Resize a specific viewport.
*
@@ -224,18 +182,6 @@ export class GameEngine {
* false UIgizmos
*/
setEditorMode(is_editor: boolean): void;
/**
* Set render mode.
*
*
* # Arguments |
* * `mode` - 0 for 2D, 1 for 3D | 0 2D1 3D
*
* When switching to 3D mode for the first time, the 3D renderer
* will be lazily initialized.
* 3D 3D
*/
setRenderMode(mode: number): void;
/**
* Set gizmo visibility.
*
@@ -258,11 +204,6 @@ export class GameEngine {
* Gizmo边框
*/
addGizmoCircle(x: number, y: number, radius: number, r: number, g: number, b: number, a: number): void;
/**
* Get the graphics backend name (e.g., "WebGL2").
* "WebGL2"
*/
getBackendName(): string;
/**
* Get all registered viewport IDs.
* ID
@@ -276,35 +217,11 @@ export class GameEngine {
* * `id` - Texture ID | ID
*/
isTextureReady(id: number): boolean;
/**
* Set scissor rect for clipping (screen coordinates, Y-down).
* Y
*
* Content outside this rect will be clipped.
*
*
* # Arguments |
* * `x` - Left edge in screen coordinates |
* * `y` - Top edge in screen coordinates (Y-down) | Y
* * `width` - Rect width |
* * `height` - Rect height |
*/
setScissorRect(x: number, y: number, width: number, height: number): void;
/**
* Add a capsule gizmo outline.
* Gizmo边框
*/
addGizmoCapsule(x: number, y: number, radius: number, half_height: number, rotation: number, r: number, g: number, b: number, a: number): void;
/**
* Make 3D camera look at a target position.
* 使 3D
*/
camera3DLookAt(target_x: number, target_y: number, target_z: number): void;
/**
* Get 3D camera field of view (in degrees).
* 3D
*/
getCamera3DFov(): number | undefined;
/**
*
* Get texture loading state
@@ -326,14 +243,6 @@ export class GameEngine {
* * `canvas_id` - HTML canvas element ID | HTML canvas元素ID
*/
registerViewport(id: string, canvas_id: string): void;
/**
* Set 3D camera field of view (in degrees).
* 3D
*
* Only affects perspective projection mode.
*
*/
setCamera3DFov(fov_degrees: number): void;
/**
* Set a material's vec2 uniform.
* vec2 uniform
@@ -349,35 +258,6 @@ export class GameEngine {
* vec4 uniform
*/
setMaterialVec4(material_id: number, name: string, x: number, y: number, z: number, w: number): boolean;
/**
* Submit mesh batch for rendering arbitrary 2D geometry.
* 2D
*
* Used for rendering ellipses, polygons, and other complex shapes.
*
*
* # Arguments |
* * `positions` - Float32Array [x, y, ...] for each vertex
* * `uvs` - Float32Array [u, v, ...] for each vertex
* * `colors` - Uint32Array of packed RGBA colors (one per vertex)
* * `indices` - Uint16Array of triangle indices
* * `texture_id` - Texture ID to use (0 for white pixel)
*/
submitMeshBatch(positions: Float32Array, uvs: Float32Array, colors: Uint32Array, indices: Uint16Array, texture_id: number): void;
/**
* Submit MSDF text batch for rendering.
* MSDF
*
* # Arguments |
* * `positions` - Float32Array [x, y, ...] for each vertex (4 per glyph)
* * `tex_coords` - Float32Array [u, v, ...] for each vertex
* * `colors` - Float32Array [r, g, b, a, ...] for each vertex
* * `outline_colors` - Float32Array [r, g, b, a, ...] for each vertex
* * `outline_widths` - Float32Array [width, ...] for each vertex
* * `texture_id` - Font atlas texture ID
* * `px_range` - Pixel range for MSDF shader
*/
submitTextBatch(positions: Float32Array, tex_coords: Float32Array, colors: Float32Array, outline_colors: Float32Array, outline_widths: Float32Array, texture_id: number, px_range: number): void;
/**
* Clear all textures and reset state.
*
@@ -389,11 +269,6 @@ export class GameEngine {
* 使
*/
clearAllTextures(): void;
/**
* Clear scissor rect (disable clipping).
*
*/
clearScissorRect(): void;
/**
* Render to a specific viewport.
*
@@ -417,11 +292,6 @@ export class GameEngine {
* * `mode` - 0=Select, 1=Move, 2=Rotate, 3=Scale
*/
setTransformMode(mode: number): void;
/**
* Get the graphics backend version string.
*
*/
getBackendVersion(): string;
/**
* Get or load texture by path.
*
@@ -468,28 +338,6 @@ export class GameEngine {
*
*/
unregisterViewport(id: string): void;
/**
* Create a blank texture for dynamic atlas.
*
*
* This creates a texture that can be filled later using `updateTextureRegion`.
* Used for runtime atlas generation to batch UI elements with different textures.
* 使 `updateTextureRegion`
* 使 UI
*
* # Arguments |
* * `width` - Texture width in pixels (recommended: 2048) | 2048
* * `height` - Texture height in pixels (recommended: 2048) | 2048
*
* # Returns |
* The texture ID for the created blank texture | ID
*/
createBlankTexture(width: number, height: number): number;
/**
* Get maximum texture size supported by the backend.
*
*/
getMaxTextureSize(): number;
/**
* Load texture by path, returning texture ID.
* ID
@@ -498,43 +346,11 @@ export class GameEngine {
* * `path` - Image path/URL to load | /URL
*/
loadTextureByPath(path: string): number;
/**
* Update a region of an existing texture with pixel data.
* 使
*
* This is used for dynamic atlas to copy individual textures into the atlas.
*
*
* # Arguments |
* * `id` - The texture ID to update | ID
* * `x` - X offset in the texture | X偏移
* * `y` - Y offset in the texture | Y偏移
* * `width` - Width of the region to update |
* * `height` - Height of the region to update |
* * `pixels` - RGBA pixel data (Uint8Array, 4 bytes per pixel) | RGBA像素数据4
*/
updateTextureRegion(id: number, x: number, y: number, width: number, height: number, pixels: Uint8Array): void;
/**
* Compile a shader with a specific ID.
* 使ID编译着色器
*/
compileShaderWithId(shader_id: number, vertex_source: string, fragment_source: string): void;
/**
* Get 3D camera position.
* 3D
*
* Returns [x, y, z] or null if 3D renderer is not initialized.
* [x, y, z] 3D null
*/
getCamera3DPosition(): Float32Array | undefined;
/**
* Get 3D camera rotation as Euler angles (in degrees).
* 3D
*
* Returns [pitch, yaw, roll] or null if 3D renderer is not initialized.
* [pitch, yaw, roll] 3D null
*/
getCamera3DRotation(): Float32Array | undefined;
/**
* Get texture ID by path.
* ID
@@ -543,21 +359,6 @@ export class GameEngine {
* * `path` - Image path to lookup |
*/
getTextureIdByPath(path: string): number | undefined;
/**
* Set 3D camera position.
* 3D
*/
setCamera3DPosition(x: number, y: number, z: number): void;
/**
* Set 3D camera rotation using Euler angles (in degrees).
* 使 3D
*
* # Arguments |
* * `pitch` - Rotation around X axis (degrees) | X
* * `yaw` - Rotation around Y axis (degrees) | Y
* * `roll` - Rotation around Z axis (degrees) | Z
*/
setCamera3DRotation(pitch: number, yaw: number, roll: number): void;
/**
* Create a material with a specific ID.
* 使ID创建材质
@@ -580,36 +381,11 @@ export class GameEngine {
* 使ID重新加载
*/
clearTexturePathCache(): void;
/**
* Get texture size by path.
*
*
* Returns an array [width, height] or null if not found.
* [width, height] null
*
* # Arguments |
* * `path` - Image path to lookup |
*/
getTextureSizeByPath(path: string): Float32Array | undefined;
/**
* Set 3D camera projection type.
* 3D
*
* # Arguments |
* * `projection_type` - 0 for Perspective, 1 for Orthographic
* * `ortho_size` - Half-height of orthographic view (only used when projection_type = 1)
*/
setCamera3DProjection(projection_type: number, ortho_size: number): void;
/**
*
* Get the number of textures currently loading
*/
getTextureLoadingCount(): number;
/**
* Set 3D camera near and far clip planes.
* 3D
*/
setCamera3DClipPlanes(near: number, far: number): void;
/**
* Create a new game engine instance.
*
@@ -646,19 +422,6 @@ export class GameEngine {
* * `height` - New viewport height |
*/
resize(width: number, height: number): void;
/**
* Render 3D content.
* 3D
*
* Should be called after submitting 3D meshes.
* 3D
*/
render3D(): void;
/**
* Resize 3D renderer viewport.
* 3D
*/
resize3D(width: number, height: number): void;
/**
* Get canvas width.
*
@@ -680,33 +443,21 @@ export interface InitOutput {
readonly gameengine_addGizmoCircle: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number) => void;
readonly gameengine_addGizmoLine: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number) => void;
readonly gameengine_addGizmoRect: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number, j: number, k: number, l: number, m: number) => void;
readonly gameengine_camera3DLookAt: (a: number, b: number, c: number, d: number) => void;
readonly gameengine_clear: (a: number, b: number, c: number, d: number, e: number) => void;
readonly gameengine_clearAllTextures: (a: number) => void;
readonly gameengine_clearScissorRect: (a: number) => void;
readonly gameengine_clearTexturePathCache: (a: number) => void;
readonly gameengine_compileShader: (a: number, b: number, c: number, d: number, e: number) => [number, number, number];
readonly gameengine_compileShaderWithId: (a: number, b: number, c: number, d: number, e: number, f: number) => [number, number];
readonly gameengine_createBlankTexture: (a: number, b: number, c: number) => [number, number, number];
readonly gameengine_createMaterial: (a: number, b: number, c: number, d: number, e: number) => number;
readonly gameengine_createMaterialWithId: (a: number, b: number, c: number, d: number, e: number, f: number) => void;
readonly gameengine_fromExternal: (a: any, b: number, c: number) => [number, number, number];
readonly gameengine_getBackendName: (a: number) => [number, number];
readonly gameengine_getBackendVersion: (a: number) => [number, number];
readonly gameengine_getCamera: (a: number) => [number, number];
readonly gameengine_getCamera3DFov: (a: number) => number;
readonly gameengine_getCamera3DPosition: (a: number) => [number, number];
readonly gameengine_getCamera3DRotation: (a: number) => [number, number];
readonly gameengine_getMaxTextureSize: (a: number) => number;
readonly gameengine_getOrLoadTextureByPath: (a: number, b: number, c: number) => [number, number, number];
readonly gameengine_getRenderMode: (a: number) => number;
readonly gameengine_getTextureIdByPath: (a: number, b: number, c: number) => number;
readonly gameengine_getTextureLoadingCount: (a: number) => number;
readonly gameengine_getTextureSizeByPath: (a: number, b: number, c: number) => any;
readonly gameengine_getTextureState: (a: number, b: number) => [number, number];
readonly gameengine_getViewportCamera: (a: number, b: number, c: number) => [number, number];
readonly gameengine_getViewportIds: (a: number) => [number, number];
readonly gameengine_has3DRenderer: (a: number) => number;
readonly gameengine_hasMaterial: (a: number, b: number) => number;
readonly gameengine_hasShader: (a: number, b: number) => number;
readonly gameengine_height: (a: number) => number;
@@ -720,21 +471,13 @@ export interface InitOutput {
readonly gameengine_removeMaterial: (a: number, b: number) => number;
readonly gameengine_removeShader: (a: number, b: number) => number;
readonly gameengine_render: (a: number) => [number, number];
readonly gameengine_render3D: (a: number) => [number, number];
readonly gameengine_render3DGizmo: (a: number, b: number, c: number, d: number, e: number) => void;
readonly gameengine_renderOverlay: (a: number) => [number, number];
readonly gameengine_renderToViewport: (a: number, b: number, c: number) => [number, number];
readonly gameengine_resize: (a: number, b: number, c: number) => void;
readonly gameengine_resize3D: (a: number, b: number, c: number) => void;
readonly gameengine_resizeViewport: (a: number, b: number, c: number, d: number, e: number) => void;
readonly gameengine_screenToWorld: (a: number, b: number, c: number) => [number, number];
readonly gameengine_setActiveViewport: (a: number, b: number, c: number) => number;
readonly gameengine_setCamera: (a: number, b: number, c: number, d: number, e: number) => void;
readonly gameengine_setCamera3DClipPlanes: (a: number, b: number, c: number) => void;
readonly gameengine_setCamera3DFov: (a: number, b: number) => void;
readonly gameengine_setCamera3DPosition: (a: number, b: number, c: number, d: number) => void;
readonly gameengine_setCamera3DProjection: (a: number, b: number, c: number) => void;
readonly gameengine_setCamera3DRotation: (a: number, b: number, c: number, d: number) => void;
readonly gameengine_setClearColor: (a: number, b: number, c: number, d: number, e: number) => void;
readonly gameengine_setEditorMode: (a: number, b: number) => void;
readonly gameengine_setMaterialBlendMode: (a: number, b: number, c: number) => number;
@@ -743,24 +486,19 @@ export interface InitOutput {
readonly gameengine_setMaterialVec2: (a: number, b: number, c: number, d: number, e: number, f: number) => number;
readonly gameengine_setMaterialVec3: (a: number, b: number, c: number, d: number, e: number, f: number, g: number) => number;
readonly gameengine_setMaterialVec4: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number) => number;
readonly gameengine_setRenderMode: (a: number, b: number) => [number, number];
readonly gameengine_setScissorRect: (a: number, b: number, c: number, d: number, e: number) => void;
readonly gameengine_setShowGizmos: (a: number, b: number) => void;
readonly gameengine_setShowGrid: (a: number, b: number) => void;
readonly gameengine_setTransformMode: (a: number, b: number) => void;
readonly gameengine_setViewportCamera: (a: number, b: number, c: number, d: number, e: number, f: number, g: number) => void;
readonly gameengine_setViewportConfig: (a: number, b: number, c: number, d: number, e: number) => void;
readonly gameengine_submitMeshBatch: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number, j: number) => [number, number];
readonly gameengine_submitSpriteBatch: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number, j: number, k: number) => [number, number];
readonly gameengine_submitTextBatch: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number, j: number, k: number, l: number, m: number) => [number, number];
readonly gameengine_unregisterViewport: (a: number, b: number, c: number) => void;
readonly gameengine_updateInput: (a: number) => void;
readonly gameengine_updateTextureRegion: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number) => [number, number];
readonly gameengine_width: (a: number) => number;
readonly gameengine_worldToScreen: (a: number, b: number, c: number) => [number, number];
readonly init: () => void;
readonly wasm_bindgen__convert__closures_____invoke__h256074d77f4ce876: (a: number, b: number) => void;
readonly wasm_bindgen__closure__destroy__h14ac3db10f717d1a: (a: number, b: number) => void;
readonly wasm_bindgen__convert__closures_____invoke__hc746ced83e8f2609: (a: number, b: number) => void;
readonly wasm_bindgen__closure__destroy__hebcd2828f83f27ed: (a: number, b: number) => void;
readonly __wbindgen_malloc: (a: number, b: number) => number;
readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
readonly __wbindgen_exn_store: (a: number) => void;
+2 -2
View File
@@ -44,8 +44,8 @@
"@esengine/sprite-editor": "workspace:*",
"@esengine/tilemap": "workspace:*",
"@esengine/tilemap-editor": "workspace:*",
"@esengine/fairygui": "workspace:*",
"@esengine/fairygui-editor": "workspace:*",
"@esengine/ui": "workspace:*",
"@esengine/ui-editor": "workspace:*",
"@monaco-editor/react": "^4.7.0",
"@tauri-apps/api": "^2.2.0",
"@tauri-apps/plugin-cli": "^2.4.1",
@@ -525,11 +525,3 @@ pub async fn read_binary_file_as_base64(path: String) -> Result<String, String>
Ok(STANDARD.encode(&bytes))
}
/// Read binary file and return as raw bytes.
/// 读取二进制文件并返回原始字节。
#[tauri::command]
pub async fn read_binary_file(file_path: String) -> Result<Vec<u8>, String> {
fs::read(&file_path)
.map_err(|e| format!("Failed to read binary file | 读取二进制文件失败: {}", e))
}
@@ -109,7 +109,6 @@ fn main() {
commands::write_json_file,
commands::list_files_by_extension,
commands::read_binary_file_as_base64,
commands::read_binary_file,
// Engine modules | 引擎模块
commands::read_engine_modules_index,
commands::read_module_manifest,
+2 -61
View File
@@ -15,7 +15,7 @@ import {
UIRegistry,
MessageHub,
EntityStoreService,
EditorComponentRegistry,
ComponentRegistry,
LocaleService,
LogService,
SettingsRegistry,
@@ -51,7 +51,6 @@ import { ConfirmDialog } from './components/ConfirmDialog';
import { ExternalModificationDialog } from './components/ExternalModificationDialog';
import { PluginGeneratorWindow } from './components/PluginGeneratorWindow';
import { BuildSettingsWindow } from './components/BuildSettingsWindow';
import { AssetPickerDialog } from './components/dialogs/AssetPickerDialog';
import { ForumPanel } from './components/forum';
import { ToastProvider, useToast } from './components/Toast';
import { TitleBar } from './components/TitleBar';
@@ -163,22 +162,6 @@ function App() {
const [commandManager] = useState(() => new CommandManager());
const { t, locale, changeLocale } = useLocale();
// Play 模式状态(用于层级面板实时同步)
// Play mode state (for hierarchy panel real-time sync)
const [isPlaying, setIsPlaying] = useState(false);
// 监听 Play 状态变化
// Listen for play state changes
useEffect(() => {
if (!messageHubRef.current || !initialized) return;
const unsubscribe = messageHubRef.current.subscribe('viewport:playState:changed', (data: { isPlaying: boolean }) => {
setIsPlaying(data.isPlaying);
});
return () => unsubscribe();
}, [initialized]);
// 初始化 Store 订阅(集中管理 MessageHub 订阅)
// Initialize store subscriptions (centrally manage MessageHub subscriptions)
useStoreSubscriptions({
@@ -186,7 +169,6 @@ function App() {
entityStore: entityStoreRef.current,
sceneManager: sceneManagerRef.current,
enabled: initialized,
isPlaying,
});
// 同步 locale 到 TauriDialogService
@@ -211,13 +193,6 @@ function App() {
externalModificationDialog, setExternalModificationDialog
} = useDialogStore();
// 资产选择器对话框状态 | Asset picker dialog state
const [assetPickerState, setAssetPickerState] = useState<{
isOpen: boolean;
extensions?: string[];
onSelect?: (path: string) => void;
}>({ isOpen: false });
// 全局监听独立调试窗口的数据请求 | Global listener for standalone debug window requests
useEffect(() => {
let broadcastInterval: ReturnType<typeof setInterval> | null = null;
@@ -394,7 +369,7 @@ function App() {
setStatus(t('header.status.remoteConnected'));
} else {
if (projectLoaded) {
const componentRegistry = Core.services.resolve(EditorComponentRegistry);
const componentRegistry = Core.services.resolve(ComponentRegistry);
const componentCount = componentRegistry?.getAllComponents().length || 0;
setStatus(t('header.status.projectOpened') + (componentCount > 0 ? ` (${componentCount} components registered)` : ''));
} else {
@@ -498,26 +473,6 @@ function App() {
return () => unsubscribe?.();
}, [initialized, addDynamicPanel, setActivePanelId]);
// 资产选择器消息订阅 | Asset picker message subscription
useEffect(() => {
if (!initialized || !messageHubRef.current) return;
const hub = messageHubRef.current;
const unsubscribe = hub.subscribe('asset:pick', (data: {
extensions?: string[];
onSelect?: (path: string) => void;
}) => {
logger.info('Opening asset picker dialog with extensions:', data.extensions);
setAssetPickerState({
isOpen: true,
extensions: data.extensions,
onSelect: data.onSelect
});
});
return () => unsubscribe?.();
}, [initialized]);
useEffect(() => {
if (!initialized || !messageHubRef.current) return;
const hub = messageHubRef.current;
@@ -1455,20 +1410,6 @@ function App() {
/>
)}
{/* 资产选择器对话框 | Asset Picker Dialog */}
<AssetPickerDialog
isOpen={assetPickerState.isOpen}
onClose={() => setAssetPickerState({ isOpen: false })}
onSelect={(path) => {
if (assetPickerState.onSelect) {
assetPickerState.onSelect(path);
}
setAssetPickerState({ isOpen: false });
}}
title={t('asset.selectAsset')}
fileExtensions={assetPickerState.extensions}
/>
{/* 渲染调试面板 | Render Debug Panel */}
<RenderDebugPanel
visible={showRenderDebug}
@@ -22,7 +22,7 @@ import { BehaviorTreePlugin } from '@esengine/behavior-tree-editor';
import { ParticlePlugin } from '@esengine/particle-editor';
import { Physics2DPlugin } from '@esengine/physics-rapier2d-editor';
import { TilemapPlugin } from '@esengine/tilemap-editor';
import { FGUIPlugin } from '@esengine/fairygui-editor';
import { UIPlugin } from '@esengine/ui-editor';
import { BlueprintPlugin } from '@esengine/blueprint-editor';
import { MaterialPlugin } from '@esengine/material-editor';
import { SpritePlugin } from '@esengine/sprite-editor';
@@ -63,7 +63,7 @@ export class PluginInstaller {
{ name: 'CameraPlugin', plugin: CameraPlugin },
{ name: 'SpritePlugin', plugin: SpritePlugin },
{ name: 'TilemapPlugin', plugin: TilemapPlugin },
{ name: 'FGUIPlugin', plugin: FGUIPlugin },
{ name: 'UIPlugin', plugin: UIPlugin },
{ name: 'BehaviorTreePlugin', plugin: BehaviorTreePlugin },
{ name: 'ParticlePlugin', plugin: ParticlePlugin },
{ name: 'Physics2DPlugin', plugin: Physics2DPlugin },
@@ -7,7 +7,7 @@ import {
IMessageHub,
SerializerRegistry,
EntityStoreService,
EditorComponentRegistry,
ComponentRegistry,
ProjectService,
ComponentDiscoveryService,
PropertyMetadataService,
@@ -48,7 +48,7 @@ import { TransformComponent } from '@esengine/engine-core';
import { SpriteComponent, SpriteAnimatorComponent } from '@esengine/sprite';
import { CameraComponent } from '@esengine/camera';
import { AudioSourceComponent } from '@esengine/audio';
import { FGUIComponent } from '@esengine/fairygui';
import { UITextComponent } from '@esengine/ui';
import { BehaviorTreeRuntimeComponent } from '@esengine/behavior-tree';
import { TauriFileAPI } from '../../adapters/TauriFileAPI';
import { DIContainer } from '../../core/di/DIContainer';
@@ -77,8 +77,7 @@ import {
Vector3FieldEditor,
Vector4FieldEditor,
ColorFieldEditor,
AnimationClipsFieldEditor,
EntityRefFieldEditor
AnimationClipsFieldEditor
} from '../../infrastructure/field-editors';
import { TransformComponentInspector } from '../../components/inspectors/component-inspectors/TransformComponentInspector';
import { buildFileSystem } from '../../services/BuildFileSystemService';
@@ -90,7 +89,7 @@ export interface EditorServices {
messageHub: MessageHub;
serializerRegistry: SerializerRegistry;
entityStore: EntityStoreService;
componentRegistry: EditorComponentRegistry;
componentRegistry: ComponentRegistry;
projectService: ProjectService;
componentDiscovery: ComponentDiscoveryService;
propertyMetadata: PropertyMetadataService;
@@ -121,7 +120,7 @@ export class ServiceRegistry {
const messageHub = new MessageHub();
const serializerRegistry = new SerializerRegistry();
const entityStore = new EntityStoreService(messageHub);
const componentRegistry = new EditorComponentRegistry();
const componentRegistry = new ComponentRegistry();
// 注册标准组件到编辑器和核心注册表
// Register to both editor registry (for UI) and core registry (for serialization)
@@ -129,7 +128,7 @@ export class ServiceRegistry {
{ name: 'TransformComponent', type: TransformComponent, editorName: 'Transform', category: 'components.category.core', description: 'components.transform.description', icon: 'Move3d' },
{ name: 'SpriteComponent', type: SpriteComponent, editorName: 'Sprite', category: 'components.category.rendering', description: 'components.sprite.description', icon: 'Image' },
{ name: 'SpriteAnimatorComponent', type: SpriteAnimatorComponent, editorName: 'SpriteAnimator', category: 'components.category.rendering', description: 'components.spriteAnimator.description', icon: 'Film' },
{ name: 'FGUIComponent', type: FGUIComponent, editorName: 'FGUI', category: 'components.category.ui', description: 'FairyGUI UI component', icon: 'Layout' },
{ name: 'UITextComponent', type: UITextComponent, editorName: 'UIText', category: 'components.category.ui', description: 'components.text.description', icon: 'Type' },
{ name: 'CameraComponent', type: CameraComponent, editorName: 'Camera', category: 'components.category.rendering', description: 'components.camera.description', icon: 'Camera' },
{ name: 'AudioSourceComponent', type: AudioSourceComponent, editorName: 'AudioSource', category: 'components.category.audio', description: 'components.audioSource.description', icon: 'Volume2' },
{ name: 'BehaviorTreeRuntimeComponent', type: BehaviorTreeRuntimeComponent, editorName: 'BehaviorTreeRuntime', category: 'components.category.ai', description: 'components.behaviorTreeRuntime.description', icon: 'GitBranch' }
@@ -168,7 +167,7 @@ export class ServiceRegistry {
Core.services.registerInstance(IMessageHub, messageHub); // Symbol 注册用于跨包插件访问
Core.services.registerInstance(SerializerRegistry, serializerRegistry);
Core.services.registerInstance(EntityStoreService, entityStore);
Core.services.registerInstance(EditorComponentRegistry, componentRegistry);
Core.services.registerInstance(ComponentRegistry, componentRegistry);
Core.services.registerInstance(ProjectService, projectService);
Core.services.registerInstance(ComponentDiscoveryService, componentDiscovery);
Core.services.registerInstance(PropertyMetadataService, propertyMetadata);
@@ -250,7 +249,6 @@ export class ServiceRegistry {
fieldEditorRegistry.register(new Vector4FieldEditor());
fieldEditorRegistry.register(new ColorFieldEditor());
fieldEditorRegistry.register(new AnimationClipsFieldEditor());
fieldEditorRegistry.register(new EntityRefFieldEditor());
// 注册组件检查器
// Register component inspectors
@@ -1,5 +1,5 @@
import { Entity, Component, getComponentDependencies, getComponentTypeName } from '@esengine/ecs-framework';
import { MessageHub, EditorComponentRegistry } from '@esengine/editor-core';
import { MessageHub, ComponentRegistry } from '@esengine/editor-core';
import { Core } from '@esengine/ecs-framework';
import { BaseCommand } from '../BaseCommand';
@@ -55,7 +55,7 @@ export class AddComponentCommand extends BaseCommand {
return;
}
const componentRegistry = Core.services.tryResolve(EditorComponentRegistry) as EditorComponentRegistry | null;
const componentRegistry = Core.services.tryResolve(ComponentRegistry) as ComponentRegistry | null;
if (!componentRegistry) {
return;
}
@@ -1,6 +1,7 @@
import { Entity, Component } from '@esengine/ecs-framework';
import { MessageHub } from '@esengine/editor-core';
import { TransformComponent } from '@esengine/engine-core';
import { UITransformComponent } from '@esengine/ui';
import { BaseCommand } from '../BaseCommand';
import { ICommand } from '../ICommand';
@@ -9,6 +10,7 @@ import { ICommand } from '../ICommand';
* Transform state snapshot
*/
export interface TransformState {
// TransformComponent
positionX?: number;
positionY?: number;
positionZ?: number;
@@ -18,6 +20,14 @@ export interface TransformState {
scaleX?: number;
scaleY?: number;
scaleZ?: number;
// UITransformComponent
x?: number;
y?: number;
width?: number;
height?: number;
rotation?: number;
uiScaleX?: number;
uiScaleY?: number;
}
/**
@@ -31,17 +41,19 @@ export type TransformOperationType = 'move' | 'rotate' | 'scale';
* Transform command for undo/redo support
*/
export class TransformCommand extends BaseCommand {
private readonly componentType: 'transform' | 'uiTransform';
private readonly timestamp: number;
constructor(
private readonly messageHub: MessageHub,
private readonly entity: Entity,
private readonly component: TransformComponent,
private readonly component: Component,
private readonly operationType: TransformOperationType,
private readonly oldState: TransformState,
private newState: TransformState
) {
super();
this.componentType = component instanceof TransformComponent ? 'transform' : 'uiTransform';
this.timestamp = Date.now();
}
@@ -102,16 +114,25 @@ export class TransformCommand extends BaseCommand {
* Apply transform state
*/
private applyState(state: TransformState): void {
const transform = this.component;
if (state.positionX !== undefined) transform.position.x = state.positionX;
if (state.positionY !== undefined) transform.position.y = state.positionY;
if (state.positionZ !== undefined) transform.position.z = state.positionZ;
if (state.rotationX !== undefined) transform.rotation.x = state.rotationX;
if (state.rotationY !== undefined) transform.rotation.y = state.rotationY;
if (state.rotationZ !== undefined) transform.rotation.z = state.rotationZ;
if (state.scaleX !== undefined) transform.scale.x = state.scaleX;
if (state.scaleY !== undefined) transform.scale.y = state.scaleY;
if (state.scaleZ !== undefined) transform.scale.z = state.scaleZ;
if (this.componentType === 'transform') {
const transform = this.component as TransformComponent;
if (state.positionX !== undefined) transform.position.x = state.positionX;
if (state.positionY !== undefined) transform.position.y = state.positionY;
if (state.positionZ !== undefined) transform.position.z = state.positionZ;
if (state.rotationX !== undefined) transform.rotation.x = state.rotationX;
if (state.rotationY !== undefined) transform.rotation.y = state.rotationY;
if (state.rotationZ !== undefined) transform.rotation.z = state.rotationZ;
if (state.scaleX !== undefined) transform.scale.x = state.scaleX;
if (state.scaleY !== undefined) transform.scale.y = state.scaleY;
if (state.scaleZ !== undefined) transform.scale.z = state.scaleZ;
} else {
const uiTransform = this.component as UITransformComponent;
if (state.x !== undefined) uiTransform.x = state.x;
if (state.y !== undefined) uiTransform.y = state.y;
if (state.rotation !== undefined) uiTransform.rotation = state.rotation;
if (state.uiScaleX !== undefined) uiTransform.scaleX = state.uiScaleX;
if (state.uiScaleY !== undefined) uiTransform.scaleY = state.uiScaleY;
}
}
/**
@@ -120,16 +141,18 @@ export class TransformCommand extends BaseCommand {
*/
private notifyChange(): void {
const propertyName = this.operationType === 'move'
? 'position'
? (this.componentType === 'transform' ? 'position' : 'x')
: this.operationType === 'rotate'
? 'rotation'
: 'scale';
: (this.componentType === 'transform' ? 'scale' : 'scaleX');
this.messageHub.publish('component:property:changed', {
entity: this.entity,
component: this.component,
propertyName,
value: this.component[propertyName as keyof TransformComponent]
value: this.componentType === 'transform'
? (this.component as TransformComponent)[propertyName as keyof TransformComponent]
: (this.component as UITransformComponent)[propertyName as keyof UITransformComponent]
});
// 通知 Inspector 刷新 | Notify Inspector to refresh
@@ -153,4 +176,18 @@ export class TransformCommand extends BaseCommand {
scaleZ: transform.scale.z
};
}
/**
* UITransformComponent
* Capture state from UITransformComponent
*/
static captureUITransformState(uiTransform: UITransformComponent): TransformState {
return {
x: uiTransform.x,
y: uiTransform.y,
rotation: uiTransform.rotation,
uiScaleX: uiTransform.scaleX,
uiScaleY: uiTransform.scaleY
};
}
}
@@ -6,7 +6,6 @@ import * as LucideIcons from 'lucide-react';
import { AnimationClipsFieldEditor } from '../infrastructure/field-editors/AnimationClipsFieldEditor';
import { AssetField } from './inspectors/fields/AssetField';
import { CollisionLayerField } from './inspectors/fields/CollisionLayerField';
import { EntityRefField } from './inspectors/fields/EntityRefField';
import { useLocale } from '../hooks/useLocale';
import '../styles/PropertyInspector.css';
@@ -340,17 +339,6 @@ export function PropertyInspector({ component, entity, version, onChange, onActi
/>
);
case 'entityRef':
return (
<EntityRefField
key={propertyName}
label={label}
value={value ?? 0}
readonly={metadata.readOnly}
onChange={(newValue) => handleChange(propertyName, newValue)}
/>
);
case 'array': {
const arrayMeta = metadata as {
itemType?: { type: string; extensions?: string[]; assetType?: string };
@@ -9,8 +9,7 @@
import { useState, useEffect, useRef, useCallback, useMemo } from 'react';
import { Entity, Core, HierarchySystem, HierarchyComponent, EntityTags, isFolder, PrefabSerializer, ComponentRegistry, getComponentInstanceTypeName, PrefabInstanceComponent } from '@esengine/ecs-framework';
import type { PrefabData, ComponentType } from '@esengine/ecs-framework';
import { EntityStoreService, MessageHub, CommandManager, EntityCreationRegistry, EntityCreationTemplate, PrefabService, AssetRegistryService, ProjectService, VirtualNodeRegistry } from '@esengine/editor-core';
import type { IVirtualNode } from '@esengine/editor-core';
import { EntityStoreService, MessageHub, CommandManager, EntityCreationRegistry, EntityCreationTemplate, PrefabService, AssetRegistryService, ProjectService } from '@esengine/editor-core';
import { useLocale } from '../hooks/useLocale';
import { useHierarchyStore } from '../stores';
import * as LucideIcons from 'lucide-react';
@@ -49,36 +48,6 @@ const categoryIconMap: Record<string, string> = {
'other': 'MoreHorizontal',
};
/**
* Map virtual node types to Lucide icon names
* Lucide
*/
const virtualNodeIconMap: Record<string, string> = {
'Component': 'LayoutGrid',
'Image': 'Image',
'Graph': 'Square',
'TextField': 'Type',
'RichTextField': 'FileText',
'Button': 'MousePointer',
'List': 'List',
'Loader': 'Loader',
'ProgressBar': 'BarChart',
'Slider': 'Sliders',
'ComboBox': 'ChevronDown',
'ScrollPane': 'Scroll',
'Group': 'FolderOpen',
'MovieClip': 'Film',
'TextInput': 'FormInput',
};
/**
* Get icon name for a virtual node type
*
*/
function getVirtualNodeIcon(nodeType: string): string {
return virtualNodeIconMap[nodeType] || 'Circle';
}
// 实体类型到图标的映射
const entityTypeIcons: Record<string, React.ReactNode> = {
'World': <Mountain size={14} className="entity-type-icon world" />,
@@ -109,21 +78,6 @@ interface EntityNode {
depth: number;
bIsFolder: boolean;
hasChildren: boolean;
/** Virtual nodes from components (e.g., FGUI internal nodes) | 组件的虚拟节点(如 FGUI 内部节点) */
virtualNodes?: IVirtualNode[];
}
/**
* Flattened list item - can be either an entity node or a virtual node
* -
*/
interface FlattenedItem {
type: 'entity' | 'virtual';
entityNode?: EntityNode;
virtualNode?: IVirtualNode;
depth: number;
parentEntityId: number;
hasChildren: boolean;
}
/**
@@ -186,15 +140,6 @@ export function SceneHierarchy({ entityStore, messageHub, commandManager, isProf
const [showFilterMenu, setShowFilterMenu] = useState(false);
const [editingEntityId, setEditingEntityId] = useState<number | null>(null);
const [editingName, setEditingName] = useState('');
// Expanded virtual node IDs (format: "entityId:virtualNodeId")
// 展开的虚拟节点 ID(格式:"entityId:virtualNodeId"
const [expandedVirtualIds, setExpandedVirtualIds] = useState<Set<string>>(new Set());
// Selected virtual node (format: "entityId:virtualNodeId")
// 选中的虚拟节点(格式:"entityId:virtualNodeId"
const [selectedVirtualId, setSelectedVirtualId] = useState<string | null>(null);
// Refresh counter to force virtual nodes recollection
// 刷新计数器,用于强制重新收集虚拟节点
const [virtualNodeRefreshKey, setVirtualNodeRefreshKey] = useState(0);
const { t, locale } = useLocale();
// Ref for auto-scrolling to selected item | 选中项自动滚动 ref
@@ -228,10 +173,6 @@ export function SceneHierarchy({ entityStore, messageHub, commandManager, isProf
/**
*
* Build hierarchical tree structure
*
* Also collects virtual nodes from components using VirtualNodeRegistry.
* 使 VirtualNodeRegistry
*/
const buildEntityTree = useCallback((rootEntities: Entity[]): EntityNode[] => {
const scene = Core.scene;
@@ -250,17 +191,12 @@ export function SceneHierarchy({ entityStore, messageHub, commandManager, isProf
}
}
// Collect virtual nodes from components
// 从组件收集虚拟节点
const virtualNodes = VirtualNodeRegistry.getAllVirtualNodesForEntity(entity);
return {
entity,
children,
depth,
bIsFolder: bIsEntityFolder,
hasChildren: children.length > 0 || virtualNodes.length > 0,
virtualNodes: virtualNodes.length > 0 ? virtualNodes : undefined
hasChildren: children.length > 0
};
};
@@ -269,68 +205,17 @@ export function SceneHierarchy({ entityStore, messageHub, commandManager, isProf
/**
*
* Flatten tree to list with depth info (for rendering)
*
* Also includes virtual nodes when their parent entity is expanded.
*
*/
const flattenTree = useCallback((
nodes: EntityNode[],
expandedSet: Set<number>,
expandedVirtualSet: Set<string>
): FlattenedItem[] => {
const result: FlattenedItem[] = [];
// Flatten virtual nodes recursively
// 递归扁平化虚拟节点
const flattenVirtualNodes = (
virtualNodes: IVirtualNode[],
parentEntityId: number,
baseDepth: number
) => {
for (const vnode of virtualNodes) {
const vnodeKey = `${parentEntityId}:${vnode.id}`;
const hasVChildren = vnode.children && vnode.children.length > 0;
result.push({
type: 'virtual',
virtualNode: vnode,
depth: baseDepth,
parentEntityId,
hasChildren: hasVChildren
});
// If virtual node is expanded, add its children
// 如果虚拟节点已展开,添加其子节点
if (hasVChildren && expandedVirtualSet.has(vnodeKey)) {
flattenVirtualNodes(vnode.children, parentEntityId, baseDepth + 1);
}
}
};
const flattenTree = useCallback((nodes: EntityNode[], expandedSet: Set<number>): EntityNode[] => {
const result: EntityNode[] = [];
const traverse = (nodeList: EntityNode[]) => {
for (const node of nodeList) {
// Add entity node
result.push({
type: 'entity',
entityNode: node,
depth: node.depth,
parentEntityId: node.entity.id,
hasChildren: node.hasChildren
});
result.push(node);
const bIsExpanded = expandedSet.has(node.entity.id);
if (bIsExpanded) {
// Add child entities
if (node.children.length > 0) {
traverse(node.children);
}
// Add virtual nodes after entity children
// 在实体子节点后添加虚拟节点
if (node.virtualNodes && node.virtualNodes.length > 0) {
flattenVirtualNodes(node.virtualNodes, node.entity.id, node.depth + 1);
}
if (bIsExpanded && node.children.length > 0) {
traverse(node.children);
}
}
};
@@ -341,92 +226,13 @@ export function SceneHierarchy({ entityStore, messageHub, commandManager, isProf
/**
*
* Hierarchy tree and flattened list
*
* virtualNodeRefreshKey is used to force rebuild when components change.
* virtualNodeRefreshKey
*/
// eslint-disable-next-line react-hooks/exhaustive-deps
const entityTree = useMemo(() => buildEntityTree(entities), [entities, buildEntityTree, virtualNodeRefreshKey]);
const flattenedItems = useMemo(
() => expandedIds.has(-1) ? flattenTree(entityTree, expandedIds, expandedVirtualIds) : [],
[entityTree, expandedIds, expandedVirtualIds, flattenTree]
const entityTree = useMemo(() => buildEntityTree(entities), [entities, buildEntityTree]);
const flattenedEntities = useMemo(
() => expandedIds.has(-1) ? flattenTree(entityTree, expandedIds) : [],
[entityTree, expandedIds, flattenTree]
);
/**
* Toggle virtual node expansion
*
*/
const toggleVirtualExpand = useCallback((parentEntityId: number, virtualNodeId: string) => {
const key = `${parentEntityId}:${virtualNodeId}`;
setExpandedVirtualIds(prev => {
const next = new Set(prev);
if (next.has(key)) {
next.delete(key);
} else {
next.add(key);
}
return next;
});
}, []);
/**
* Handle virtual node click
*
*/
const handleVirtualNodeClick = useCallback((parentEntityId: number, virtualNode: IVirtualNode) => {
const key = `${parentEntityId}:${virtualNode.id}`;
setSelectedVirtualId(key);
// Clear entity selection when selecting virtual node
// 选择虚拟节点时清除实体选择
setSelectedIds(new Set());
// Publish event for Inspector to display virtual node properties
// 发布事件以便 Inspector 显示虚拟节点属性
messageHub.publish('virtual-node:selected', {
parentEntityId,
virtualNodeId: virtualNode.id,
virtualNode
});
}, [messageHub, setSelectedIds]);
// Subscribe to scene:modified to refresh virtual nodes when components change
// 订阅 scene:modified 事件,当组件变化时刷新虚拟节点
useEffect(() => {
const unsubModified = messageHub.subscribe('scene:modified', () => {
setVirtualNodeRefreshKey(prev => prev + 1);
});
// Also subscribe to component-specific events
// 同时订阅组件相关事件
const unsubComponentAdded = messageHub.subscribe('component:added', () => {
setVirtualNodeRefreshKey(prev => prev + 1);
});
const unsubComponentRemoved = messageHub.subscribe('component:removed', () => {
setVirtualNodeRefreshKey(prev => prev + 1);
});
return () => {
unsubModified();
unsubComponentAdded();
unsubComponentRemoved();
};
}, [messageHub]);
// Subscribe to VirtualNodeRegistry changes (event-driven, no polling needed)
// 订阅 VirtualNodeRegistry 变化(事件驱动,无需轮询)
useEffect(() => {
const unsubscribe = VirtualNodeRegistry.onChange((event) => {
// Refresh if the changed entity is expanded
// 如果变化的实体是展开的,则刷新
if (expandedIds.has(event.entityId)) {
setVirtualNodeRefreshKey(prev => prev + 1);
}
});
return unsubscribe;
}, [expandedIds]);
// 获取插件实体创建模板 | Get entity creation templates from plugins
useEffect(() => {
const updateTemplates = () => {
@@ -451,14 +257,6 @@ export function SceneHierarchy({ entityStore, messageHub, commandManager, isProf
// Note: Scene/entity/remote subscriptions moved to useStoreSubscriptions
const handleEntityClick = (entity: Entity, e: React.MouseEvent) => {
// Clear virtual node selection when selecting an entity
// 选择实体时清除虚拟节点选择
setSelectedVirtualId(null);
// Force refresh virtual nodes to pick up any newly loaded components
// 强制刷新虚拟节点以获取新加载的组件
setVirtualNodeRefreshKey(prev => prev + 1);
if (e.ctrlKey || e.metaKey) {
setSelectedIds(prev => {
const next = new Set(prev);
@@ -1129,26 +927,22 @@ export function SceneHierarchy({ entityStore, messageHub, commandManager, isProf
}
// 方向键导航 | Arrow key navigation
// Only navigate entity nodes, skip virtual nodes
// 只导航实体节点,跳过虚拟节点
const entityItems = flattenedItems.filter(item => item.type === 'entity');
if ((e.key === 'ArrowUp' || e.key === 'ArrowDown') && entityItems.length > 0) {
if ((e.key === 'ArrowUp' || e.key === 'ArrowDown') && flattenedEntities.length > 0) {
e.preventDefault();
const currentIndex = selectedId
? entityItems.findIndex(item => item.entityNode?.entity.id === selectedId)
? flattenedEntities.findIndex(n => n.entity.id === selectedId)
: -1;
let newIndex: number;
if (e.key === 'ArrowUp') {
newIndex = currentIndex <= 0 ? entityItems.length - 1 : currentIndex - 1;
newIndex = currentIndex <= 0 ? flattenedEntities.length - 1 : currentIndex - 1;
} else {
newIndex = currentIndex >= entityItems.length - 1 ? 0 : currentIndex + 1;
newIndex = currentIndex >= flattenedEntities.length - 1 ? 0 : currentIndex + 1;
}
const newEntity = entityItems[newIndex]?.entityNode?.entity;
const newEntity = flattenedEntities[newIndex]?.entity;
if (newEntity) {
setSelectedIds(new Set([newEntity.id]));
setSelectedVirtualId(null); // Clear virtual selection
entityStore.selectEntity(newEntity);
messageHub.publish('entity:selected', { entity: newEntity });
}
@@ -1158,7 +952,7 @@ export function SceneHierarchy({ entityStore, messageHub, commandManager, isProf
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [selectedId, isShowingRemote, editingEntityId, flattenedItems, entityStore, messageHub, handleStartRename, handleConfirmRename, handleCancelRename, handleDuplicateEntity]);
}, [selectedId, isShowingRemote, editingEntityId, flattenedEntities, entityStore, messageHub, handleStartRename, handleConfirmRename, handleCancelRename, handleDuplicateEntity]);
/**
*
@@ -1509,164 +1303,107 @@ export function SceneHierarchy({ entityStore, messageHub, commandManager, isProf
</div>
</div>
{/* Hierarchical Entity and Virtual Node Items */}
{flattenedItems.map((item, index) => {
// Render entity node
if (item.type === 'entity' && item.entityNode) {
const node = item.entityNode;
const { entity, bIsFolder } = node;
const bIsExpanded = expandedIds.has(entity.id);
const bIsSelected = selectedIds.has(entity.id);
const bIsDragging = draggedEntityId === entity.id;
const currentDropTarget = dropTarget?.entityId === entity.id ? dropTarget : null;
const bIsPrefabInstance = isEntityPrefabInstance(entity);
{/* Hierarchical Entity Items */}
{flattenedEntities.map((node) => {
const { entity, depth, hasChildren, bIsFolder } = node;
const bIsExpanded = expandedIds.has(entity.id);
const bIsSelected = selectedIds.has(entity.id);
const bIsDragging = draggedEntityId === entity.id;
const currentDropTarget = dropTarget?.entityId === entity.id ? dropTarget : null;
const bIsPrefabInstance = isEntityPrefabInstance(entity);
// 计算缩进 (每层 16px,加上基础 8px)
const indent = 8 + item.depth * 16;
// 计算缩进 (每层 16px,加上基础 8px)
const indent = 8 + depth * 16;
// 构建 drop indicator 类名
let dropIndicatorClass = '';
if (currentDropTarget) {
dropIndicatorClass = `drop-${currentDropTarget.indicator}`;
}
return (
<div
key={`entity-${entity.id}`}
ref={bIsSelected ? selectedItemRef : undefined}
className={`outliner-item ${bIsSelected ? 'selected' : ''} ${bIsDragging ? 'dragging' : ''} ${dropIndicatorClass} ${bIsPrefabInstance ? 'prefab-instance' : ''}`}
style={{ paddingLeft: `${indent}px` }}
draggable
onClick={(e) => handleEntityClick(entity, e)}
onDragStart={(e) => handleDragStart(e, entity.id)}
onDragOver={(e) => handleDragOver(e, node)}
onDragLeave={handleDragLeave}
onDrop={(e) => handleDrop(e, node)}
onDragEnd={handleDragEnd}
onContextMenu={(e) => {
e.stopPropagation();
handleEntityClick(entity, e);
handleContextMenu(e, entity.id);
}}
>
<div className="outliner-item-icons">
{isEntityVisible(entity) ? (
<Eye
size={12}
className="item-icon visibility"
onClick={(e) => handleToggleVisibility(entity, e)}
/>
) : (
<EyeOff
size={12}
className="item-icon visibility hidden"
onClick={(e) => handleToggleVisibility(entity, e)}
/>
)}
</div>
<div className="outliner-item-content">
{/* 展开/折叠按钮 */}
{item.hasChildren || bIsFolder ? (
<span
className="outliner-item-expand clickable"
onClick={(e) => { e.stopPropagation(); toggleExpand(entity.id); }}
>
{bIsExpanded ? <ChevronDown size={12} /> : <ChevronRight size={12} />}
</span>
) : (
<span className="outliner-item-expand" />
)}
{getEntityIcon(entity)}
{editingEntityId === entity.id ? (
<input
className="outliner-item-name-input"
value={editingName}
onChange={(e) => setEditingName(e.target.value)}
onBlur={handleConfirmRename}
onKeyDown={(e) => {
if (e.key === 'Enter') handleConfirmRename();
if (e.key === 'Escape') handleCancelRename();
e.stopPropagation();
}}
onClick={(e) => e.stopPropagation()}
autoFocus
/>
) : (
<span
className="outliner-item-name"
onDoubleClick={(e) => {
e.stopPropagation();
setEditingEntityId(entity.id);
setEditingName(entity.name || '');
}}
>
{entity.name || `Entity ${entity.id}`}
</span>
)}
{/* 预制体实例徽章 | Prefab instance badge */}
{bIsPrefabInstance && (
<span className="prefab-badge" title={t('inspector.prefab.instance', {}, 'Prefab Instance')}>
P
</span>
)}
</div>
<div className="outliner-item-type">{getEntityType(entity)}</div>
</div>
);
// 构建 drop indicator 类名
let dropIndicatorClass = '';
if (currentDropTarget) {
dropIndicatorClass = `drop-${currentDropTarget.indicator}`;
}
// Render virtual node (read-only)
// 渲染虚拟节点(只读)
if (item.type === 'virtual' && item.virtualNode) {
const vnode = item.virtualNode;
const vnodeKey = `${item.parentEntityId}:${vnode.id}`;
const bIsVExpanded = expandedVirtualIds.has(vnodeKey);
const bIsVSelected = selectedVirtualId === vnodeKey;
// 计算缩进
const indent = 8 + item.depth * 16;
return (
<div
key={`virtual-${vnodeKey}-${index}`}
className={`outliner-item virtual-node ${bIsVSelected ? 'selected' : ''} ${!vnode.visible ? 'hidden-node' : ''}`}
style={{ paddingLeft: `${indent}px` }}
onClick={() => handleVirtualNodeClick(item.parentEntityId, vnode)}
>
<div className="outliner-item-icons">
{vnode.visible ? (
<Eye size={12} className="item-icon visibility" />
) : (
<EyeOff size={12} className="item-icon visibility hidden" />
)}
</div>
<div className="outliner-item-content">
{/* 展开/折叠按钮 */}
{item.hasChildren ? (
<span
className="outliner-item-expand clickable"
onClick={(e) => {
e.stopPropagation();
toggleVirtualExpand(item.parentEntityId, vnode.id);
}}
>
{bIsVExpanded ? <ChevronDown size={12} /> : <ChevronRight size={12} />}
</span>
) : (
<span className="outliner-item-expand" />
)}
{/* 虚拟节点类型图标 */}
{getIconComponent(getVirtualNodeIcon(vnode.type), 14)}
<span className="outliner-item-name virtual-name">
{vnode.name}
return (
<div
key={entity.id}
ref={bIsSelected ? selectedItemRef : undefined}
className={`outliner-item ${bIsSelected ? 'selected' : ''} ${bIsDragging ? 'dragging' : ''} ${dropIndicatorClass} ${bIsPrefabInstance ? 'prefab-instance' : ''}`}
style={{ paddingLeft: `${indent}px` }}
draggable
onClick={(e) => handleEntityClick(entity, e)}
onDragStart={(e) => handleDragStart(e, entity.id)}
onDragOver={(e) => handleDragOver(e, node)}
onDragLeave={handleDragLeave}
onDrop={(e) => handleDrop(e, node)}
onDragEnd={handleDragEnd}
onContextMenu={(e) => {
e.stopPropagation();
handleEntityClick(entity, e);
handleContextMenu(e, entity.id);
}}
>
<div className="outliner-item-icons">
{isEntityVisible(entity) ? (
<Eye
size={12}
className="item-icon visibility"
onClick={(e) => handleToggleVisibility(entity, e)}
/>
) : (
<EyeOff
size={12}
className="item-icon visibility hidden"
onClick={(e) => handleToggleVisibility(entity, e)}
/>
)}
</div>
<div className="outliner-item-content">
{/* 展开/折叠按钮 */}
{hasChildren || bIsFolder ? (
<span
className="outliner-item-expand clickable"
onClick={(e) => { e.stopPropagation(); toggleExpand(entity.id); }}
>
{bIsExpanded ? <ChevronDown size={12} /> : <ChevronRight size={12} />}
</span>
</div>
<div className="outliner-item-type virtual-type">{vnode.type}</div>
) : (
<span className="outliner-item-expand" />
)}
{getEntityIcon(entity)}
{editingEntityId === entity.id ? (
<input
className="outliner-item-name-input"
value={editingName}
onChange={(e) => setEditingName(e.target.value)}
onBlur={handleConfirmRename}
onKeyDown={(e) => {
if (e.key === 'Enter') handleConfirmRename();
if (e.key === 'Escape') handleCancelRename();
e.stopPropagation();
}}
onClick={(e) => e.stopPropagation()}
autoFocus
/>
) : (
<span
className="outliner-item-name"
onDoubleClick={(e) => {
e.stopPropagation();
setEditingEntityId(entity.id);
setEditingName(entity.name || '');
}}
>
{entity.name || `Entity ${entity.id}`}
</span>
)}
{/* 预制体实例徽章 | Prefab instance badge */}
{bIsPrefabInstance && (
<span className="prefab-badge" title={t('inspector.prefab.instance', {}, 'Prefab Instance')}>
P
</span>
)}
</div>
);
}
return null;
<div className="outliner-item-type">{getEntityType(entity)}</div>
</div>
);
})}
</div>
)
@@ -162,45 +162,39 @@ export function SettingsWindow({ onClose, settingsRegistry, initialCategoryId }:
const initialValues = new Map<string, any>();
for (const [key, descriptor] of allSettings.entries()) {
// 特定的 project 设置需要从 ProjectService 加载
// Specific project settings need to load from ProjectService
if (key === 'project.uiDesignResolution.width' && projectService) {
const resolution = projectService.getUIDesignResolution();
initialValues.set(key, resolution.width);
} else if (key === 'project.uiDesignResolution.height' && projectService) {
const resolution = projectService.getUIDesignResolution();
initialValues.set(key, resolution.height);
} else if (key === 'project.uiDesignResolution.preset' && projectService) {
const resolution = projectService.getUIDesignResolution();
initialValues.set(key, `${resolution.width}x${resolution.height}`);
} else if (key === 'project.disabledModules' && projectService) {
// Load disabled modules from ProjectService
initialValues.set(key, projectService.getDisabledModules());
if (key.startsWith('project.') && projectService) {
if (key === 'project.uiDesignResolution.width') {
const resolution = projectService.getUIDesignResolution();
initialValues.set(key, resolution.width);
} else if (key === 'project.uiDesignResolution.height') {
const resolution = projectService.getUIDesignResolution();
initialValues.set(key, resolution.height);
} else if (key === 'project.uiDesignResolution.preset') {
const resolution = projectService.getUIDesignResolution();
initialValues.set(key, `${resolution.width}x${resolution.height}`);
} else if (key === 'project.disabledModules') {
// Load disabled modules from ProjectService
initialValues.set(key, projectService.getDisabledModules());
} else {
initialValues.set(key, descriptor.defaultValue);
}
} else {
// 其他设置(包括 project.dynamicAtlas.*)从 SettingsService 加载
// Other settings (including project.dynamicAtlas.*) load from SettingsService
const value = settings.get(key, descriptor.defaultValue);
initialValues.set(key, value);
if (key.startsWith('profiler.')) {
console.log(`[SettingsWindow] Loading ${key}: stored=${settings.get(key, undefined)}, default=${descriptor.defaultValue}, using=${value}`);
}
}
}
console.log('[SettingsWindow] Initial values for profiler:',
Array.from(initialValues.entries()).filter(([k]) => k.startsWith('profiler.')));
setValues(initialValues);
}, [settingsRegistry, initialCategoryId]);
const handleValueChange = (key: string, value: any, descriptor: SettingDescriptor) => {
const newValues = new Map(values);
newValues.set(key, value);
// When preset is selected, also update width and height values
// 当选择预设时,同时更新宽度和高度值
if (key === 'project.uiDesignResolution.preset' && typeof value === 'string' && value.includes('x')) {
const [w, h] = value.split('x').map(Number);
if (w && h) {
newValues.set('project.uiDesignResolution.width', w);
newValues.set('project.uiDesignResolution.height', h);
}
}
setValues(newValues);
const newErrors = new Map(errors);
@@ -214,22 +208,12 @@ export function SettingsWindow({ onClose, settingsRegistry, initialCategoryId }:
setErrors(newErrors);
// 实时保存设置
// Real-time save settings
const settings = SettingsService.getInstance();
// 除了特定的 project 设置需要延迟保存外,其他都实时保存
// Save in real-time except for specific project settings that need deferred save
const deferredProjectSettings = [
'project.uiDesignResolution.',
'project.disabledModules'
];
const shouldDeferSave = deferredProjectSettings.some(prefix => key.startsWith(prefix));
if (!shouldDeferSave) {
if (!key.startsWith('project.')) {
settings.set(key, value);
console.log(`[SettingsWindow] Saved ${key}:`, value);
// 触发设置变更事件
// Trigger settings changed event
window.dispatchEvent(new CustomEvent('settings:changed', {
detail: { [key]: value }
}));
@@ -245,27 +229,28 @@ export function SettingsWindow({ onClose, settingsRegistry, initialCategoryId }:
const projectService = Core.services.tryResolve<ProjectService>(ProjectService);
const changedSettings: Record<string, any> = {};
// Get width and height directly from values - these are the actual UI input values
// 直接从 values 获取宽高 - 这些是实际的 UI 输入值
const widthFromValues = values.get('project.uiDesignResolution.width');
const heightFromValues = values.get('project.uiDesignResolution.height');
// Use the width/height values directly (they are always set from either user input or initial load)
// 直接使用 width/height 值(它们总是从用户输入或初始加载设置的)
const newWidth = typeof widthFromValues === 'number' ? widthFromValues : 1920;
const newHeight = typeof heightFromValues === 'number' ? heightFromValues : 1080;
// Check if resolution differs from saved config
// 检查分辨率是否与保存的配置不同
const currentResolution = projectService?.getUIDesignResolution() || { width: 1920, height: 1080 };
const uiResolutionChanged = newWidth !== currentResolution.width || newHeight !== currentResolution.height;
let uiResolutionChanged = false;
let newWidth = 1920;
let newHeight = 1080;
let disabledModulesChanged = false;
let newDisabledModules: string[] = [];
for (const [key, value] of values.entries()) {
if (key.startsWith('project.') && projectService) {
if (key === 'project.disabledModules') {
if (key === 'project.uiDesignResolution.width') {
newWidth = value;
uiResolutionChanged = true;
} else if (key === 'project.uiDesignResolution.height') {
newHeight = value;
uiResolutionChanged = true;
} else if (key === 'project.uiDesignResolution.preset') {
const [w, h] = value.split('x').map(Number);
if (w && h) {
newWidth = w;
newHeight = h;
uiResolutionChanged = true;
}
} else if (key === 'project.disabledModules') {
newDisabledModules = value as string[];
disabledModulesChanged = true;
}
@@ -277,9 +262,7 @@ export function SettingsWindow({ onClose, settingsRegistry, initialCategoryId }:
}
if (uiResolutionChanged && projectService) {
console.log(`[SettingsWindow] Saving UI resolution: ${newWidth}x${newHeight}`);
await projectService.setUIDesignResolution({ width: newWidth, height: newHeight });
console.log(`[SettingsWindow] UI resolution saved, verifying: ${JSON.stringify(projectService.getUIDesignResolution())}`);
}
if (disabledModulesChanged && projectService) {
@@ -579,14 +562,14 @@ export function SettingsWindow({ onClose, settingsRegistry, initialCategoryId }:
const selectedCategory = categories.find((c) => c.id === selectedCategoryId);
return (
<div className="settings-overlay" onClick={handleSave}>
<div className="settings-overlay" onClick={handleCancel}>
<div className="settings-window-new" onClick={(e) => e.stopPropagation()}>
{/* Left Sidebar */}
<div className="settings-sidebar-new">
<div className="settings-sidebar-header">
<SettingsIcon size={16} />
<span>{t('settingsWindow.editorPreferences')}</span>
<button className="settings-sidebar-close" onClick={handleSave}>
<button className="settings-sidebar-close" onClick={handleCancel}>
<X size={14} />
</button>
</div>

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