Files
esengine/packages/fairygui-editor/src/FGUIEditorModule.ts
YHH a1e1189f9d feat(fairygui): FairyGUI 完整集成 (#314)
* feat(fairygui): FairyGUI ECS 集成核心架构

实现 FairyGUI 的 ECS 原生集成,完全替代旧 UI 系统:

核心类:
- GObject: UI 对象基类,支持变换、可见性、关联、齿轮
- GComponent: 容器组件,管理子对象和控制器
- GRoot: 根容器,管理焦点、弹窗、输入分发
- GGroup: 组容器,支持水平/垂直布局

抽象层:
- DisplayObject: 显示对象基类
- EventDispatcher: 事件分发
- Timer: 计时器
- Stage: 舞台,管理输入和缩放

布局系统:
- Relations: 约束关联管理
- RelationItem: 24 种关联类型

基础设施:
- Controller: 状态控制器
- Transition: 过渡动画
- ScrollPane: 滚动面板
- UIPackage: 包管理
- ByteBuffer: 二进制解析

* refactor(ui): 删除旧 UI 系统,使用 FairyGUI 替代

* feat(fairygui): 实现 UI 控件

- 添加显示类:Image、TextField、Graph
- 添加基础控件:GImage、GTextField、GGraph
- 添加交互控件:GButton、GProgressBar、GSlider
- 更新 IRenderCollector 支持 Graph 渲染
- 扩展 Controller 添加 selectedPageId
- 添加 STATE_CHANGED 事件类型

* feat(fairygui): 现代化架构重构

- 增强 EventDispatcher 支持类型安全、优先级和传播控制
- 添加 PropertyBinding 响应式属性绑定系统
- 添加 ServiceContainer 依赖注入容器
- 添加 UIConfig 全局配置系统
- 添加 UIObjectFactory 对象工厂
- 实现 RenderBridge 渲染桥接层
- 实现 Canvas2DBackend 作为默认渲染后端
- 扩展 IRenderCollector 支持更多图元类型

* feat(fairygui): 九宫格渲染和资源加载修复

- 修复 FGUIUpdateSystem 支持路径和 GUID 两种加载方式
- 修复 GTextInput 同时设置 _displayObject 和 _textField
- 实现九宫格渲染展开为 9 个子图元
- 添加 sourceWidth/sourceHeight 用于九宫格计算
- 添加 DOMTextRenderer 文本渲染层(临时方案)

* fix(fairygui): 修复 GGraph 颜色读取

* feat(fairygui): 虚拟节点 Inspector 和文本渲染支持

* fix(fairygui): 编辑器状态刷新和遗留引用修复

- 修复切换 FGUI 包后组件列表未刷新问题
- 修复切换组件后 viewport 未清理旧内容问题
- 修复虚拟节点在包加载后未刷新问题
- 重构为事件驱动架构,移除轮询机制
- 修复 @esengine/ui 遗留引用,统一使用 @esengine/fairygui

* fix: 移除 tsconfig 中的 @esengine/ui 引用
2025-12-22 10:52:54 +08:00

748 lines
24 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* FGUIEditorModule
*
* Editor module for FairyGUI integration.
* Registers components, inspectors, and entity templates.
*
* FairyGUI 编辑器模块,注册组件、检视器和实体模板
*/
import type { ServiceContainer, Entity } from '@esengine/ecs-framework';
import { Core } from '@esengine/ecs-framework';
import type { IEditorModuleLoader, EntityCreationTemplate } from '@esengine/editor-core';
import {
EntityStoreService,
MessageHub,
EditorComponentRegistry,
ComponentInspectorRegistry,
GizmoRegistry,
GizmoColors,
VirtualNodeRegistry
} from '@esengine/editor-core';
import type { IGizmoRenderData, IRectGizmoData, GizmoColor, IVirtualNode } from '@esengine/editor-core';
import { TransformComponent } from '@esengine/engine-core';
import {
FGUIComponent,
GComponent,
GObject,
Stage,
GGraph,
GImage,
GTextField,
GLoader,
GButton,
GList,
GProgressBar,
GSlider
} from '@esengine/fairygui';
import { fguiComponentInspector } from './inspectors';
/**
* Gizmo colors for FGUI nodes
* FGUI 节点的 Gizmo 颜色
*/
const FGUIGizmoColors = {
/** Root component bounds | 根组件边界 */
root: { r: 0.2, g: 0.6, b: 1.0, a: 0.8 } as GizmoColor,
/** Child element bounds (selected virtual node) | 子元素边界(选中的虚拟节点) */
childSelected: { r: 1.0, g: 0.8, b: 0.2, a: 0.8 } as GizmoColor,
/** Child element bounds (unselected) | 子元素边界(未选中) */
childUnselected: { r: 1.0, g: 0.8, b: 0.2, a: 0.15 } as GizmoColor
};
/**
* Collect gizmo data from FGUI node tree
* 从 FGUI 节点树收集 Gizmo 数据
*
* Uses the same coordinate conversion as FGUIRenderDataProvider:
* - FGUI: top-left origin, Y-down
* - Engine: center origin, Y-up
* - Conversion: engineX = fguiX - halfWidth, engineY = halfHeight - fguiY
*
* 使用与 FGUIRenderDataProvider 相同的坐标转换:
* - FGUI左上角为原点Y 向下
* - 引擎中心为原点Y 向上
* - 转换公式engineX = fguiX - halfWidth, engineY = halfHeight - fguiY
*
* @param obj The GObject to collect from | 要收集的 GObject
* @param halfWidth Half of Stage.designWidth | Stage.designWidth 的一半
* @param halfHeight Half of Stage.designHeight | Stage.designHeight 的一半
* @param gizmos Array to add gizmos to | 添加 gizmos 的数组
* @param entityId The entity ID for virtual node selection check | 用于检查虚拟节点选中的实体 ID
* @param selectedVirtualNodeId Currently selected virtual node ID | 当前选中的虚拟节点 ID
* @param parentPath Path prefix for virtual node ID generation | 虚拟节点 ID 生成的路径前缀
*/
function collectFGUIGizmos(
obj: GObject,
halfWidth: number,
halfHeight: number,
gizmos: IGizmoRenderData[],
entityId: number,
selectedVirtualNodeId: string | null,
parentPath: string
): void {
// Skip invisible objects
if (!obj.visible) return;
// Generate virtual node ID (same logic as collectFGUIVirtualNodes)
const nodePath = parentPath ? `${parentPath}/${obj.name || obj.id}` : (obj.name || obj.id);
// Use localToGlobal to get the global position in FGUI coordinate system
// This handles all parent transforms correctly
// 使用 localToGlobal 获取 FGUI 坐标系中的全局位置
// 这正确处理了所有父级变换
const globalPos = obj.localToGlobal(0, 0);
const fguiX = globalPos.x;
const fguiY = globalPos.y;
// Convert from FGUI coordinates to engine coordinates
// Same formula as FGUIRenderDataProvider
// 从 FGUI 坐标转换为引擎坐标,与 FGUIRenderDataProvider 使用相同公式
// Engine position is the top-left corner converted to engine coords
const engineX = fguiX - halfWidth;
const engineY = halfHeight - fguiY;
// For gizmo rect, we need the center position
// Engine Y increases upward, so center is at (engineX + width/2, engineY - height/2)
// 对于 gizmo 矩形,我们需要中心位置
// 引擎 Y 向上递增,所以中心在 (engineX + width/2, engineY - height/2)
const centerX = engineX + obj.width / 2;
const centerY = engineY - obj.height / 2;
// Determine color based on selection state
// 根据选中状态确定颜色
const isSelected = nodePath === selectedVirtualNodeId;
const color = isSelected ? FGUIGizmoColors.childSelected : FGUIGizmoColors.childUnselected;
// Add rect gizmo for this object
const rectGizmo: IRectGizmoData = {
type: 'rect',
x: centerX,
y: centerY,
width: obj.width,
height: obj.height,
rotation: 0,
originX: 0.5,
originY: 0.5,
color,
showHandles: isSelected,
virtualNodeId: nodePath
};
gizmos.push(rectGizmo);
// If this is a container, recurse into children
if (obj instanceof GComponent) {
for (let i = 0; i < obj.numChildren; i++) {
const child = obj.getChildAt(i);
collectFGUIGizmos(child, halfWidth, halfHeight, gizmos, entityId, selectedVirtualNodeId, nodePath);
}
}
}
/**
* Gizmo provider for FGUIComponent
* FGUIComponent 的 Gizmo 提供者
*
* Generates rect gizmos for all visible FGUI nodes.
* Uses the same coordinate conversion as FGUIRenderDataProvider.
* 为所有可见的 FGUI 节点生成矩形 gizmos。
* 使用与 FGUIRenderDataProvider 相同的坐标转换。
*/
function fguiGizmoProvider(
component: FGUIComponent,
entity: Entity,
isSelected: boolean
): IGizmoRenderData[] {
const gizmos: IGizmoRenderData[] = [];
// Get the root GObject
const root = component.root;
if (!root) return gizmos;
// Get Stage design size for coordinate conversion
// Use the same values as FGUIRenderDataProvider
// 获取 Stage 设计尺寸用于坐标转换,与 FGUIRenderDataProvider 使用相同的值
const stage = Stage.inst;
const halfWidth = stage.designWidth / 2;
const halfHeight = stage.designHeight / 2;
// Root gizmo - root is at (0, 0) in FGUI coords
// In engine coords: center is at (-halfWidth + width/2, halfHeight - height/2)
// 根 Gizmo - 根节点在 FGUI 坐标 (0, 0)
// 在引擎坐标中:中心在 (-halfWidth + width/2, halfHeight - height/2)
const rootCenterX = -halfWidth + root.width / 2;
const rootCenterY = halfHeight - root.height / 2;
const rootGizmo: IRectGizmoData = {
type: 'rect',
x: rootCenterX,
y: rootCenterY,
width: root.width,
height: root.height,
rotation: 0,
originX: 0.5,
originY: 0.5,
color: isSelected ? FGUIGizmoColors.root : { ...FGUIGizmoColors.root, a: 0.4 },
showHandles: isSelected
};
gizmos.push(rootGizmo);
// Collect child gizmos only when selected (performance optimization)
if (isSelected && component.component) {
const comp = component.component;
// Get currently selected virtual node for this entity
// 获取此实体当前选中的虚拟节点
const selectedInfo = VirtualNodeRegistry.getSelectedVirtualNode();
const selectedVirtualNodeId = (selectedInfo && selectedInfo.entityId === entity.id)
? selectedInfo.virtualNodeId
: null;
// First add gizmo for the component itself
// 首先为组件本身添加 gizmo
collectFGUIGizmos(comp, halfWidth, halfHeight, gizmos, entity.id, selectedVirtualNodeId, '');
}
return gizmos;
}
/**
* Get the type name of a GObject
* 获取 GObject 的类型名称
*/
function getGObjectTypeName(obj: GObject): string {
// Use constructor name as type
const name = obj.constructor.name;
// Remove 'G' prefix for cleaner display
if (name.startsWith('G') && name.length > 1) {
return name.slice(1);
}
return name;
}
/**
* Graph type enum to string mapping
* 图形类型枚举到字符串的映射
*/
const GraphTypeNames: Record<number, string> = {
0: 'Empty',
1: 'Rect',
2: 'Ellipse',
3: 'Polygon',
4: 'RegularPolygon'
};
/**
* Flip type enum to string mapping
* 翻转类型枚举到字符串的映射
*/
const FlipTypeNames: Record<number, string> = {
0: 'None',
1: 'Horizontal',
2: 'Vertical',
3: 'Both'
};
/**
* Fill method enum to string mapping
* 填充方法枚举到字符串的映射
*/
const FillMethodNames: Record<number, string> = {
0: 'None',
1: 'Horizontal',
2: 'Vertical',
3: 'Radial90',
4: 'Radial180',
5: 'Radial360'
};
/**
* Align type enum to string mapping
* 对齐类型枚举到字符串的映射
*/
const AlignTypeNames: Record<number, string> = {
0: 'Left',
1: 'Center',
2: 'Right'
};
/**
* Vertical align type enum to string mapping
* 垂直对齐类型枚举到字符串的映射
*/
const VertAlignTypeNames: Record<number, string> = {
0: 'Top',
1: 'Middle',
2: 'Bottom'
};
/**
* Loader fill type enum to string mapping
* 加载器填充类型枚举到字符串的映射
*/
const LoaderFillTypeNames: Record<number, string> = {
0: 'None',
1: 'Scale',
2: 'ScaleMatchHeight',
3: 'ScaleMatchWidth',
4: 'ScaleFree',
5: 'ScaleNoBorder'
};
/**
* Button mode enum to string mapping
* 按钮模式枚举到字符串的映射
*/
const ButtonModeNames: Record<number, string> = {
0: 'Common',
1: 'Check',
2: 'Radio'
};
/**
* Auto size type enum to string mapping
* 自动尺寸类型枚举到字符串的映射
*/
const AutoSizeTypeNames: Record<number, string> = {
0: 'None',
1: 'Both',
2: 'Height',
3: 'Shrink',
4: 'Ellipsis'
};
/**
* Extract type-specific properties from a GObject
* 从 GObject 提取类型特定的属性
*/
function extractTypeSpecificData(obj: GObject): Record<string, unknown> {
const data: Record<string, unknown> = {};
// GGraph specific properties
if (obj instanceof GGraph) {
data.graphType = GraphTypeNames[obj.type] || obj.type;
// Use public getters where available, fall back to private fields
data.lineColor = obj.lineColor;
data.fillColor = obj.fillColor;
// Access private fields via type assertion for properties without public getters
const graph = obj as unknown as {
_lineSize: number;
_cornerRadius: number[] | null;
_sides: number;
_startAngle: number;
};
data.lineSize = graph._lineSize;
if (graph._cornerRadius) {
data.cornerRadius = graph._cornerRadius.join(', ');
}
if (obj.type === 4) { // RegularPolygon
data.sides = graph._sides;
data.startAngle = graph._startAngle;
}
}
// GImage specific properties
if (obj instanceof GImage) {
data.color = obj.color;
data.flip = FlipTypeNames[obj.flip] || obj.flip;
data.fillMethod = FillMethodNames[obj.fillMethod] || obj.fillMethod;
if (obj.fillMethod !== 0) {
data.fillOrigin = obj.fillOrigin;
data.fillClockwise = obj.fillClockwise;
data.fillAmount = obj.fillAmount;
}
}
// GTextField specific properties
if (obj instanceof GTextField) {
data.text = obj.text;
data.font = obj.font;
data.fontSize = obj.fontSize;
data.color = obj.color;
data.align = AlignTypeNames[obj.align] || obj.align;
data.valign = VertAlignTypeNames[obj.valign] || obj.valign;
data.leading = obj.leading;
data.letterSpacing = obj.letterSpacing;
data.bold = obj.bold;
data.italic = obj.italic;
data.underline = obj.underline;
data.singleLine = obj.singleLine;
data.autoSize = AutoSizeTypeNames[obj.autoSize] || obj.autoSize;
if (obj.stroke > 0) {
data.stroke = obj.stroke;
data.strokeColor = obj.strokeColor;
}
}
// GLoader specific properties
if (obj instanceof GLoader) {
data.url = obj.url;
data.align = AlignTypeNames[obj.align] || obj.align;
data.verticalAlign = VertAlignTypeNames[obj.verticalAlign] || obj.verticalAlign;
data.fill = LoaderFillTypeNames[obj.fill] || obj.fill;
data.shrinkOnly = obj.shrinkOnly;
data.autoSize = obj.autoSize;
data.color = obj.color;
data.fillMethod = FillMethodNames[obj.fillMethod] || obj.fillMethod;
if (obj.fillMethod !== 0) {
data.fillOrigin = obj.fillOrigin;
data.fillClockwise = obj.fillClockwise;
data.fillAmount = obj.fillAmount;
}
}
// GButton specific properties
if (obj instanceof GButton) {
data.title = obj.title;
data.icon = obj.icon;
data.mode = ButtonModeNames[obj.mode] || obj.mode;
data.selected = obj.selected;
data.titleColor = obj.titleColor;
data.titleFontSize = obj.titleFontSize;
if (obj.selectedTitle) {
data.selectedTitle = obj.selectedTitle;
}
if (obj.selectedIcon) {
data.selectedIcon = obj.selectedIcon;
}
}
// GList specific properties
if (obj instanceof GList) {
data.defaultItem = obj.defaultItem;
data.itemCount = obj.numItems;
data.selectedIndex = obj.selectedIndex;
data.scrollPane = obj.scrollPane ? 'Yes' : 'No';
}
// GProgressBar specific properties
if (obj instanceof GProgressBar) {
data.value = obj.value;
data.max = obj.max;
}
// GSlider specific properties
if (obj instanceof GSlider) {
data.value = obj.value;
data.max = obj.max;
}
// GComponent specific properties (for all components)
if (obj instanceof GComponent) {
data.numChildren = obj.numChildren;
data.numControllers = obj.numControllers;
// Access private _transitions array via type assertion for display
const comp = obj as unknown as { _transitions: unknown[] };
data.numTransitions = comp._transitions?.length || 0;
}
return data;
}
/**
* Collect virtual nodes from FGUI node tree
* 从 FGUI 节点树收集虚拟节点
*
* Uses localToGlobal to get correct global positions.
* 使用 localToGlobal 获取正确的全局位置。
*/
function collectFGUIVirtualNodes(
obj: GObject,
halfWidth: number,
halfHeight: number,
parentPath: string
): IVirtualNode {
// Use localToGlobal to get the global position in FGUI coordinate system
// 使用 localToGlobal 获取 FGUI 坐标系中的全局位置
const globalPos = obj.localToGlobal(0, 0);
// Convert to engine coordinates for display
// 转换为引擎坐标用于显示
const engineX = globalPos.x - halfWidth;
const engineY = halfHeight - globalPos.y;
const nodePath = parentPath ? `${parentPath}/${obj.name || obj.id}` : (obj.name || obj.id);
const children: IVirtualNode[] = [];
// If this is a container, collect children
if (obj instanceof GComponent) {
for (let i = 0; i < obj.numChildren; i++) {
const child = obj.getChildAt(i);
children.push(collectFGUIVirtualNodes(child, halfWidth, halfHeight, nodePath));
}
}
// Extract common properties
const commonData: Record<string, unknown> = {
className: obj.constructor.name,
x: obj.x,
y: obj.y,
width: obj.width,
height: obj.height,
alpha: obj.alpha,
visible: obj.visible,
touchable: obj.touchable,
rotation: obj.rotation,
scaleX: obj.scaleX,
scaleY: obj.scaleY
};
// Extract type-specific properties
const typeSpecificData = extractTypeSpecificData(obj);
return {
id: nodePath,
name: obj.name || `[${getGObjectTypeName(obj)}]`,
type: getGObjectTypeName(obj),
children,
visible: obj.visible,
data: {
...commonData,
...typeSpecificData
},
x: engineX,
y: engineY,
width: obj.width,
height: obj.height
};
}
/**
* Virtual node provider for FGUIComponent
* FGUIComponent 的虚拟节点提供者
*
* Returns the internal FGUI node tree as virtual nodes.
* 将内部 FGUI 节点树作为虚拟节点返回。
*/
function fguiVirtualNodeProvider(
component: FGUIComponent,
_entity: Entity
): IVirtualNode[] {
if (!component.isReady || !component.component) {
return [];
}
// Get Stage design size for coordinate conversion
// 获取 Stage 设计尺寸用于坐标转换
const stage = Stage.inst;
const halfWidth = stage.designWidth / 2;
const halfHeight = stage.designHeight / 2;
// Collect from the loaded component
// 从加载的组件收集
const rootNode = collectFGUIVirtualNodes(
component.component,
halfWidth,
halfHeight,
''
);
// Return the children of the root (we don't want to duplicate the root)
return rootNode.children.length > 0 ? rootNode.children : [rootNode];
}
/**
* FGUIEditorModule
*
* Editor module that provides FairyGUI integration.
*
* 提供 FairyGUI 集成的编辑器模块
*/
export class FGUIEditorModule implements IEditorModuleLoader {
/** MessageHub subscription cleanup | MessageHub 订阅清理函数 */
private _unsubscribes: (() => void)[] = [];
/** Tracked FGUIComponents for state change callbacks | 跟踪的 FGUIComponent 用于状态变化回调 */
private _trackedComponents = new WeakSet<FGUIComponent>();
/**
* Install the module
* 安装模块
*/
async install(services: ServiceContainer): Promise<void> {
// Register component
const componentRegistry = services.resolve(EditorComponentRegistry);
if (componentRegistry) {
componentRegistry.register({
name: 'FGUIComponent',
type: FGUIComponent,
category: 'components.category.ui',
description: 'FairyGUI component for loading and displaying .fui packages',
icon: 'Layout'
});
}
// Register custom inspector
const inspectorRegistry = services.resolve(ComponentInspectorRegistry);
if (inspectorRegistry) {
inspectorRegistry.register(fguiComponentInspector);
}
// Register gizmo provider for FGUIComponent
// 为 FGUIComponent 注册 Gizmo 提供者
GizmoRegistry.register(FGUIComponent, fguiGizmoProvider);
// Register virtual node provider for FGUIComponent
// 为 FGUIComponent 注册虚拟节点提供者
VirtualNodeRegistry.register(FGUIComponent, fguiVirtualNodeProvider);
// Setup state change bridge for virtual node updates
// 设置状态变化桥接,用于虚拟节点更新
this._setupStateChangeBridge(services);
}
/**
* Setup bridge between FGUIComponent state changes and VirtualNodeRegistry
* 设置 FGUIComponent 状态变化与 VirtualNodeRegistry 之间的桥接
*/
private _setupStateChangeBridge(services: ServiceContainer): void {
const messageHub = services.resolve(MessageHub);
if (!messageHub) return;
// Hook into FGUIComponent when components are added
// 当组件被添加时挂钩 FGUIComponent
const hookComponent = (comp: FGUIComponent, entity: Entity) => {
if (this._trackedComponents.has(comp)) return;
this._trackedComponents.add(comp);
comp.onStateChange = (type) => {
VirtualNodeRegistry.notifyChange(entity.id, type, comp);
};
};
// Scan existing entities for FGUIComponents
// 扫描现有实体中的 FGUIComponent
const scanExistingEntities = () => {
const scene = Core.scene;
if (!scene) return;
for (const entity of scene.entities.buffer) {
const fguiComp = entity.getComponent(FGUIComponent);
if (fguiComp) {
hookComponent(fguiComp, entity);
}
}
};
// Subscribe to component:added events
// 订阅 component:added 事件
const unsubAdded = messageHub.subscribe('component:added', (event: { entityId: number; componentType: string }) => {
if (event.componentType !== 'FGUIComponent') return;
const scene = Core.scene;
if (!scene) return;
const entity = scene.findEntityById(event.entityId);
if (!entity) return;
const fguiComp = entity.getComponent(FGUIComponent);
if (fguiComp) {
hookComponent(fguiComp, entity);
}
});
// Subscribe to scene:loaded to scan existing components
// 订阅 scene:loaded 扫描现有组件
const unsubSceneLoaded = messageHub.subscribe('scene:loaded', () => {
scanExistingEntities();
});
// Initial scan
scanExistingEntities();
this._unsubscribes.push(unsubAdded, unsubSceneLoaded);
}
/**
* Uninstall the module
* 卸载模块
*/
async uninstall(): Promise<void> {
// Cleanup subscriptions
for (const unsub of this._unsubscribes) {
unsub();
}
this._unsubscribes = [];
// Unregister gizmo provider
GizmoRegistry.unregister(FGUIComponent);
// Unregister virtual node provider
VirtualNodeRegistry.unregister(FGUIComponent);
}
/**
* Get entity creation templates
* 获取实体创建模板
*/
getEntityCreationTemplates(): EntityCreationTemplate[] {
return [
{
id: 'create-fgui-root',
label: 'FGUI Root',
icon: 'Layout',
category: 'ui',
order: 300,
create: (): number => this.createFGUIEntity('FGUI Root', { width: 1920, height: 1080 })
},
{
id: 'create-fgui-view',
label: 'FGUI View',
icon: 'Image',
category: 'ui',
order: 301,
create: (): number => this.createFGUIEntity('FGUI View')
}
];
}
/**
* Create FGUI entity with optional configuration
* 创建 FGUI 实体,可选配置
*/
private createFGUIEntity(baseName: string, config?: { width?: number; height?: number }): number {
const scene = Core.scene;
if (!scene) {
throw new Error('Scene not available');
}
const entityStore = Core.services.resolve(EntityStoreService);
const messageHub = Core.services.resolve(MessageHub);
if (!entityStore || !messageHub) {
throw new Error('EntityStoreService or MessageHub not available');
}
// Generate unique name
const existingCount = entityStore.getAllEntities()
.filter((e: Entity) => e.name.startsWith(baseName)).length;
const entityName = existingCount > 0 ? `${baseName} ${existingCount + 1}` : baseName;
// Create entity
const entity = scene.createEntity(entityName);
// Add transform component
entity.addComponent(new TransformComponent());
// Add FGUI component
const fguiComponent = new FGUIComponent();
if (config?.width) fguiComponent.width = config.width;
if (config?.height) fguiComponent.height = config.height;
entity.addComponent(fguiComponent);
// Register and select entity
entityStore.addEntity(entity);
messageHub.publish('entity:added', { entity });
messageHub.publish('scene:modified', {});
entityStore.selectEntity(entity);
return entity.id;
}
}
/**
* Default FGUI editor module instance
* 默认 FGUI 编辑器模块实例
*/
export const fguiEditorModule = new FGUIEditorModule();