feat: UI输入框IME支持和编辑器Inspector重构 (#310)

UI系统改进:
- 添加 IMEHelper 支持中文/日文/韩文输入法
- UIInputFieldComponent 添加组合输入状态管理
- UIInputSystem 添加 IME 事件处理
- UIInputFieldRenderSystem 优化渲染逻辑
- UIRenderCollector 增强纹理处理

引擎改进:
- EngineBridge 添加新的渲染接口
- EngineRenderSystem 优化渲染流程
- Rust 引擎添加新的渲染功能

编辑器改进:
- 新增模块化 Inspector 组件架构
- EntityRefField 增强实体引用选择
- 优化 FlexLayoutDock 和 SceneHierarchy 样式
- 添加国际化文本
This commit is contained in:
YHH
2025-12-19 15:45:14 +08:00
committed by GitHub
parent 536c4c5593
commit ecdb8f2021
46 changed files with 5825 additions and 257 deletions

View File

@@ -293,6 +293,32 @@ export class EngineBridge implements ITextureEngineBridge {
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.
* 加载纹理。

View File

@@ -51,6 +51,13 @@ export interface ProviderRenderData {
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 };
}
/**
@@ -615,31 +622,97 @@ export class EngineRenderSystem extends EntitySystem {
this.bridge.pushScreenSpaceMode(canvasWidth, canvasHeight);
// Clear batcher for screen space content
// 清空批处理器用于屏幕空间内容
this.batcher.clear();
// Group sprites by clipRect (in render order)
// 按 clipRect 分组 sprites按渲染顺序
type ClipGroup = {
clipRect: { x: number; y: number; width: number; height: number } | undefined;
sprites: SpriteRenderData[];
};
// Submit screen space sprites
// 提交屏幕空间 sprites
const clipGroups: ClipGroup[] = [];
let currentClipRect: { x: number; y: number; width: number; height: number } | undefined = undefined;
let currentGroup: SpriteRenderData[] = [];
// Helper to check if two clip rects are equal
// 辅助函数检查两个裁剪矩形是否相等
const clipRectsEqual = (
a: { x: number; y: number; width: number; height: number } | undefined,
b: { x: number; y: number; width: number; height: number } | undefined
): boolean => {
if (a === b) return true;
if (!a || !b) return false;
return a.x === b.x && a.y === b.y && a.width === b.width && a.height === b.height;
};
// Group sprites by consecutive clipRect
// 按连续的 clipRect 分组 sprites
for (const item of screenSpaceItems) {
for (const sprite of item.sprites) {
this.batcher.addSprite(sprite);
const spriteClipRect = sprite.clipRect;
if (!clipRectsEqual(spriteClipRect, currentClipRect)) {
// Save current group if not empty
// 如果当前组不为空则保存
if (currentGroup.length > 0) {
clipGroups.push({ clipRect: currentClipRect, sprites: currentGroup });
}
// Start new group
// 开始新组
currentClipRect = spriteClipRect;
currentGroup = [sprite];
} else {
currentGroup.push(sprite);
}
}
}
if (!this.batcher.isEmpty) {
const sprites = this.batcher.getSprites();
// Apply material overrides before rendering
// 在渲染前应用材质覆盖
this.applySpriteMaterialOverrides(sprites);
this.bridge.submitSprites(sprites);
// Render overlay (without clearing screen)
// 渲染叠加层(不清屏)
this.bridge.renderOverlay();
// Don't forget the last group
// 别忘了最后一组
if (currentGroup.length > 0) {
clipGroups.push({ clipRect: currentClipRect, sprites: currentGroup });
}
// Render each clip group
// 渲染每个裁剪组
for (const group of clipGroups) {
// Set or clear scissor rect
// 设置或清除裁剪矩形
if (group.clipRect) {
this.bridge.setScissorRect(
group.clipRect.x,
group.clipRect.y,
group.clipRect.width,
group.clipRect.height
);
} else {
this.bridge.clearScissorRect();
}
// Clear batcher and add sprites
// 清空批处理器并添加 sprites
this.batcher.clear();
for (const sprite of group.sprites) {
this.batcher.addSprite(sprite);
}
if (!this.batcher.isEmpty) {
const sprites = this.batcher.getSprites();
// Apply material overrides before rendering
// 在渲染前应用材质覆盖
this.applySpriteMaterialOverrides(sprites);
this.bridge.submitSprites(sprites);
// Render overlay (without clearing screen)
// 渲染叠加层(不清屏)
this.bridge.renderOverlay();
}
}
// Clear scissor rect after all groups
// 所有组渲染完后清除裁剪矩形
this.bridge.clearScissorRect();
// Restore world space camera
// 恢复世界空间相机
this.bridge.popScreenSpaceMode();
@@ -802,6 +875,7 @@ export class EngineRenderSystem extends EntitySystem {
// 检查材质数据
const hasMaterialIds = data.materialIds && data.materialIds.length > 0;
const hasMaterialOverrides = data.materialOverrides && Object.keys(data.materialOverrides).length > 0;
const hasClipRect = !!data.clipRect;
const sprites: SpriteRenderData[] = [];
for (let i = 0; i < data.tileCount; i++) {
@@ -836,6 +910,11 @@ export class EngineRenderSystem extends EntitySystem {
if (hasMaterialOverrides) {
renderData.materialOverrides = data.materialOverrides;
}
// Add clipRect if present (all sprites in batch share same clipRect)
// 如果存在 clipRect添加它批次中所有精灵共享相同 clipRect
if (hasClipRect) {
renderData.clipRect = data.clipRect;
}
sprites.push(renderData);
}

View File

@@ -52,6 +52,13 @@ 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 };
}
/**

View File

@@ -217,6 +217,20 @@ 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边框。
@@ -269,6 +283,11 @@ export class GameEngine {
* 请谨慎使用,因为所有纹理引用都将变得无效。
*/
clearAllTextures(): void;
/**
* Clear scissor rect (disable clipping).
* 清除裁剪矩形(禁用裁剪)。
*/
clearScissorRect(): void;
/**
* Render to a specific viewport.
* 渲染到特定视口。
@@ -489,6 +508,7 @@ export interface InitOutput {
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_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];
@@ -532,6 +552,7 @@ 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_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;