Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c5aaecc68b |
@@ -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 }}
|
||||
|
||||
|
||||
@@ -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. **渐进迁移**:可分阶段进行,不影响现有功能
|
||||
@@ -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.
|
||||
@@ -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);
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
{ "path": "../core" },
|
||||
{ "path": "../engine-core" },
|
||||
{ "path": "../editor-core" },
|
||||
{ "path": "../ui" },
|
||||
{ "path": "../editor-runtime" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,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",
|
||||
|
||||
@@ -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 {
|
||||
/**
|
||||
* 是否可更新
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
/**
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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[];
|
||||
/** 所有匹配实体的总数 */
|
||||
|
||||
@@ -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.
|
||||
* 注册组件类型并分配位掩码。
|
||||
|
||||
@@ -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,3 +1,2 @@
|
||||
export { EventBus, GlobalEventBus } from '../EventBus';
|
||||
export { TypeSafeEventSystem } from '../EventSystem';
|
||||
export type { EventListenerConfig, EventStats } from '../EventSystem';
|
||||
export { TypeSafeEventSystem, EventListenerConfig, EventStats } from '../EventSystem';
|
||||
|
||||
@@ -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 };
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
/** 查询执行时间(毫秒) */
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,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';
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
/**
|
||||
* 实体ID(运行时ID)
|
||||
*
|
||||
|
||||
@@ -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 {
|
||||
/**
|
||||
* 是否包含组件数据的深度对比
|
||||
* 默认true,设为false可提升性能但可能漏掉组件内部字段变更
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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-127,segments[1] 对应 bit 128-191,以此类推
|
||||
*/
|
||||
export type BitMask64Data = {
|
||||
export interface BitMask64Data {
|
||||
base: BitMask64Segment;
|
||||
/** 扩展段数组,每个元素是一个 64 位段,用于超过 64 位的场景 */
|
||||
segments?: BitMask64Segment[];
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -4,7 +4,7 @@ import { getComponentTypeName } from '../Decorators';
|
||||
/**
|
||||
* 查询条件类型
|
||||
*/
|
||||
export type QueryCondition = {
|
||||
export interface QueryCondition {
|
||||
all: ComponentType[];
|
||||
any: ComponentType[];
|
||||
none: ComponentType[];
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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名称
|
||||
*/
|
||||
|
||||
@@ -7,7 +7,7 @@ const logger = createLogger('WorldManager');
|
||||
/**
|
||||
* WorldManager配置接口
|
||||
*/
|
||||
export type IWorldManagerConfig = {
|
||||
export interface IWorldManagerConfig {
|
||||
/**
|
||||
* 最大World数量
|
||||
*/
|
||||
|
||||
@@ -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 {
|
||||
/**
|
||||
* 平台类型
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
*
|
||||
* 实现此接口的服务将在每帧被Core自动调用update方法
|
||||
*/
|
||||
export type IUpdatable = {
|
||||
export interface IUpdatable {
|
||||
/**
|
||||
* 每帧更新方法
|
||||
*
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,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;
|
||||
|
||||
@@ -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,4 +1,4 @@
|
||||
export type ITimer<TContext = unknown> = {
|
||||
export interface ITimer<TContext = unknown> {
|
||||
context: TContext;
|
||||
|
||||
/**
|
||||
|
||||
@@ -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) | 材质ID(0 = 默认)
|
||||
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 表示 2D,1 表示 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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
@@ -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(运行时模式)时,编辑器专用 UI(如网格、gizmos、坐标轴指示器)会自动隐藏。
|
||||
*/
|
||||
setEditorMode(is_editor: boolean): void;
|
||||
/**
|
||||
* Set render mode.
|
||||
* 设置渲染模式。
|
||||
*
|
||||
* # Arguments | 参数
|
||||
* * `mode` - 0 for 2D, 1 for 3D | 0 表示 2D,1 表示 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;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@esengine/editor-app",
|
||||
"version": "1.0.14",
|
||||
"version": "1.0.15-test",
|
||||
"description": "ESEngine Editor Application - Cross-platform desktop editor",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
@@ -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,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"productName": "ESEngine Editor",
|
||||
"version": "1.0.14",
|
||||
"version": "1.0.15-test",
|
||||
"identifier": "com.esengine.editor",
|
||||
"build": {
|
||||
"beforeDevCommand": "npm run build:watch",
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user