refactor: reorganize package structure and decouple framework packages (#338)

* refactor: reorganize package structure and decouple framework packages

## Package Structure Reorganization
- Reorganized 55 packages into categorized subdirectories:
  - packages/framework/ - Generic framework (Laya/Cocos compatible)
  - packages/engine/ - ESEngine core modules
  - packages/rendering/ - Rendering modules (WASM dependent)
  - packages/physics/ - Physics modules
  - packages/streaming/ - World streaming
  - packages/network-ext/ - Network extensions
  - packages/editor/ - Editor framework and plugins
  - packages/rust/ - Rust WASM engine
  - packages/tools/ - Build tools and SDK

## Framework Package Decoupling
- Decoupled behavior-tree and blueprint packages from ESEngine dependencies
- Created abstracted interfaces (IBTAssetManager, IBehaviorTreeAssetContent)
- ESEngine-specific code moved to esengine/ subpath exports
- Framework packages now usable with Cocos/Laya without ESEngine

## CI Configuration
- Updated CI to only type-check and lint framework packages
- Added type-check:framework and lint:framework scripts

## Breaking Changes
- Package import paths changed due to directory reorganization
- ESEngine integrations now use subpath imports (e.g., '@esengine/behavior-tree/esengine')

* fix: update es-engine file path after directory reorganization

* docs: update README to focus on framework over engine

* ci: only build framework packages, remove Rust/WASM dependencies

* fix: remove esengine subpath from behavior-tree and blueprint builds

ESEngine integration code will only be available in full engine builds.
Framework packages are now purely engine-agnostic.

* fix: move network-protocols to framework, build both in CI

* fix: update workflow paths from packages/core to packages/framework/core

* fix: exclude esengine folder from type-check in behavior-tree and blueprint

* fix: update network tsconfig references to new paths

* fix: add test:ci:framework to only test framework packages in CI

* fix: only build core and math npm packages in CI

* fix: exclude test files from CodeQL and fix string escaping security issue
This commit is contained in:
YHH
2025-12-26 14:50:35 +08:00
committed by GitHub
parent a84ff902e4
commit 155411e743
1936 changed files with 4147 additions and 11578 deletions

View File

@@ -0,0 +1,15 @@
{
"id": "fairygui-editor",
"name": "@esengine/fairygui-editor",
"displayName": "FairyGUI Editor",
"description": "Editor support for FairyGUI | FairyGUI 编辑器支持",
"version": "1.0.0",
"category": "Editor",
"icon": "Layout",
"isEditorPlugin": true,
"runtimeModule": "@esengine/fairygui",
"exports": {
"inspectors": ["FGUIComponentInspector"],
"templates": ["FGUIEntityTemplate"]
}
}

View File

@@ -0,0 +1,53 @@
{
"name": "@esengine/fairygui-editor",
"version": "1.0.0",
"description": "Editor support for @esengine/fairygui - inspectors, gizmos, and entity templates",
"main": "dist/index.js",
"module": "dist/index.js",
"types": "dist/index.d.ts",
"type": "module",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
},
"./plugin.json": "./plugin.json"
},
"files": [
"dist",
"plugin.json"
],
"scripts": {
"build": "tsup",
"build:watch": "tsup --watch",
"type-check": "tsc --noEmit",
"clean": "rimraf dist"
},
"dependencies": {
"@esengine/fairygui": "workspace:*"
},
"peerDependencies": {
"@esengine/editor-core": "workspace:*"
},
"devDependencies": {
"@esengine/ecs-framework": "workspace:*",
"@esengine/engine-core": "workspace:*",
"@esengine/editor-core": "workspace:*",
"@esengine/asset-system": "workspace:*",
"@esengine/build-config": "workspace:*",
"lucide-react": "^0.545.0",
"react": "^18.3.1",
"@types/react": "^18.3.12",
"rimraf": "^5.0.5",
"tsup": "^8.0.0",
"typescript": "^5.3.3"
},
"keywords": [
"ecs",
"fairygui",
"editor"
],
"author": "",
"license": "MIT",
"private": true
}

View File

@@ -0,0 +1,15 @@
{
"id": "@esengine/fairygui",
"name": "FairyGUI",
"version": "1.0.0",
"description": "FairyGUI UI system for ECS framework",
"category": "UI",
"isCore": false,
"defaultEnabled": true,
"isEngineModule": true,
"dependencies": ["engine-core", "asset-system"],
"exports": {
"components": ["FGUIComponent"],
"systems": ["FGUIRenderSystem"]
}
}

View File

@@ -0,0 +1,747 @@
/**
* 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();

View File

@@ -0,0 +1,54 @@
/**
* @esengine/fairygui-editor
*
* Editor support for @esengine/fairygui - inspectors, gizmos, and entity templates.
*
* FairyGUI 编辑器支持 - 检视器、Gizmo 和实体模板
*/
import type { IEditorPlugin, ModuleManifest } from '@esengine/editor-core';
import { FGUIRuntimeModule } from '@esengine/fairygui';
import { FGUIEditorModule, fguiEditorModule } from './FGUIEditorModule';
// Re-exports
export { FGUIEditorModule, fguiEditorModule } from './FGUIEditorModule';
export { FGUIInspectorContent, FGUIComponentInspector, fguiComponentInspector } from './inspectors';
/**
* Plugin manifest
* 插件清单
*/
const manifest: ModuleManifest = {
id: '@esengine/fairygui',
name: '@esengine/fairygui',
displayName: 'FairyGUI',
version: '1.0.0',
description: 'FairyGUI UI system for ECS framework with editor support',
category: 'Other',
isCore: false,
defaultEnabled: true,
isEngineModule: true,
canContainContent: true,
dependencies: ['engine-core', 'asset-system'],
editorPackage: '@esengine/fairygui-editor',
exports: {
components: ['FGUIComponent'],
systems: ['FGUIRenderSystem'],
loaders: ['FUIAssetLoader']
},
assetExtensions: {
'.fui': 'fui'
}
};
/**
* Complete FGUI Plugin (runtime + editor)
* 完整的 FGUI 插件(运行时 + 编辑器)
*/
export const FGUIPlugin: IEditorPlugin = {
manifest,
runtimeModule: new FGUIRuntimeModule(),
editorModule: fguiEditorModule
};
export default fguiEditorModule;

View File

@@ -0,0 +1,242 @@
/**
* FGUIInspector
*
* Custom inspector for FGUIComponent.
* Uses 'append' mode to add Component selection UI after the default PropertyInspector.
*
* FGUIComponent 的自定义检视器,在默认 PropertyInspector 后追加组件选择 UI
*/
import React, { useCallback, useMemo, useState, useEffect } from 'react';
import { Package, AlertCircle, CheckCircle, Loader } from 'lucide-react';
import type { Component } from '@esengine/ecs-framework';
import type { ComponentInspectorContext, IComponentInspector } from '@esengine/editor-core';
import { VirtualNodeRegistry } from '@esengine/editor-core';
import { FGUIComponent } from '@esengine/fairygui';
/** Shared styles | 共享样式 */
const styles = {
section: {
marginTop: '8px',
padding: '8px',
background: 'var(--color-bg-secondary, #252526)',
borderRadius: '4px',
border: '1px solid var(--color-border, #3a3a3a)'
} as React.CSSProperties,
sectionHeader: {
display: 'flex',
alignItems: 'center',
gap: '6px',
marginBottom: '8px',
fontSize: '11px',
fontWeight: 600,
color: 'var(--color-text-secondary, #888)',
textTransform: 'uppercase' as const,
letterSpacing: '0.5px'
} as React.CSSProperties,
row: {
display: 'flex',
alignItems: 'center',
marginBottom: '6px',
gap: '8px'
} as React.CSSProperties,
label: {
width: '70px',
flexShrink: 0,
fontSize: '12px',
color: 'var(--color-text-secondary, #888)'
} as React.CSSProperties,
select: {
flex: 1,
padding: '5px 8px',
background: 'var(--color-bg-tertiary, #1e1e1e)',
border: '1px solid var(--color-border, #3a3a3a)',
borderRadius: '4px',
color: 'inherit',
fontSize: '12px',
minWidth: 0,
cursor: 'pointer'
} as React.CSSProperties,
statusBadge: {
display: 'inline-flex',
alignItems: 'center',
gap: '4px',
padding: '3px 10px',
borderRadius: '12px',
fontSize: '11px',
fontWeight: 500
} as React.CSSProperties
};
/**
* FGUIInspectorContent
*
* React component for FGUI inspector content.
* Shows package status and component selection dropdown.
*
* FGUI 检视器内容的 React 组件,显示包状态和组件选择下拉框
*/
export const FGUIInspectorContent: React.FC<{ context: ComponentInspectorContext }> = ({ context }) => {
const component = context.component as FGUIComponent;
const onChange = context.onChange;
const entityId = context.entity?.id;
// Track version to trigger re-render when component state changes
// 跟踪版本以在组件状态变化时触发重新渲染
const [refreshKey, setRefreshKey] = useState(0);
// Subscribe to VirtualNodeRegistry changes (event-driven, no polling)
// 订阅 VirtualNodeRegistry 变化(事件驱动,无需轮询)
useEffect(() => {
if (entityId === undefined) return;
const unsubscribe = VirtualNodeRegistry.onChange((event) => {
if (event.entityId === entityId) {
setRefreshKey(prev => prev + 1);
}
});
return unsubscribe;
}, [entityId]);
// Get available components from loaded package
// Use refreshKey as dependency to refresh when package/component changes
// 使用 refreshKey 作为依赖,当包/组件变化时刷新
const availableComponents = useMemo(() => {
if (!component.package) return [];
const exported = component.getAvailableComponentNames();
if (exported.length > 0) return exported;
return component.getAllComponentNames();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [component.package, refreshKey]);
// Handle component name change
const handleComponentChange = useCallback((e: React.ChangeEvent<HTMLSelectElement>) => {
if (onChange) {
onChange('componentName', e.target.value);
}
}, [onChange]);
// Render status badge
const renderStatus = () => {
if (component.isLoading) {
return (
<span style={{ ...styles.statusBadge, background: 'rgba(251, 191, 36, 0.15)', color: '#fbbf24' }}>
<Loader size={12} style={{ animation: 'fgui-spin 1s linear infinite' }} />
Loading...
</span>
);
}
if (component.error) {
return (
<span style={{ ...styles.statusBadge, background: 'rgba(248, 113, 113, 0.15)', color: '#f87171' }}>
<AlertCircle size={12} />
Error
</span>
);
}
if (component.isReady) {
return (
<span style={{ ...styles.statusBadge, background: 'rgba(74, 222, 128, 0.15)', color: '#4ade80' }}>
<CheckCircle size={12} />
{component.package?.name || 'Ready'}
</span>
);
}
return (
<span style={{ ...styles.statusBadge, background: 'rgba(136, 136, 136, 0.15)', color: '#888' }}>
<Package size={12} />
No Package
</span>
);
};
return (
<div style={styles.section}>
{/* Section Header */}
<div style={styles.sectionHeader}>
<Package size={12} />
<span>FGUI Runtime</span>
</div>
{/* Status Row */}
<div style={styles.row}>
<span style={styles.label}>Status</span>
<div style={{ flex: 1 }}>
{renderStatus()}
</div>
</div>
{/* Error Message */}
{component.error && (
<div style={{
marginBottom: '8px',
padding: '6px 8px',
background: 'rgba(248, 113, 113, 0.1)',
border: '1px solid rgba(248, 113, 113, 0.3)',
borderRadius: '4px',
fontSize: '11px',
color: '#f87171',
wordBreak: 'break-word'
}}>
{component.error}
</div>
)}
{/* Component Selection - only show when package is loaded */}
{availableComponents.length > 0 && (
<div style={{ ...styles.row, marginBottom: 0 }}>
<span style={styles.label}>Component</span>
<select
value={component.componentName}
onChange={handleComponentChange}
style={styles.select}
>
<option value="">Select...</option>
{availableComponents.map((name) => (
<option key={name} value={name}>{name}</option>
))}
</select>
</div>
)}
{/* Spin animation for loader */}
<style>{`
@keyframes fgui-spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
`}</style>
</div>
);
};
/**
* FGUIComponentInspector
*
* Component inspector for FGUIComponent.
* Uses 'append' mode to show additional UI after the default PropertyInspector.
*
* FGUIComponent 的组件检视器,使用 'append' 模式在默认 Inspector 后追加 UI
*/
export class FGUIComponentInspector implements IComponentInspector<FGUIComponent> {
readonly id = 'fgui-component-inspector';
readonly name = 'FGUI Component Inspector';
readonly priority = 100;
readonly targetComponents = ['FGUIComponent'];
readonly renderMode = 'append' as const;
canHandle(component: Component): component is FGUIComponent {
return component instanceof FGUIComponent;
}
render(context: ComponentInspectorContext): React.ReactElement {
return React.createElement(FGUIInspectorContent, { context });
}
}
/**
* Default FGUI component inspector instance
* 默认 FGUI 组件检视器实例
*/
export const fguiComponentInspector = new FGUIComponentInspector();

View File

@@ -0,0 +1,9 @@
/**
* FairyGUI Editor Inspectors
*
* Custom inspectors for FairyGUI components.
*
* FairyGUI 组件的自定义检视器
*/
export { FGUIInspectorContent, FGUIComponentInspector, fguiComponentInspector } from './FGUIInspector';

View File

@@ -0,0 +1,12 @@
{
"extends": "../build-config/tsconfig.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src",
"jsx": "react-jsx",
"declaration": true,
"declarationDir": "./dist"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}

View File

@@ -0,0 +1,20 @@
import { defineConfig } from 'tsup';
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm'],
dts: true,
clean: true,
external: [
'react',
'react-dom',
'@esengine/ecs-framework',
'@esengine/editor-core',
'@esengine/asset-system',
'@esengine/fairygui',
'lucide-react'
],
esbuildOptions(options) {
options.jsx = 'automatic';
}
});