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 引用
This commit is contained in:
@@ -27,11 +27,27 @@ export interface GizmoColor {
|
||||
a: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Base gizmo data with optional virtual node reference
|
||||
* 带有可选虚拟节点引用的基础 Gizmo 数据
|
||||
*/
|
||||
export interface IGizmoDataBase {
|
||||
/**
|
||||
* Optional virtual node ID for component internal nodes
|
||||
* 可选的虚拟节点 ID,用于组件内部节点
|
||||
*
|
||||
* When set, clicking this gizmo will select the virtual node
|
||||
* instead of just the entity.
|
||||
* 设置后,点击此 gizmo 将选中虚拟节点而不只是实体。
|
||||
*/
|
||||
virtualNodeId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rectangle gizmo data (rendered via Rust WebGL)
|
||||
* 矩形 gizmo 数据(通过 Rust WebGL 渲染)
|
||||
*/
|
||||
export interface IRectGizmoData {
|
||||
export interface IRectGizmoData extends IGizmoDataBase {
|
||||
type: 'rect';
|
||||
/** Center X position in world space | 世界空间中心 X 位置 */
|
||||
x: number;
|
||||
@@ -57,7 +73,7 @@ export interface IRectGizmoData {
|
||||
* Circle gizmo data
|
||||
* 圆形 gizmo 数据
|
||||
*/
|
||||
export interface ICircleGizmoData {
|
||||
export interface ICircleGizmoData extends IGizmoDataBase {
|
||||
type: 'circle';
|
||||
/** Center X position | 中心 X 位置 */
|
||||
x: number;
|
||||
@@ -73,7 +89,7 @@ export interface ICircleGizmoData {
|
||||
* Line gizmo data
|
||||
* 线条 gizmo 数据
|
||||
*/
|
||||
export interface ILineGizmoData {
|
||||
export interface ILineGizmoData extends IGizmoDataBase {
|
||||
type: 'line';
|
||||
/** Line points | 线段点 */
|
||||
points: Array<{ x: number; y: number }>;
|
||||
@@ -87,7 +103,7 @@ export interface ILineGizmoData {
|
||||
* Grid gizmo data
|
||||
* 网格 gizmo 数据
|
||||
*/
|
||||
export interface IGridGizmoData {
|
||||
export interface IGridGizmoData extends IGizmoDataBase {
|
||||
type: 'grid';
|
||||
/** Top-left X position | 左上角 X 位置 */
|
||||
x: number;
|
||||
@@ -109,7 +125,7 @@ export interface IGridGizmoData {
|
||||
* Capsule gizmo data
|
||||
* 胶囊 gizmo 数据
|
||||
*/
|
||||
export interface ICapsuleGizmoData {
|
||||
export interface ICapsuleGizmoData extends IGizmoDataBase {
|
||||
type: 'capsule';
|
||||
/** Center X position | 中心 X 位置 */
|
||||
x: number;
|
||||
|
||||
@@ -127,6 +127,8 @@ const EXTENSION_TYPE_MAP: Record<string, AssetRegistryType> = {
|
||||
'.tsx': 'tileset',
|
||||
// Particle system
|
||||
'.particle': 'particle',
|
||||
// FairyGUI
|
||||
'.fui': 'fui',
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -23,6 +23,19 @@ export interface GizmoHitResult {
|
||||
entityId: number;
|
||||
/** Distance from hit point to gizmo center | 命中点到 Gizmo 中心的距离 */
|
||||
distance: number;
|
||||
/** Virtual node ID if this gizmo represents a virtual node | 虚拟节点 ID(如果此 gizmo 代表虚拟节点) */
|
||||
virtualNodeId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Click result with entity and optional virtual node
|
||||
* 点击结果,包含实体和可选的虚拟节点
|
||||
*/
|
||||
export interface GizmoClickResult {
|
||||
/** Entity ID | 实体 ID */
|
||||
entityId: number;
|
||||
/** Virtual node ID if clicked on a virtual node gizmo | 虚拟节点 ID(如果点击了虚拟节点 gizmo) */
|
||||
virtualNodeId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -73,6 +86,23 @@ export interface IGizmoInteractionService {
|
||||
* 清除悬停状态
|
||||
*/
|
||||
clearHover(): void;
|
||||
|
||||
/**
|
||||
* Handle click at position with virtual node support
|
||||
* 处理位置点击,支持虚拟节点
|
||||
*
|
||||
* @param worldX World X coordinate | 世界 X 坐标
|
||||
* @param worldY World Y coordinate | 世界 Y 坐标
|
||||
* @param zoom Current viewport zoom level | 当前视口缩放级别
|
||||
* @returns Click result with entity and optional virtual node | 点击结果
|
||||
*/
|
||||
handleClickEx(worldX: number, worldY: number, zoom: number): GizmoClickResult | null;
|
||||
|
||||
/**
|
||||
* Get currently hovered virtual node ID
|
||||
* 获取当前悬停的虚拟节点 ID
|
||||
*/
|
||||
getHoveredVirtualNodeId(): string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -85,6 +115,7 @@ export interface IGizmoInteractionService {
|
||||
export class GizmoInteractionService implements IGizmoInteractionService {
|
||||
private hoveredEntityId: number | null = null;
|
||||
private hoveredGizmo: IGizmoRenderData | null = null;
|
||||
private hoveredVirtualNodeId: string | null = null;
|
||||
|
||||
/** Hover color multiplier for RGB channels | 悬停时 RGB 通道的颜色倍增 */
|
||||
private static readonly HOVER_COLOR_MULTIPLIER = 1.3;
|
||||
@@ -96,8 +127,8 @@ export class GizmoInteractionService implements IGizmoInteractionService {
|
||||
private lastClickPos: { x: number; y: number } | null = null;
|
||||
/** Last click time | 上次点击时间 */
|
||||
private lastClickTime: number = 0;
|
||||
/** All hit entities at current click position | 当前点击位置的所有命中实体 */
|
||||
private hitEntitiesAtClick: number[] = [];
|
||||
/** All hit results at current click position | 当前点击位置的所有命中结果 */
|
||||
private hitResultsAtClick: GizmoClickResult[] = [];
|
||||
/** Current cycle index | 当前循环索引 */
|
||||
private cycleIndex: number = 0;
|
||||
/** Position tolerance for same-position detection | 判断相同位置的容差 */
|
||||
@@ -121,6 +152,14 @@ export class GizmoInteractionService implements IGizmoInteractionService {
|
||||
return this.hoveredGizmo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get currently hovered virtual node ID
|
||||
* 获取当前悬停的虚拟节点 ID
|
||||
*/
|
||||
getHoveredVirtualNodeId(): string | null {
|
||||
return this.hoveredVirtualNodeId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update mouse position and perform hit test
|
||||
* 更新鼠标位置并执行命中测试
|
||||
@@ -130,6 +169,7 @@ export class GizmoInteractionService implements IGizmoInteractionService {
|
||||
if (!scene) {
|
||||
this.hoveredEntityId = null;
|
||||
this.hoveredGizmo = null;
|
||||
this.hoveredVirtualNodeId = null;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -166,7 +206,8 @@ export class GizmoInteractionService implements IGizmoInteractionService {
|
||||
closestHit = {
|
||||
gizmo,
|
||||
entityId: entity.id,
|
||||
distance
|
||||
distance,
|
||||
virtualNodeId: gizmo.virtualNodeId
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -176,6 +217,7 @@ export class GizmoInteractionService implements IGizmoInteractionService {
|
||||
|
||||
this.hoveredEntityId = closestHit?.entityId ?? null;
|
||||
this.hoveredGizmo = closestHit?.gizmo ?? null;
|
||||
this.hoveredVirtualNodeId = closestHit?.virtualNodeId ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -206,56 +248,66 @@ export class GizmoInteractionService implements IGizmoInteractionService {
|
||||
* 支持重复点击时循环选择重叠的实体
|
||||
*/
|
||||
handleClick(worldX: number, worldY: number, zoom: number): number | null {
|
||||
const result = this.handleClickEx(worldX, worldY, zoom);
|
||||
return result?.entityId ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle click at position with virtual node support
|
||||
* Supports cycling through overlapping gizmos on repeated clicks
|
||||
* 处理位置点击,支持虚拟节点
|
||||
* 支持重复点击时循环选择重叠的 gizmos
|
||||
*/
|
||||
handleClickEx(worldX: number, worldY: number, zoom: number): GizmoClickResult | null {
|
||||
const now = Date.now();
|
||||
const isSamePosition = this.lastClickPos !== null &&
|
||||
Math.abs(worldX - this.lastClickPos.x) < GizmoInteractionService.CLICK_POSITION_TOLERANCE / zoom &&
|
||||
Math.abs(worldY - this.lastClickPos.y) < GizmoInteractionService.CLICK_POSITION_TOLERANCE / zoom;
|
||||
const isWithinTimeWindow = (now - this.lastClickTime) < GizmoInteractionService.CLICK_TIME_TOLERANCE;
|
||||
|
||||
// If clicking at same position within time window, cycle to next entity
|
||||
// 如果在时间窗口内点击相同位置,循环到下一个实体
|
||||
if (isSamePosition && isWithinTimeWindow && this.hitEntitiesAtClick.length > 1) {
|
||||
this.cycleIndex = (this.cycleIndex + 1) % this.hitEntitiesAtClick.length;
|
||||
// If clicking at same position within time window, cycle to next result
|
||||
// 如果在时间窗口内点击相同位置,循环到下一个结果
|
||||
if (isSamePosition && isWithinTimeWindow && this.hitResultsAtClick.length > 1) {
|
||||
this.cycleIndex = (this.cycleIndex + 1) % this.hitResultsAtClick.length;
|
||||
this.lastClickTime = now;
|
||||
const selectedId = this.hitEntitiesAtClick[this.cycleIndex];
|
||||
this.hoveredEntityId = selectedId;
|
||||
return selectedId;
|
||||
const result = this.hitResultsAtClick[this.cycleIndex];
|
||||
this.hoveredEntityId = result.entityId;
|
||||
this.hoveredVirtualNodeId = result.virtualNodeId ?? null;
|
||||
return result;
|
||||
}
|
||||
|
||||
// New position or timeout - collect all hit entities
|
||||
// 新位置或超时 - 收集所有命中的实体
|
||||
this.hitEntitiesAtClick = this.collectAllHitEntities(worldX, worldY, zoom);
|
||||
// New position or timeout - collect all hit results
|
||||
// 新位置或超时 - 收集所有命中结果
|
||||
this.hitResultsAtClick = this.collectAllHitResults(worldX, worldY, zoom);
|
||||
this.cycleIndex = 0;
|
||||
this.lastClickPos = { x: worldX, y: worldY };
|
||||
this.lastClickTime = now;
|
||||
|
||||
if (this.hitEntitiesAtClick.length > 0) {
|
||||
const selectedId = this.hitEntitiesAtClick[0];
|
||||
this.hoveredEntityId = selectedId;
|
||||
return selectedId;
|
||||
if (this.hitResultsAtClick.length > 0) {
|
||||
const result = this.hitResultsAtClick[0];
|
||||
this.hoveredEntityId = result.entityId;
|
||||
this.hoveredVirtualNodeId = result.virtualNodeId ?? null;
|
||||
return result;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect all entities hit at the given position, sorted by distance
|
||||
* 收集给定位置命中的所有实体,按距离排序
|
||||
* Collect all hit results at the given position, sorted by distance
|
||||
* 收集给定位置的所有命中结果,按距离排序
|
||||
*/
|
||||
private collectAllHitEntities(worldX: number, worldY: number, zoom: number): number[] {
|
||||
private collectAllHitResults(worldX: number, worldY: number, zoom: number): GizmoClickResult[] {
|
||||
const scene = Core.scene;
|
||||
if (!scene) return [];
|
||||
|
||||
const hits: GizmoHitResult[] = [];
|
||||
const hits: Array<GizmoClickResult & { distance: number }> = [];
|
||||
|
||||
for (const entity of scene.entities.buffer) {
|
||||
if (!GizmoRegistry.hasAnyGizmoProvider(entity)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let entityHit = false;
|
||||
let minDistance = Infinity;
|
||||
|
||||
for (const component of entity.components) {
|
||||
const componentType = component.constructor as ComponentType;
|
||||
if (!GizmoRegistry.hasProvider(componentType)) {
|
||||
@@ -265,30 +317,37 @@ export class GizmoInteractionService implements IGizmoInteractionService {
|
||||
const gizmos = GizmoRegistry.getGizmoData(component, entity, false);
|
||||
for (const gizmo of gizmos) {
|
||||
if (GizmoHitTester.hitTest(worldX, worldY, gizmo, zoom)) {
|
||||
entityHit = true;
|
||||
const center = GizmoHitTester.getGizmoCenter(gizmo);
|
||||
const distance = Math.sqrt(
|
||||
(worldX - center.x) ** 2 + (worldY - center.y) ** 2
|
||||
);
|
||||
minDistance = Math.min(minDistance, distance);
|
||||
hits.push({
|
||||
entityId: entity.id,
|
||||
virtualNodeId: gizmo.virtualNodeId,
|
||||
distance
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (entityHit) {
|
||||
hits.push({
|
||||
gizmo: {} as IGizmoRenderData, // Not needed for sorting
|
||||
entityId: entity.id,
|
||||
distance: minDistance
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by distance (closest first)
|
||||
// 按距离排序(最近的在前)
|
||||
hits.sort((a, b) => a.distance - b.distance);
|
||||
|
||||
return hits.map(hit => hit.entityId);
|
||||
// Remove duplicates (same entity + virtualNodeId), keeping closest
|
||||
// 去重(相同实体 + virtualNodeId),保留最近的
|
||||
const seen = new Set<string>();
|
||||
const uniqueHits: GizmoClickResult[] = [];
|
||||
for (const hit of hits) {
|
||||
const key = `${hit.entityId}:${hit.virtualNodeId ?? ''}`;
|
||||
if (!seen.has(key)) {
|
||||
seen.add(key);
|
||||
uniqueHits.push({ entityId: hit.entityId, virtualNodeId: hit.virtualNodeId });
|
||||
}
|
||||
}
|
||||
|
||||
return uniqueHits;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -298,5 +357,6 @@ export class GizmoInteractionService implements IGizmoInteractionService {
|
||||
clearHover(): void {
|
||||
this.hoveredEntityId = null;
|
||||
this.hoveredGizmo = null;
|
||||
this.hoveredVirtualNodeId = null;
|
||||
}
|
||||
}
|
||||
|
||||
323
packages/editor-core/src/Services/VirtualNodeRegistry.ts
Normal file
323
packages/editor-core/src/Services/VirtualNodeRegistry.ts
Normal file
@@ -0,0 +1,323 @@
|
||||
/**
|
||||
* VirtualNodeRegistry
|
||||
*
|
||||
* Registry for virtual child nodes in the scene hierarchy.
|
||||
* Allows components to expose internal structure as read-only nodes
|
||||
* in the hierarchy panel.
|
||||
*
|
||||
* Uses event-driven architecture for efficient change notification.
|
||||
*
|
||||
* 场景层级中虚拟子节点的注册表。
|
||||
* 允许组件将内部结构作为只读节点暴露在层级面板中。
|
||||
* 使用事件驱动架构实现高效的变化通知。
|
||||
*/
|
||||
|
||||
import type { Component, ComponentType, Entity } from '@esengine/ecs-framework';
|
||||
|
||||
/**
|
||||
* Virtual node data
|
||||
* 虚拟节点数据
|
||||
*/
|
||||
export interface IVirtualNode {
|
||||
/** Unique ID within the parent component | 父组件内的唯一 ID */
|
||||
id: string;
|
||||
|
||||
/** Display name | 显示名称 */
|
||||
name: string;
|
||||
|
||||
/** Node type for icon selection | 节点类型(用于图标选择) */
|
||||
type: string;
|
||||
|
||||
/** Child nodes | 子节点 */
|
||||
children: IVirtualNode[];
|
||||
|
||||
/** Whether this node is visible | 此节点是否可见 */
|
||||
visible: boolean;
|
||||
|
||||
/** Node-specific data for Inspector | Inspector 使用的节点数据 */
|
||||
data: Record<string, unknown>;
|
||||
|
||||
/** World X position (for Gizmo) | 世界 X 坐标(用于 Gizmo) */
|
||||
x: number;
|
||||
|
||||
/** World Y position (for Gizmo) | 世界 Y 坐标(用于 Gizmo) */
|
||||
y: number;
|
||||
|
||||
/** Width (for Gizmo) | 宽度(用于 Gizmo) */
|
||||
width: number;
|
||||
|
||||
/** Height (for Gizmo) | 高度(用于 Gizmo) */
|
||||
height: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Virtual node provider function
|
||||
* 虚拟节点提供者函数
|
||||
*
|
||||
* Returns an array of virtual nodes for a component instance.
|
||||
* 为组件实例返回虚拟节点数组。
|
||||
*/
|
||||
export type VirtualNodeProviderFn<T extends Component = Component> = (
|
||||
component: T,
|
||||
entity: Entity
|
||||
) => IVirtualNode[];
|
||||
|
||||
/**
|
||||
* Change event types for virtual nodes
|
||||
* 虚拟节点的变化事件类型
|
||||
*/
|
||||
export type VirtualNodeChangeType = 'loaded' | 'updated' | 'disposed';
|
||||
|
||||
/**
|
||||
* Virtual node change event payload
|
||||
* 虚拟节点变化事件载荷
|
||||
*/
|
||||
export interface VirtualNodeChangeEvent {
|
||||
/** Entity ID that changed | 发生变化的实体 ID */
|
||||
entityId: number;
|
||||
/** Type of change | 变化类型 */
|
||||
type: VirtualNodeChangeType;
|
||||
/** Component that triggered the change (optional) | 触发变化的组件(可选) */
|
||||
component?: Component;
|
||||
}
|
||||
|
||||
/**
|
||||
* Change listener function type
|
||||
* 变化监听器函数类型
|
||||
*/
|
||||
export type VirtualNodeChangeListener = (event: VirtualNodeChangeEvent) => void;
|
||||
|
||||
/**
|
||||
* VirtualNodeRegistry
|
||||
*
|
||||
* Manages virtual node providers for different component types.
|
||||
* Provides event-driven change notifications for efficient UI updates.
|
||||
*
|
||||
* 管理不同组件类型的虚拟节点提供者。
|
||||
* 提供事件驱动的变化通知以实现高效的 UI 更新。
|
||||
*/
|
||||
export class VirtualNodeRegistry {
|
||||
private static providers = new Map<ComponentType, VirtualNodeProviderFn>();
|
||||
|
||||
/** Currently selected virtual node info | 当前选中的虚拟节点信息 */
|
||||
private static selectedVirtualNodeInfo: {
|
||||
entityId: number;
|
||||
virtualNodeId: string;
|
||||
} | null = null;
|
||||
|
||||
/** Change listeners | 变化监听器 */
|
||||
private static changeListeners = new Set<VirtualNodeChangeListener>();
|
||||
|
||||
// ============= Provider Registration | 提供者注册 =============
|
||||
|
||||
/**
|
||||
* Register a virtual node provider for a component type
|
||||
* 为组件类型注册虚拟节点提供者
|
||||
*/
|
||||
static register<T extends Component>(
|
||||
componentType: ComponentType<T>,
|
||||
provider: VirtualNodeProviderFn<T>
|
||||
): void {
|
||||
this.providers.set(componentType, provider as VirtualNodeProviderFn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister a virtual node provider
|
||||
* 取消注册虚拟节点提供者
|
||||
*/
|
||||
static unregister(componentType: ComponentType): void {
|
||||
this.providers.delete(componentType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a component type has a virtual node provider
|
||||
* 检查组件类型是否有虚拟节点提供者
|
||||
*/
|
||||
static hasProvider(componentType: ComponentType): boolean {
|
||||
return this.providers.has(componentType);
|
||||
}
|
||||
|
||||
// ============= Virtual Node Collection | 虚拟节点收集 =============
|
||||
|
||||
/**
|
||||
* Get virtual nodes for a component
|
||||
* 获取组件的虚拟节点
|
||||
*/
|
||||
static getVirtualNodes(
|
||||
component: Component,
|
||||
entity: Entity
|
||||
): IVirtualNode[] {
|
||||
const componentType = component.constructor as ComponentType;
|
||||
const provider = this.providers.get(componentType);
|
||||
|
||||
if (provider) {
|
||||
try {
|
||||
return provider(component, entity);
|
||||
} catch (e) {
|
||||
console.warn(`[VirtualNodeRegistry] Error in provider for ${componentType.name}:`, e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all virtual nodes for an entity
|
||||
* 获取实体的所有虚拟节点
|
||||
*/
|
||||
static getAllVirtualNodesForEntity(entity: Entity): IVirtualNode[] {
|
||||
const allNodes: IVirtualNode[] = [];
|
||||
|
||||
for (const component of entity.components) {
|
||||
const nodes = this.getVirtualNodes(component, entity);
|
||||
allNodes.push(...nodes);
|
||||
}
|
||||
|
||||
return allNodes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an entity has any components with virtual node providers
|
||||
* 检查实体是否有任何带有虚拟节点提供者的组件
|
||||
*/
|
||||
static hasAnyVirtualNodeProvider(entity: Entity): boolean {
|
||||
for (const component of entity.components) {
|
||||
const componentType = component.constructor as ComponentType;
|
||||
if (this.providers.has(componentType)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// ============= Event System | 事件系统 =============
|
||||
|
||||
/**
|
||||
* Subscribe to virtual node changes
|
||||
* 订阅虚拟节点变化
|
||||
*
|
||||
* @param listener Callback function for change events
|
||||
* @returns Unsubscribe function
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const unsubscribe = VirtualNodeRegistry.onChange((event) => {
|
||||
* if (event.entityId === selectedEntityId) {
|
||||
* refreshVirtualNodes();
|
||||
* }
|
||||
* });
|
||||
*
|
||||
* // Later, cleanup
|
||||
* unsubscribe();
|
||||
* ```
|
||||
*/
|
||||
static onChange(listener: VirtualNodeChangeListener): () => void {
|
||||
this.changeListeners.add(listener);
|
||||
return () => {
|
||||
this.changeListeners.delete(listener);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify that an entity's virtual nodes have changed
|
||||
* Components should call this when their internal structure changes
|
||||
*
|
||||
* 通知实体的虚拟节点已更改
|
||||
* 组件在内部结构变化时应调用此方法
|
||||
*
|
||||
* @param entityId The entity ID that changed
|
||||
* @param type Type of change ('loaded', 'updated', 'disposed')
|
||||
* @param component Optional component reference
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // In FGUIComponent after loading completes:
|
||||
* VirtualNodeRegistry.notifyChange(this.entity.id, 'loaded', this);
|
||||
*
|
||||
* // In FGUIComponent when switching component:
|
||||
* VirtualNodeRegistry.notifyChange(this.entity.id, 'updated', this);
|
||||
* ```
|
||||
*/
|
||||
static notifyChange(
|
||||
entityId: number,
|
||||
type: VirtualNodeChangeType = 'updated',
|
||||
component?: Component
|
||||
): void {
|
||||
const event: VirtualNodeChangeEvent = { entityId, type, component };
|
||||
for (const listener of this.changeListeners) {
|
||||
try {
|
||||
listener(event);
|
||||
} catch (e) {
|
||||
console.warn('[VirtualNodeRegistry] Error in change listener:', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a React hook-friendly subscription
|
||||
* 创建对 React Hook 友好的订阅
|
||||
*
|
||||
* @param entityIds Set of entity IDs to watch
|
||||
* @param callback Callback when any watched entity changes
|
||||
* @returns Unsubscribe function
|
||||
*/
|
||||
static watchEntities(
|
||||
entityIds: Set<number>,
|
||||
callback: () => void
|
||||
): () => void {
|
||||
return this.onChange((event) => {
|
||||
if (entityIds.has(event.entityId)) {
|
||||
callback();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ============= Selection State | 选择状态 =============
|
||||
|
||||
/**
|
||||
* Set the currently selected virtual node
|
||||
* 设置当前选中的虚拟节点
|
||||
*/
|
||||
static setSelectedVirtualNode(entityId: number, virtualNodeId: string): void {
|
||||
this.selectedVirtualNodeInfo = { entityId, virtualNodeId };
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the virtual node selection
|
||||
* 清除虚拟节点选择
|
||||
*/
|
||||
static clearSelectedVirtualNode(): void {
|
||||
this.selectedVirtualNodeInfo = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the currently selected virtual node info
|
||||
* 获取当前选中的虚拟节点信息
|
||||
*/
|
||||
static getSelectedVirtualNode(): { entityId: number; virtualNodeId: string } | null {
|
||||
return this.selectedVirtualNodeInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a specific virtual node is selected
|
||||
* 检查特定虚拟节点是否被选中
|
||||
*/
|
||||
static isVirtualNodeSelected(entityId: number, virtualNodeId: string): boolean {
|
||||
return this.selectedVirtualNodeInfo !== null &&
|
||||
this.selectedVirtualNodeInfo.entityId === entityId &&
|
||||
this.selectedVirtualNodeInfo.virtualNodeId === virtualNodeId;
|
||||
}
|
||||
|
||||
// ============= Cleanup | 清理 =============
|
||||
|
||||
/**
|
||||
* Clear all registered providers and listeners
|
||||
* 清除所有已注册的提供者和监听器
|
||||
*/
|
||||
static clear(): void {
|
||||
this.providers.clear();
|
||||
this.changeListeners.clear();
|
||||
this.selectedVirtualNodeInfo = null;
|
||||
}
|
||||
}
|
||||
@@ -50,6 +50,8 @@ export * from './Services/IViewportService';
|
||||
export * from './Services/PreviewSceneService';
|
||||
export * from './Services/EditorViewportService';
|
||||
export * from './Services/PrefabService';
|
||||
export * from './Services/VirtualNodeRegistry';
|
||||
export * from './Services/GizmoInteractionService';
|
||||
|
||||
// Build System | 构建系统
|
||||
export * from './Services/Build';
|
||||
|
||||
Reference in New Issue
Block a user