Feature/render pipeline (#232)
* refactor(engine): 重构2D渲染管线坐标系统 * feat(engine): 完善2D渲染管线和编辑器视口功能 * feat(editor): 实现Viewport变换工具系统 * feat(editor): 优化Inspector渲染性能并修复Gizmo变换工具显示 * feat(editor): 实现Run on Device移动预览功能 * feat(editor): 添加组件属性控制和依赖关系系统 * feat(editor): 实现动画预览功能和优化SpriteAnimator编辑器 * feat(editor): 修复SpriteAnimator动画预览功能并迁移CI到pnpm * feat(editor): 修复SpriteAnimator动画预览并迁移到pnpm * feat(editor): 修复SpriteAnimator动画预览并迁移到pnpm * feat(editor): 修复SpriteAnimator动画预览并迁移到pnpm * feat(editor): 修复SpriteAnimator动画预览并迁移到pnpm * feat(ci): 迁移项目到pnpm并修复CI构建问题 * chore: 迁移CI工作流到pnpm并添加WASM构建支持 * chore: 迁移CI工作流到pnpm并添加WASM构建支持 * chore: 迁移CI工作流到pnpm并添加WASM构建支持 * chore: 迁移CI工作流到pnpm并添加WASM构建支持 * chore: 迁移CI工作流到pnpm并添加WASM构建支持 * chore: 迁移CI工作流到pnpm并添加WASM构建支持 * chore: 移除 network 相关包 * chore: 移除 network 相关包
This commit is contained in:
@@ -1,161 +0,0 @@
|
||||
/**
|
||||
* Sprite component for ECS entities.
|
||||
* 用于ECS实体的精灵组件。
|
||||
*/
|
||||
|
||||
import { Component, ECSComponent } from '@esengine/ecs-framework';
|
||||
|
||||
/**
|
||||
* Sprite component data.
|
||||
* 精灵组件数据。
|
||||
*
|
||||
* Attach this component to entities that should be rendered as sprites.
|
||||
* 将此组件附加到应作为精灵渲染的实体。
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const entity = scene.createEntity('player');
|
||||
* entity.addComponent(SpriteComponent);
|
||||
* const sprite = entity.getComponent(SpriteComponent);
|
||||
* sprite.textureId = 1;
|
||||
* sprite.width = 64;
|
||||
* sprite.height = 64;
|
||||
* ```
|
||||
*/
|
||||
@ECSComponent('Sprite')
|
||||
export class SpriteComponent extends Component {
|
||||
/**
|
||||
* Texture ID for this sprite.
|
||||
* 此精灵的纹理ID。
|
||||
*/
|
||||
textureId: number = 0;
|
||||
|
||||
/**
|
||||
* Sprite width in pixels.
|
||||
* 精灵宽度(像素)。
|
||||
*/
|
||||
width: number = 0;
|
||||
|
||||
/**
|
||||
* Sprite height in pixels.
|
||||
* 精灵高度(像素)。
|
||||
*/
|
||||
height: number = 0;
|
||||
|
||||
/**
|
||||
* UV coordinates [u0, v0, u1, v1].
|
||||
* UV坐标。
|
||||
* Default is full texture [0, 0, 1, 1].
|
||||
* 默认为完整纹理。
|
||||
*/
|
||||
uv: [number, number, number, number] = [0, 0, 1, 1];
|
||||
|
||||
/**
|
||||
* Packed RGBA color (0xAABBGGRR format for WebGL).
|
||||
* 打包的RGBA颜色。
|
||||
* Default is white (0xFFFFFFFF).
|
||||
* 默认为白色。
|
||||
*/
|
||||
color: number = 0xFFFFFFFF;
|
||||
|
||||
/**
|
||||
* Origin point X (0-1, where 0.5 is center).
|
||||
* 原点X(0-1,0.5为中心)。
|
||||
*/
|
||||
originX: number = 0.5;
|
||||
|
||||
/**
|
||||
* Origin point Y (0-1, where 0.5 is center).
|
||||
* 原点Y(0-1,0.5为中心)。
|
||||
*/
|
||||
originY: number = 0.5;
|
||||
|
||||
/**
|
||||
* Whether sprite is visible.
|
||||
* 精灵是否可见。
|
||||
*/
|
||||
visible: boolean = true;
|
||||
|
||||
/**
|
||||
* Render layer/order (higher = rendered on top).
|
||||
* 渲染层级/顺序(越高越在上面)。
|
||||
*/
|
||||
layer: number = 0;
|
||||
|
||||
/**
|
||||
* Flip sprite horizontally.
|
||||
* 水平翻转精灵。
|
||||
*/
|
||||
flipX: boolean = false;
|
||||
|
||||
/**
|
||||
* Flip sprite vertically.
|
||||
* 垂直翻转精灵。
|
||||
*/
|
||||
flipY: boolean = false;
|
||||
|
||||
/**
|
||||
* Set UV from a sprite atlas region.
|
||||
* 从精灵图集区域设置UV。
|
||||
*
|
||||
* @param x - Region X in pixels | 区域X(像素)
|
||||
* @param y - Region Y in pixels | 区域Y(像素)
|
||||
* @param w - Region width in pixels | 区域宽度(像素)
|
||||
* @param h - Region height in pixels | 区域高度(像素)
|
||||
* @param atlasWidth - Atlas total width | 图集总宽度
|
||||
* @param atlasHeight - Atlas total height | 图集总高度
|
||||
*/
|
||||
setAtlasRegion(
|
||||
x: number,
|
||||
y: number,
|
||||
w: number,
|
||||
h: number,
|
||||
atlasWidth: number,
|
||||
atlasHeight: number
|
||||
): void {
|
||||
this.uv = [
|
||||
x / atlasWidth,
|
||||
y / atlasHeight,
|
||||
(x + w) / atlasWidth,
|
||||
(y + h) / atlasHeight
|
||||
];
|
||||
this.width = w;
|
||||
this.height = h;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set color from RGBA values (0-255).
|
||||
* 从RGBA值设置颜色(0-255)。
|
||||
*
|
||||
* @param r - Red | 红色
|
||||
* @param g - Green | 绿色
|
||||
* @param b - Blue | 蓝色
|
||||
* @param a - Alpha | 透明度
|
||||
*/
|
||||
setColorRGBA(r: number, g: number, b: number, a: number = 255): void {
|
||||
this.color = ((a & 0xFF) << 24) | ((b & 0xFF) << 16) | ((g & 0xFF) << 8) | (r & 0xFF);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set color from hex value (0xRRGGBB or 0xRRGGBBAA).
|
||||
* 从十六进制值设置颜色。
|
||||
*
|
||||
* @param hex - Hex color value | 十六进制颜色值
|
||||
*/
|
||||
setColorHex(hex: number): void {
|
||||
if (hex > 0xFFFFFF) {
|
||||
// 0xRRGGBBAA format
|
||||
const r = (hex >> 24) & 0xFF;
|
||||
const g = (hex >> 16) & 0xFF;
|
||||
const b = (hex >> 8) & 0xFF;
|
||||
const a = hex & 0xFF;
|
||||
this.color = (a << 24) | (b << 16) | (g << 8) | r;
|
||||
} else {
|
||||
// 0xRRGGBB format
|
||||
const r = (hex >> 16) & 0xFF;
|
||||
const g = (hex >> 8) & 0xFF;
|
||||
const b = hex & 0xFF;
|
||||
this.color = (0xFF << 24) | (b << 16) | (g << 8) | r;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,9 @@
|
||||
* TypeScript ECS与Rust引擎之间的主桥接层。
|
||||
*/
|
||||
|
||||
import type { SpriteRenderData, TextureLoadRequest, EngineStats } from '../types';
|
||||
import type { SpriteRenderData, TextureLoadRequest, EngineStats, CameraConfig } from '../types';
|
||||
import type { IEngineBridge } from '@esengine/asset-system';
|
||||
import type { GameEngine } from '../wasm/es_engine';
|
||||
|
||||
/**
|
||||
* Engine bridge configuration.
|
||||
@@ -41,11 +43,15 @@ export interface EngineBridgeConfig {
|
||||
* bridge.render();
|
||||
* ```
|
||||
*/
|
||||
export class EngineBridge {
|
||||
private engine: any; // GameEngine from WASM
|
||||
export class EngineBridge implements IEngineBridge {
|
||||
private engine: GameEngine | null = null;
|
||||
private config: Required<EngineBridgeConfig>;
|
||||
private initialized = false;
|
||||
|
||||
// Path resolver for converting file paths to URLs
|
||||
// 用于将文件路径转换为URL的路径解析器
|
||||
private pathResolver: ((path: string) => string) | null = null;
|
||||
|
||||
// Pre-allocated typed arrays for batch submission
|
||||
// 预分配的类型数组用于批量提交
|
||||
private transformBuffer: Float32Array;
|
||||
@@ -64,6 +70,7 @@ export class EngineBridge {
|
||||
private lastFrameTime = 0;
|
||||
private frameCount = 0;
|
||||
private fpsAccumulator = 0;
|
||||
private debugLogged = false;
|
||||
|
||||
/**
|
||||
* Create a new engine bridge.
|
||||
@@ -136,7 +143,7 @@ export class EngineBridge {
|
||||
|
||||
try {
|
||||
// Dynamic import of WASM module | 动态导入WASM模块
|
||||
const wasmModule = await import(/* webpackIgnore: true */ wasmPath);
|
||||
const wasmModule = await import(/* @vite-ignore */ wasmPath);
|
||||
await this.initializeWithModule(wasmModule);
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to initialize engine: ${error} | 引擎初始化失败: ${error}`);
|
||||
@@ -167,6 +174,17 @@ export class EngineBridge {
|
||||
return this.engine?.height ?? 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get engine instance (throws if not initialized)
|
||||
* 获取引擎实例(未初始化时抛出异常)
|
||||
*/
|
||||
private getEngine(): GameEngine {
|
||||
if (!this.engine) {
|
||||
throw new Error('Engine not initialized. Call initialize() first.');
|
||||
}
|
||||
return this.engine;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the screen.
|
||||
* 清除屏幕。
|
||||
@@ -178,7 +196,7 @@ export class EngineBridge {
|
||||
*/
|
||||
clear(r: number, g: number, b: number, a: number): void {
|
||||
if (!this.initialized) return;
|
||||
this.engine.clear(r, g, b, a);
|
||||
this.getEngine().clear(r, g, b, a);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -220,8 +238,15 @@ export class EngineBridge {
|
||||
this.colorBuffer[i] = sprite.color;
|
||||
}
|
||||
|
||||
// Debug: log texture IDs only once when we have 2+ sprites (for multi-texture test)
|
||||
if (!this.debugLogged && count >= 2) {
|
||||
const textureIds = Array.from(this.textureIdBuffer.subarray(0, count));
|
||||
console.log(`TS submitSprites: ${count} sprites, textureIds: [${textureIds.join(', ')}]`);
|
||||
this.debugLogged = true;
|
||||
}
|
||||
|
||||
// Submit to engine (single WASM call) | 提交到引擎(单次WASM调用)
|
||||
this.engine.submitSpriteBatch(
|
||||
this.getEngine().submitSpriteBatch(
|
||||
this.transformBuffer.subarray(0, count * 7),
|
||||
this.textureIdBuffer.subarray(0, count),
|
||||
this.uvBuffer.subarray(0, count * 4),
|
||||
@@ -239,7 +264,7 @@ export class EngineBridge {
|
||||
if (!this.initialized) return;
|
||||
|
||||
const startTime = performance.now();
|
||||
this.engine.render();
|
||||
this.getEngine().render();
|
||||
const endTime = performance.now();
|
||||
|
||||
// Update statistics | 更新统计信息
|
||||
@@ -265,9 +290,12 @@ export class EngineBridge {
|
||||
* @param id - Texture ID | 纹理ID
|
||||
* @param url - Image URL | 图片URL
|
||||
*/
|
||||
loadTexture(id: number, url: string): void {
|
||||
if (!this.initialized) return;
|
||||
this.engine.loadTexture(id, url);
|
||||
loadTexture(id: number, url: string): Promise<void> {
|
||||
if (!this.initialized) return Promise.resolve();
|
||||
this.getEngine().loadTexture(id, url);
|
||||
// Currently synchronous, but return Promise for interface compatibility
|
||||
// 目前是同步的,但返回Promise以兼容接口
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -276,12 +304,89 @@ export class EngineBridge {
|
||||
*
|
||||
* @param requests - Texture load requests | 纹理加载请求
|
||||
*/
|
||||
loadTextures(requests: TextureLoadRequest[]): void {
|
||||
async loadTextures(requests: Array<{ id: number; url: string }>): Promise<void> {
|
||||
for (const req of requests) {
|
||||
this.loadTexture(req.id, req.url);
|
||||
await this.loadTexture(req.id, req.url);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load texture by path, returning texture ID.
|
||||
* 按路径加载纹理,返回纹理ID。
|
||||
*
|
||||
* @param path - Image path/URL | 图片路径/URL
|
||||
* @returns Texture ID | 纹理ID
|
||||
*/
|
||||
loadTextureByPath(path: string): number {
|
||||
if (!this.initialized) return 0;
|
||||
return this.getEngine().loadTextureByPath(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get texture ID by path.
|
||||
* 按路径获取纹理ID。
|
||||
*
|
||||
* @param path - Image path | 图片路径
|
||||
* @returns Texture ID or undefined | 纹理ID或undefined
|
||||
*/
|
||||
getTextureIdByPath(path: string): number | undefined {
|
||||
if (!this.initialized) return undefined;
|
||||
return this.getEngine().getTextureIdByPath(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set path resolver for converting file paths to URLs.
|
||||
* 设置路径解析器用于将文件路径转换为URL。
|
||||
*
|
||||
* @param resolver - Function to resolve paths | 解析路径的函数
|
||||
*/
|
||||
setPathResolver(resolver: (path: string) => string): void {
|
||||
this.pathResolver = resolver;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or load texture by path.
|
||||
* 按路径获取或加载纹理。
|
||||
*
|
||||
* @param path - Image path/URL | 图片路径/URL
|
||||
* @returns Texture ID | 纹理ID
|
||||
*/
|
||||
getOrLoadTextureByPath(path: string): number {
|
||||
if (!this.initialized) return 0;
|
||||
|
||||
// Resolve path if resolver is set
|
||||
// 如果设置了解析器,则解析路径
|
||||
const resolvedPath = this.pathResolver ? this.pathResolver(path) : path;
|
||||
return this.getEngine().getOrLoadTextureByPath(resolvedPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unload texture from GPU.
|
||||
* 从GPU卸载纹理。
|
||||
*
|
||||
* @param id - Texture ID | 纹理ID
|
||||
*/
|
||||
unloadTexture(id: number): void {
|
||||
if (!this.initialized) return;
|
||||
// TODO: Implement in Rust engine
|
||||
// TODO: 在Rust引擎中实现
|
||||
console.warn('unloadTexture not yet implemented in engine');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get texture information.
|
||||
* 获取纹理信息。
|
||||
*
|
||||
* @param id - Texture ID | 纹理ID
|
||||
*/
|
||||
getTextureInfo(id: number): { width: number; height: number } | null {
|
||||
if (!this.initialized) return null;
|
||||
// TODO: Implement in Rust engine
|
||||
// TODO: 在Rust引擎中实现
|
||||
// Return default values for now / 暂时返回默认值
|
||||
return { width: 64, height: 64 };
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a key is pressed.
|
||||
* 检查按键是否按下。
|
||||
@@ -290,7 +395,7 @@ export class EngineBridge {
|
||||
*/
|
||||
isKeyDown(keyCode: string): boolean {
|
||||
if (!this.initialized) return false;
|
||||
return this.engine.isKeyDown(keyCode);
|
||||
return this.getEngine().isKeyDown(keyCode);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -299,7 +404,7 @@ export class EngineBridge {
|
||||
*/
|
||||
updateInput(): void {
|
||||
if (!this.initialized) return;
|
||||
this.engine.updateInput();
|
||||
this.getEngine().updateInput();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -319,11 +424,212 @@ export class EngineBridge {
|
||||
*/
|
||||
resize(width: number, height: number): void {
|
||||
if (!this.initialized) return;
|
||||
if (this.engine.resize) {
|
||||
this.engine.resize(width, height);
|
||||
const engine = this.getEngine();
|
||||
if (engine.resize) {
|
||||
engine.resize(width, height);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set camera position, zoom, and rotation.
|
||||
* 设置相机位置、缩放和旋转。
|
||||
*
|
||||
* @param config - Camera configuration | 相机配置
|
||||
*/
|
||||
setCamera(config: CameraConfig): void {
|
||||
if (!this.initialized) return;
|
||||
this.getEngine().setCamera(config.x, config.y, config.zoom, config.rotation);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get camera state.
|
||||
* 获取相机状态。
|
||||
*/
|
||||
getCamera(): CameraConfig {
|
||||
if (!this.initialized) {
|
||||
return { x: 0, y: 0, zoom: 1, rotation: 0 };
|
||||
}
|
||||
const state = this.getEngine().getCamera();
|
||||
return {
|
||||
x: state[0],
|
||||
y: state[1],
|
||||
zoom: state[2],
|
||||
rotation: state[3]
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Set grid visibility.
|
||||
* 设置网格可见性。
|
||||
*/
|
||||
setShowGrid(show: boolean): void {
|
||||
if (!this.initialized) return;
|
||||
this.getEngine().setShowGrid(show);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set clear color (background color).
|
||||
* 设置清除颜色(背景颜色)。
|
||||
*
|
||||
* @param r - Red component (0.0-1.0) | 红色分量
|
||||
* @param g - Green component (0.0-1.0) | 绿色分量
|
||||
* @param b - Blue component (0.0-1.0) | 蓝色分量
|
||||
* @param a - Alpha component (0.0-1.0) | 透明度分量
|
||||
*/
|
||||
setClearColor(r: number, g: number, b: number, a: number): void {
|
||||
if (!this.initialized) return;
|
||||
this.getEngine().setClearColor(r, g, b, a);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a rectangle gizmo outline.
|
||||
* 添加矩形Gizmo边框。
|
||||
*
|
||||
* @param x - Center X position | 中心X位置
|
||||
* @param y - Center Y position | 中心Y位置
|
||||
* @param width - Rectangle width | 矩形宽度
|
||||
* @param height - Rectangle height | 矩形高度
|
||||
* @param rotation - Rotation in radians | 旋转角度(弧度)
|
||||
* @param originX - Origin X (0-1) | 原点X (0-1)
|
||||
* @param originY - Origin Y (0-1) | 原点Y (0-1)
|
||||
* @param r - Red (0-1) | 红色
|
||||
* @param g - Green (0-1) | 绿色
|
||||
* @param b - Blue (0-1) | 蓝色
|
||||
* @param a - Alpha (0-1) | 透明度
|
||||
* @param showHandles - Whether to show transform handles | 是否显示变换手柄
|
||||
*/
|
||||
addGizmoRect(
|
||||
x: number,
|
||||
y: number,
|
||||
width: number,
|
||||
height: number,
|
||||
rotation: number,
|
||||
originX: number,
|
||||
originY: number,
|
||||
r: number,
|
||||
g: number,
|
||||
b: number,
|
||||
a: number,
|
||||
showHandles: boolean = true
|
||||
): void {
|
||||
if (!this.initialized) return;
|
||||
this.getEngine().addGizmoRect(x, y, width, height, rotation, originX, originY, r, g, b, a, showHandles);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set transform tool mode.
|
||||
* 设置变换工具模式。
|
||||
*
|
||||
* @param mode - 0=Select, 1=Move, 2=Rotate, 3=Scale
|
||||
*/
|
||||
setTransformMode(mode: number): void {
|
||||
if (!this.initialized) return;
|
||||
this.getEngine().setTransformMode(mode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set gizmo visibility.
|
||||
* 设置辅助工具可见性。
|
||||
*/
|
||||
setShowGizmos(show: boolean): void {
|
||||
if (!this.initialized) return;
|
||||
this.getEngine().setShowGizmos(show);
|
||||
}
|
||||
|
||||
// ===== Multi-viewport API =====
|
||||
// ===== 多视口 API =====
|
||||
|
||||
/**
|
||||
* Register a new viewport.
|
||||
* 注册新视口。
|
||||
*
|
||||
* @param id - Unique viewport identifier | 唯一视口标识符
|
||||
* @param canvasId - HTML canvas element ID | HTML canvas元素ID
|
||||
*/
|
||||
registerViewport(id: string, canvasId: string): void {
|
||||
if (!this.initialized) return;
|
||||
this.getEngine().registerViewport(id, canvasId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister a viewport.
|
||||
* 注销视口。
|
||||
*/
|
||||
unregisterViewport(id: string): void {
|
||||
if (!this.initialized) return;
|
||||
this.getEngine().unregisterViewport(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the active viewport.
|
||||
* 设置活动视口。
|
||||
*/
|
||||
setActiveViewport(id: string): boolean {
|
||||
if (!this.initialized) return false;
|
||||
return this.getEngine().setActiveViewport(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set camera for a specific viewport.
|
||||
* 为特定视口设置相机。
|
||||
*/
|
||||
setViewportCamera(viewportId: string, config: CameraConfig): void {
|
||||
if (!this.initialized) return;
|
||||
this.getEngine().setViewportCamera(viewportId, config.x, config.y, config.zoom, config.rotation);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get camera for a specific viewport.
|
||||
* 获取特定视口的相机。
|
||||
*/
|
||||
getViewportCamera(viewportId: string): CameraConfig | null {
|
||||
if (!this.initialized) return null;
|
||||
const state = this.getEngine().getViewportCamera(viewportId);
|
||||
if (!state) return null;
|
||||
return {
|
||||
x: state[0],
|
||||
y: state[1],
|
||||
zoom: state[2],
|
||||
rotation: state[3]
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Set viewport configuration.
|
||||
* 设置视口配置。
|
||||
*/
|
||||
setViewportConfig(viewportId: string, showGrid: boolean, showGizmos: boolean): void {
|
||||
if (!this.initialized) return;
|
||||
this.getEngine().setViewportConfig(viewportId, showGrid, showGizmos);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resize a specific viewport.
|
||||
* 调整特定视口大小。
|
||||
*/
|
||||
resizeViewport(viewportId: string, width: number, height: number): void {
|
||||
if (!this.initialized) return;
|
||||
this.getEngine().resizeViewport(viewportId, width, height);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render to a specific viewport.
|
||||
* 渲染到特定视口。
|
||||
*/
|
||||
renderToViewport(viewportId: string): void {
|
||||
if (!this.initialized) return;
|
||||
this.getEngine().renderToViewport(viewportId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all registered viewport IDs.
|
||||
* 获取所有已注册的视口ID。
|
||||
*/
|
||||
getViewportIds(): string[] {
|
||||
if (!this.initialized) return [];
|
||||
return this.getEngine().getViewportIds();
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispose the bridge and release resources.
|
||||
* 销毁桥接并释放资源。
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import { Entity, Component } from '@esengine/ecs-framework';
|
||||
import type { EngineBridge } from './EngineBridge';
|
||||
import { RenderBatcher } from './RenderBatcher';
|
||||
import { SpriteComponent } from '../components/SpriteComponent';
|
||||
import { SpriteComponent } from '@esengine/ecs-components';
|
||||
import type { SpriteRenderData } from '../types';
|
||||
|
||||
/**
|
||||
@@ -17,9 +17,9 @@ import type { SpriteRenderData } from '../types';
|
||||
* 你的变换组件应该实现此接口。
|
||||
*/
|
||||
export interface ITransformComponent {
|
||||
position: { x: number; y: number };
|
||||
rotation: number;
|
||||
scale: { x: number; y: number };
|
||||
position: { x: number; y: number; z?: number };
|
||||
rotation: number | { x: number; y: number; z: number };
|
||||
scale: { x: number; y: number; z?: number };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -94,17 +94,25 @@ export class SpriteRenderHelper {
|
||||
}
|
||||
}
|
||||
|
||||
// Handle rotation as number or Vector3 (use z for 2D)
|
||||
const rotation = typeof transform.rotation === 'number'
|
||||
? transform.rotation
|
||||
: transform.rotation.z;
|
||||
|
||||
// Convert hex color string to packed RGBA
|
||||
const color = this.hexToPackedColor(sprite.color, sprite.alpha);
|
||||
|
||||
const renderData: SpriteRenderData = {
|
||||
x: transform.position.x,
|
||||
y: transform.position.y,
|
||||
rotation: transform.rotation,
|
||||
rotation,
|
||||
scaleX: transform.scale.x,
|
||||
scaleY: transform.scale.y,
|
||||
originX: sprite.originX,
|
||||
originY: sprite.originY,
|
||||
textureId: sprite.textureId,
|
||||
uv,
|
||||
color: sprite.color
|
||||
color
|
||||
};
|
||||
|
||||
this.batcher.addSprite(renderData);
|
||||
@@ -137,4 +145,26 @@ export class SpriteRenderHelper {
|
||||
clear(): void {
|
||||
this.batcher.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert hex color string to packed RGBA.
|
||||
* 将十六进制颜色字符串转换为打包的RGBA。
|
||||
*/
|
||||
private hexToPackedColor(hex: string, alpha: number): number {
|
||||
let r = 255, g = 255, b = 255;
|
||||
if (hex.startsWith('#')) {
|
||||
const hexValue = hex.slice(1);
|
||||
if (hexValue.length === 3) {
|
||||
r = parseInt(hexValue[0] + hexValue[0], 16);
|
||||
g = parseInt(hexValue[1] + hexValue[1], 16);
|
||||
b = parseInt(hexValue[2] + hexValue[2], 16);
|
||||
} else if (hexValue.length === 6) {
|
||||
r = parseInt(hexValue.slice(0, 2), 16);
|
||||
g = parseInt(hexValue.slice(2, 4), 16);
|
||||
b = parseInt(hexValue.slice(4, 6), 16);
|
||||
}
|
||||
}
|
||||
const a = Math.round(alpha * 255);
|
||||
return ((a & 0xFF) << 24) | ((b & 0xFF) << 16) | ((g & 0xFF) << 8) | (r & 0xFF);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,5 +9,5 @@ export { EngineBridge, EngineBridgeConfig } from './core/EngineBridge';
|
||||
export { RenderBatcher } from './core/RenderBatcher';
|
||||
export { SpriteRenderHelper, ITransformComponent } from './core/SpriteRenderHelper';
|
||||
export { EngineRenderSystem, type TransformComponentType } from './systems/EngineRenderSystem';
|
||||
export { SpriteComponent } from './components/SpriteComponent';
|
||||
export { CameraSystem } from './systems/CameraSystem';
|
||||
export * from './types';
|
||||
|
||||
52
packages/ecs-engine-bindgen/src/systems/CameraSystem.ts
Normal file
52
packages/ecs-engine-bindgen/src/systems/CameraSystem.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* Camera System
|
||||
* 相机系统
|
||||
*/
|
||||
|
||||
import { EntitySystem, Matcher, Entity, ECSSystem } from '@esengine/ecs-framework';
|
||||
import { CameraComponent } from '@esengine/ecs-components';
|
||||
import type { EngineBridge } from '../core/EngineBridge';
|
||||
|
||||
@ECSSystem('Camera', { updateOrder: -100 })
|
||||
export class CameraSystem extends EntitySystem {
|
||||
private bridge: EngineBridge;
|
||||
private lastAppliedCameraId: number | null = null;
|
||||
|
||||
constructor(bridge: EngineBridge) {
|
||||
// Match entities with CameraComponent
|
||||
super(Matcher.empty().all(CameraComponent));
|
||||
this.bridge = bridge;
|
||||
}
|
||||
|
||||
protected override onBegin(): void {
|
||||
// Will process cameras in process()
|
||||
}
|
||||
|
||||
protected override process(entities: readonly Entity[]): void {
|
||||
// Use first enabled camera
|
||||
for (const entity of entities) {
|
||||
if (!entity.enabled) continue;
|
||||
|
||||
const camera = entity.getComponent(CameraComponent);
|
||||
if (!camera) continue;
|
||||
|
||||
// Only apply if camera changed
|
||||
if (this.lastAppliedCameraId !== entity.id) {
|
||||
this.applyCamera(camera);
|
||||
this.lastAppliedCameraId = entity.id;
|
||||
}
|
||||
|
||||
// Only use first active camera
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private applyCamera(camera: CameraComponent): void {
|
||||
// Apply background color
|
||||
const bgColor = camera.backgroundColor || '#000000';
|
||||
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.bridge.setClearColor(r, g, b, 1.0);
|
||||
}
|
||||
}
|
||||
@@ -3,10 +3,10 @@
|
||||
* 用于ECS的引擎渲染系统。
|
||||
*/
|
||||
|
||||
import { EntitySystem, Matcher, Entity, ComponentType, ECSSystem, Component } from '@esengine/ecs-framework';
|
||||
import { EntitySystem, Matcher, Entity, ComponentType, ECSSystem, Component, Core } from '@esengine/ecs-framework';
|
||||
import { SpriteComponent, CameraComponent, TransformComponent } from '@esengine/ecs-components';
|
||||
import type { EngineBridge } from '../core/EngineBridge';
|
||||
import { RenderBatcher } from '../core/RenderBatcher';
|
||||
import { SpriteComponent } from '../components/SpriteComponent';
|
||||
import type { SpriteRenderData } from '../types';
|
||||
import type { ITransformComponent } from '../core/SpriteRenderHelper';
|
||||
|
||||
@@ -47,6 +47,13 @@ export class EngineRenderSystem extends EntitySystem {
|
||||
private bridge: EngineBridge;
|
||||
private batcher: RenderBatcher;
|
||||
private transformType: TransformComponentType;
|
||||
private showGizmos = true;
|
||||
private selectedEntityIds: Set<number> = new Set();
|
||||
private transformMode: 'select' | 'move' | 'rotate' | 'scale' = 'select';
|
||||
|
||||
// Reusable map to avoid allocation per frame
|
||||
// 可重用的映射以避免每帧分配
|
||||
private entityRenderMap: Map<number, SpriteRenderData> = new Map();
|
||||
|
||||
/**
|
||||
* Create a new engine render system.
|
||||
@@ -78,12 +85,13 @@ export class EngineRenderSystem extends EntitySystem {
|
||||
* Called before processing entities.
|
||||
* 处理实体之前调用。
|
||||
*/
|
||||
protected begin(): void {
|
||||
protected override onBegin(): void {
|
||||
|
||||
// Clear the batch | 清空批处理
|
||||
this.batcher.clear();
|
||||
|
||||
// Clear screen | 清屏
|
||||
this.bridge.clear(0, 0, 0, 1);
|
||||
// Clear screen with dark background | 用深色背景清屏
|
||||
this.bridge.clear(0.1, 0.1, 0.12, 1);
|
||||
|
||||
// Update input | 更新输入
|
||||
this.bridge.updateInput();
|
||||
@@ -95,19 +103,22 @@ export class EngineRenderSystem extends EntitySystem {
|
||||
*
|
||||
* @param entities - Entities to process | 要处理的实体
|
||||
*/
|
||||
protected process(entities: readonly Entity[]): void {
|
||||
protected override process(entities: readonly Entity[]): void {
|
||||
// Clear and reuse map for gizmo drawing
|
||||
// 清空并重用映射用于绘制gizmo
|
||||
this.entityRenderMap.clear();
|
||||
|
||||
for (const entity of entities) {
|
||||
const sprite = entity.getComponent(SpriteComponent);
|
||||
const transform = entity.getComponent(this.transformType) as unknown as ITransformComponent | null;
|
||||
|
||||
if (!sprite || !transform || !sprite.visible) {
|
||||
if (!sprite || !transform) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Calculate UV with flip | 计算带翻转的UV
|
||||
let uv = sprite.uv;
|
||||
const uv: [number, number, number, number] = [0, 0, 1, 1];
|
||||
if (sprite.flipX || sprite.flipY) {
|
||||
uv = [...sprite.uv] as [number, number, number, number];
|
||||
if (sprite.flipX) {
|
||||
[uv[0], uv[2]] = [uv[2], uv[0]];
|
||||
}
|
||||
@@ -116,35 +127,213 @@ export class EngineRenderSystem extends EntitySystem {
|
||||
}
|
||||
}
|
||||
|
||||
// Handle rotation as number or Vector3 (use z for 2D)
|
||||
const rotation = typeof transform.rotation === 'number'
|
||||
? transform.rotation
|
||||
: transform.rotation.z;
|
||||
|
||||
// Convert hex color string to packed RGBA | 将十六进制颜色字符串转换为打包的RGBA
|
||||
const color = this.hexToPackedColor(sprite.color, sprite.alpha);
|
||||
|
||||
// Get texture ID from sprite component
|
||||
// 从精灵组件获取纹理ID
|
||||
// Use Rust engine's path-based texture loading for automatic caching
|
||||
// 使用Rust引擎的基于路径的纹理加载实现自动缓存
|
||||
let textureId = 0;
|
||||
if (sprite.texture) {
|
||||
textureId = this.bridge.getOrLoadTextureByPath(sprite.texture);
|
||||
} else {
|
||||
// Debug: sprite has no texture
|
||||
console.warn(`[EngineRenderSystem] Entity ${entity.id} has no texture`);
|
||||
}
|
||||
|
||||
// Pass actual display dimensions (sprite size * transform scale)
|
||||
// 传递实际显示尺寸(sprite尺寸 * 变换缩放)
|
||||
const renderData: SpriteRenderData = {
|
||||
x: transform.position.x,
|
||||
y: transform.position.y,
|
||||
rotation: transform.rotation,
|
||||
scaleX: transform.scale.x,
|
||||
scaleY: transform.scale.y,
|
||||
originX: sprite.originX,
|
||||
originY: sprite.originY,
|
||||
textureId: sprite.textureId,
|
||||
rotation,
|
||||
scaleX: sprite.width * transform.scale.x,
|
||||
scaleY: sprite.height * transform.scale.y,
|
||||
originX: sprite.anchorX,
|
||||
originY: sprite.anchorY,
|
||||
textureId,
|
||||
uv,
|
||||
color: sprite.color
|
||||
color
|
||||
};
|
||||
|
||||
this.batcher.addSprite(renderData);
|
||||
this.entityRenderMap.set(entity.id, renderData);
|
||||
}
|
||||
|
||||
// Submit batch and render at the end of process | 在process结束时提交批处理并渲染
|
||||
if (!this.batcher.isEmpty) {
|
||||
const sprites = this.batcher.getSprites();
|
||||
this.bridge.submitSprites(sprites);
|
||||
}
|
||||
|
||||
// Draw gizmos for selected entities (always, even if no sprites)
|
||||
// 为选中的实体绘制Gizmo(始终绘制,即使没有精灵)
|
||||
if (this.showGizmos && this.selectedEntityIds.size > 0) {
|
||||
for (const entityId of this.selectedEntityIds) {
|
||||
const renderData = this.entityRenderMap.get(entityId);
|
||||
if (renderData) {
|
||||
this.bridge.addGizmoRect(
|
||||
renderData.x,
|
||||
renderData.y,
|
||||
renderData.scaleX,
|
||||
renderData.scaleY,
|
||||
renderData.rotation,
|
||||
renderData.originX,
|
||||
renderData.originY,
|
||||
0.0, 1.0, 0.5, 1.0, // Green color | 绿色
|
||||
true // Show transform handles for selection gizmo
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Draw camera frustum gizmos
|
||||
// 绘制相机视锥体 gizmo
|
||||
if (this.showGizmos) {
|
||||
this.drawCameraFrustums();
|
||||
}
|
||||
|
||||
this.bridge.render();
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw camera frustum gizmos for all cameras in scene.
|
||||
* 为场景中所有相机绘制视锥体 gizmo。
|
||||
*/
|
||||
private drawCameraFrustums(): void {
|
||||
const scene = Core.scene;
|
||||
if (!scene) return;
|
||||
|
||||
const cameraEntities = scene.entities.findEntitiesWithComponent(CameraComponent);
|
||||
|
||||
for (const entity of cameraEntities) {
|
||||
const camera = entity.getComponent(CameraComponent);
|
||||
const transform = entity.getComponent(TransformComponent);
|
||||
|
||||
if (!camera || !transform) continue;
|
||||
|
||||
// Calculate frustum size based on canvas size and orthographicSize
|
||||
// 根据 canvas 尺寸和 orthographicSize 计算视锥体大小
|
||||
// At runtime, zoom = 1 / orthographicSize
|
||||
// So visible area = canvas size * orthographicSize
|
||||
const canvas = document.getElementById('viewport-canvas') as HTMLCanvasElement;
|
||||
if (!canvas) continue;
|
||||
|
||||
// The actual visible world units when running
|
||||
// 运行时实际可见的世界单位
|
||||
const zoom = camera.orthographicSize > 0 ? 1 / camera.orthographicSize : 1;
|
||||
const width = canvas.width / zoom;
|
||||
const height = canvas.height / zoom;
|
||||
|
||||
// Handle rotation
|
||||
const rotation = typeof transform.rotation === 'number'
|
||||
? transform.rotation
|
||||
: transform.rotation.z;
|
||||
|
||||
// Draw frustum rectangle (white color for camera)
|
||||
// 绘制视锥体矩形(相机用白色)
|
||||
this.bridge.addGizmoRect(
|
||||
transform.position.x,
|
||||
transform.position.y,
|
||||
width,
|
||||
height,
|
||||
rotation,
|
||||
0.5, // origin center
|
||||
0.5,
|
||||
1.0, 1.0, 1.0, 0.8, // White color with some transparency
|
||||
false // Don't show transform handles for camera frustum
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called after processing entities.
|
||||
* 处理实体之后调用。
|
||||
* Set gizmo visibility.
|
||||
* 设置Gizmo可见性。
|
||||
*/
|
||||
protected end(): void {
|
||||
// Submit batch and render | 提交批处理并渲染
|
||||
if (!this.batcher.isEmpty) {
|
||||
this.bridge.submitSprites(this.batcher.getSprites());
|
||||
}
|
||||
this.bridge.render();
|
||||
setShowGizmos(show: boolean): void {
|
||||
this.showGizmos = show;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get gizmo visibility.
|
||||
* 获取Gizmo可见性。
|
||||
*/
|
||||
getShowGizmos(): boolean {
|
||||
return this.showGizmos;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set selected entity IDs.
|
||||
* 设置选中的实体ID。
|
||||
*/
|
||||
setSelectedEntityIds(ids: number[]): void {
|
||||
this.selectedEntityIds = new Set(ids);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get selected entity IDs.
|
||||
* 获取选中的实体ID。
|
||||
*/
|
||||
getSelectedEntityIds(): number[] {
|
||||
return Array.from(this.selectedEntityIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set transform tool mode.
|
||||
* 设置变换工具模式。
|
||||
*/
|
||||
setTransformMode(mode: 'select' | 'move' | 'rotate' | 'scale'): void {
|
||||
this.transformMode = mode;
|
||||
|
||||
// Convert string mode to u8 for Rust engine
|
||||
const modeMap: Record<string, number> = {
|
||||
'select': 0,
|
||||
'move': 1,
|
||||
'rotate': 2,
|
||||
'scale': 3
|
||||
};
|
||||
this.bridge.setTransformMode(modeMap[mode]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get transform tool mode.
|
||||
* 获取变换工具模式。
|
||||
*/
|
||||
getTransformMode(): 'select' | 'move' | 'rotate' | 'scale' {
|
||||
return this.transformMode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert hex color string to packed RGBA.
|
||||
* 将十六进制颜色字符串转换为打包的RGBA。
|
||||
*/
|
||||
private hexToPackedColor(hex: string, alpha: number): number {
|
||||
// Parse hex color like "#ffffff" or "#fff"
|
||||
let r = 255, g = 255, b = 255;
|
||||
if (hex.startsWith('#')) {
|
||||
const hexValue = hex.slice(1);
|
||||
if (hexValue.length === 3) {
|
||||
r = parseInt(hexValue[0] + hexValue[0], 16);
|
||||
g = parseInt(hexValue[1] + hexValue[1], 16);
|
||||
b = parseInt(hexValue[2] + hexValue[2], 16);
|
||||
} else if (hexValue.length === 6) {
|
||||
r = parseInt(hexValue.slice(0, 2), 16);
|
||||
g = parseInt(hexValue.slice(2, 4), 16);
|
||||
b = parseInt(hexValue.slice(4, 6), 16);
|
||||
}
|
||||
}
|
||||
const a = Math.round(alpha * 255);
|
||||
// Pack as 0xAABBGGRR for WebGL
|
||||
return ((a & 0xFF) << 24) | ((b & 0xFF) << 16) | ((g & 0xFF) << 8) | (r & 0xFF);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the number of sprites rendered.
|
||||
* 获取渲染的精灵数量。
|
||||
|
||||
Reference in New Issue
Block a user