diff --git a/packages/blueprint/src/composition/BlueprintComposer.ts b/packages/blueprint/src/composition/BlueprintComposer.ts new file mode 100644 index 00000000..cc69c013 --- /dev/null +++ b/packages/blueprint/src/composition/BlueprintComposer.ts @@ -0,0 +1,575 @@ +/** + * @zh 蓝图组合器接口和实现 + * @en Blueprint Composer Interface and Implementation + * + * @zh 将多个蓝图片段组合成一个完整的蓝图 + * @en Composes multiple blueprint fragments into a complete blueprint + */ + +import type { BlueprintAsset, BlueprintVariable } from '../types/blueprint'; +import type { BlueprintNode, BlueprintConnection } from '../types/nodes'; +import type { IBlueprintFragment } from './BlueprintFragment'; + +// ============================================================================= +// 槽位定义 | Slot Definition +// ============================================================================= + +/** + * @zh 片段槽位 + * @en Fragment slot + * + * @zh 组合器中放置片段的位置 + * @en A position in the composer where a fragment is placed + */ +export interface FragmentSlot { + /** + * @zh 槽位 ID + * @en Slot ID + */ + readonly id: string; + + /** + * @zh 槽位名称 + * @en Slot name + */ + readonly name: string; + + /** + * @zh 放置的片段 + * @en Placed fragment + */ + readonly fragment: IBlueprintFragment; + + /** + * @zh 在组合图中的位置偏移 + * @en Position offset in the composed graph + */ + readonly position: { x: number; y: number }; +} + +/** + * @zh 槽位间连接 + * @en Connection between slots + */ +export interface SlotConnection { + /** + * @zh 连接 ID + * @en Connection ID + */ + readonly id: string; + + /** + * @zh 源槽位 ID + * @en Source slot ID + */ + readonly fromSlotId: string; + + /** + * @zh 源引脚名称 + * @en Source pin name + */ + readonly fromPin: string; + + /** + * @zh 目标槽位 ID + * @en Target slot ID + */ + readonly toSlotId: string; + + /** + * @zh 目标引脚名称 + * @en Target pin name + */ + readonly toPin: string; +} + +// ============================================================================= +// 组合器接口 | Composer Interface +// ============================================================================= + +/** + * @zh 蓝图组合器接口 + * @en Blueprint composer interface + * + * @zh 用于将多个蓝图片段组合成一个完整蓝图 + * @en Used to compose multiple blueprint fragments into a complete blueprint + */ +export interface IBlueprintComposer { + /** + * @zh 组合器名称 + * @en Composer name + */ + readonly name: string; + + /** + * @zh 获取所有槽位 + * @en Get all slots + */ + getSlots(): FragmentSlot[]; + + /** + * @zh 获取所有连接 + * @en Get all connections + */ + getConnections(): SlotConnection[]; + + /** + * @zh 添加片段到槽位 + * @en Add fragment to slot + * + * @param fragment - @zh 蓝图片段 @en Blueprint fragment + * @param slotId - @zh 槽位 ID @en Slot ID + * @param options - @zh 选项 @en Options + */ + addFragment( + fragment: IBlueprintFragment, + slotId: string, + options?: { + name?: string; + position?: { x: number; y: number }; + } + ): void; + + /** + * @zh 移除槽位 + * @en Remove slot + * + * @param slotId - @zh 槽位 ID @en Slot ID + */ + removeSlot(slotId: string): void; + + /** + * @zh 连接两个槽位的引脚 + * @en Connect pins between two slots + * + * @param fromSlotId - @zh 源槽位 ID @en Source slot ID + * @param fromPin - @zh 源引脚名称 @en Source pin name + * @param toSlotId - @zh 目标槽位 ID @en Target slot ID + * @param toPin - @zh 目标引脚名称 @en Target pin name + */ + connect( + fromSlotId: string, + fromPin: string, + toSlotId: string, + toPin: string + ): void; + + /** + * @zh 断开连接 + * @en Disconnect + * + * @param connectionId - @zh 连接 ID @en Connection ID + */ + disconnect(connectionId: string): void; + + /** + * @zh 验证组合是否有效 + * @en Validate if the composition is valid + */ + validate(): CompositionValidationResult; + + /** + * @zh 编译成蓝图资产 + * @en Compile into blueprint asset + */ + compile(): BlueprintAsset; + + /** + * @zh 清空组合器 + * @en Clear the composer + */ + clear(): void; +} + +// ============================================================================= +// 验证结果 | Validation Result +// ============================================================================= + +/** + * @zh 组合验证结果 + * @en Composition validation result + */ +export interface CompositionValidationResult { + /** + * @zh 是否有效 + * @en Whether valid + */ + readonly isValid: boolean; + + /** + * @zh 错误列表 + * @en Error list + */ + readonly errors: CompositionError[]; + + /** + * @zh 警告列表 + * @en Warning list + */ + readonly warnings: CompositionWarning[]; +} + +/** + * @zh 组合错误 + * @en Composition error + */ +export interface CompositionError { + readonly type: 'missing-connection' | 'type-mismatch' | 'cycle-detected' | 'invalid-slot'; + readonly message: string; + readonly slotId?: string; + readonly pinName?: string; +} + +/** + * @zh 组合警告 + * @en Composition warning + */ +export interface CompositionWarning { + readonly type: 'unused-output' | 'unconnected-input'; + readonly message: string; + readonly slotId?: string; + readonly pinName?: string; +} + +// ============================================================================= +// 组合器实现 | Composer Implementation +// ============================================================================= + +/** + * @zh 蓝图组合器实现 + * @en Blueprint composer implementation + */ +export class BlueprintComposer implements IBlueprintComposer { + readonly name: string; + + private slots: Map = new Map(); + private connections: Map = new Map(); + private connectionIdCounter = 0; + + constructor(name: string) { + this.name = name; + } + + getSlots(): FragmentSlot[] { + return Array.from(this.slots.values()); + } + + getConnections(): SlotConnection[] { + return Array.from(this.connections.values()); + } + + addFragment( + fragment: IBlueprintFragment, + slotId: string, + options?: { + name?: string; + position?: { x: number; y: number }; + } + ): void { + if (this.slots.has(slotId)) { + throw new Error(`Slot '${slotId}' already exists`); + } + + const slot: FragmentSlot = { + id: slotId, + name: options?.name ?? fragment.name, + fragment, + position: options?.position ?? { x: 0, y: 0 } + }; + + this.slots.set(slotId, slot); + } + + removeSlot(slotId: string): void { + if (!this.slots.has(slotId)) { + return; + } + + // Remove all connections involving this slot + const toRemove: string[] = []; + for (const [id, conn] of this.connections) { + if (conn.fromSlotId === slotId || conn.toSlotId === slotId) { + toRemove.push(id); + } + } + for (const id of toRemove) { + this.connections.delete(id); + } + + this.slots.delete(slotId); + } + + connect( + fromSlotId: string, + fromPin: string, + toSlotId: string, + toPin: string + ): void { + const fromSlot = this.slots.get(fromSlotId); + const toSlot = this.slots.get(toSlotId); + + if (!fromSlot) { + throw new Error(`Source slot '${fromSlotId}' not found`); + } + if (!toSlot) { + throw new Error(`Target slot '${toSlotId}' not found`); + } + + const fromPinDef = fromSlot.fragment.outputs.find(p => p.name === fromPin); + const toPinDef = toSlot.fragment.inputs.find(p => p.name === toPin); + + if (!fromPinDef) { + throw new Error(`Output pin '${fromPin}' not found in slot '${fromSlotId}'`); + } + if (!toPinDef) { + throw new Error(`Input pin '${toPin}' not found in slot '${toSlotId}'`); + } + + const connectionId = `conn_${++this.connectionIdCounter}`; + + const connection: SlotConnection = { + id: connectionId, + fromSlotId, + fromPin, + toSlotId, + toPin + }; + + this.connections.set(connectionId, connection); + } + + disconnect(connectionId: string): void { + this.connections.delete(connectionId); + } + + validate(): CompositionValidationResult { + const errors: CompositionError[] = []; + const warnings: CompositionWarning[] = []; + + // Check for required inputs without connections + for (const slot of this.slots.values()) { + for (const input of slot.fragment.inputs) { + const hasConnection = Array.from(this.connections.values()).some( + c => c.toSlotId === slot.id && c.toPin === input.name + ); + + if (!hasConnection && input.defaultValue === undefined) { + warnings.push({ + type: 'unconnected-input', + message: `Input '${input.name}' in slot '${slot.id}' is not connected`, + slotId: slot.id, + pinName: input.name + }); + } + } + + // Check for unused outputs + for (const output of slot.fragment.outputs) { + const hasConnection = Array.from(this.connections.values()).some( + c => c.fromSlotId === slot.id && c.fromPin === output.name + ); + + if (!hasConnection) { + warnings.push({ + type: 'unused-output', + message: `Output '${output.name}' in slot '${slot.id}' is not connected`, + slotId: slot.id, + pinName: output.name + }); + } + } + } + + // Check type compatibility + for (const conn of this.connections.values()) { + const fromSlot = this.slots.get(conn.fromSlotId); + const toSlot = this.slots.get(conn.toSlotId); + + if (!fromSlot || !toSlot) { + errors.push({ + type: 'invalid-slot', + message: `Invalid slot reference in connection '${conn.id}'` + }); + continue; + } + + const fromPinDef = fromSlot.fragment.outputs.find(p => p.name === conn.fromPin); + const toPinDef = toSlot.fragment.inputs.find(p => p.name === conn.toPin); + + if (fromPinDef && toPinDef && fromPinDef.type !== toPinDef.type) { + if (fromPinDef.type !== 'any' && toPinDef.type !== 'any') { + errors.push({ + type: 'type-mismatch', + message: `Type mismatch: '${fromPinDef.type}' -> '${toPinDef.type}' in connection '${conn.id}'` + }); + } + } + } + + return { + isValid: errors.length === 0, + errors, + warnings + }; + } + + compile(): BlueprintAsset { + const nodes: BlueprintNode[] = []; + const connections: BlueprintConnection[] = []; + const variables: BlueprintVariable[] = []; + + const nodeIdMap = new Map>(); + + // Copy nodes from each fragment with new IDs + let nodeIdCounter = 0; + for (const slot of this.slots.values()) { + const slotNodeMap = new Map(); + nodeIdMap.set(slot.id, slotNodeMap); + + for (const node of slot.fragment.graph.nodes) { + const newNodeId = `node_${++nodeIdCounter}`; + slotNodeMap.set(node.id, newNodeId); + + nodes.push({ + ...node, + id: newNodeId, + position: { + x: node.position.x + slot.position.x, + y: node.position.y + slot.position.y + } + }); + } + + // Copy internal connections + for (const conn of slot.fragment.graph.connections) { + const newFromId = slotNodeMap.get(conn.fromNodeId); + const newToId = slotNodeMap.get(conn.toNodeId); + + if (newFromId && newToId) { + connections.push({ + ...conn, + id: `conn_internal_${connections.length}`, + fromNodeId: newFromId, + toNodeId: newToId + }); + } + } + + // Copy variables (with slot prefix to avoid conflicts) + for (const variable of slot.fragment.graph.variables) { + variables.push({ + ...variable, + name: `${slot.id}_${variable.name}` + }); + } + } + + // Create connections between slots based on exposed pins + for (const slotConn of this.connections.values()) { + const fromSlot = this.slots.get(slotConn.fromSlotId); + const toSlot = this.slots.get(slotConn.toSlotId); + + if (!fromSlot || !toSlot) continue; + + const fromPinDef = fromSlot.fragment.outputs.find(p => p.name === slotConn.fromPin); + const toPinDef = toSlot.fragment.inputs.find(p => p.name === slotConn.toPin); + + if (!fromPinDef || !toPinDef) continue; + + const fromNodeMap = nodeIdMap.get(slotConn.fromSlotId); + const toNodeMap = nodeIdMap.get(slotConn.toSlotId); + + if (!fromNodeMap || !toNodeMap) continue; + + const fromNodeId = fromNodeMap.get(fromPinDef.internalNodeId); + const toNodeId = toNodeMap.get(toPinDef.internalNodeId); + + if (fromNodeId && toNodeId) { + connections.push({ + id: `conn_slot_${connections.length}`, + fromNodeId, + fromPin: fromPinDef.internalPinName, + toNodeId, + toPin: toPinDef.internalPinName + }); + } + } + + return { + version: 1, + type: 'blueprint', + metadata: { + name: this.name, + description: `Composed from ${this.slots.size} fragments`, + createdAt: Date.now(), + modifiedAt: Date.now() + }, + variables, + nodes, + connections + }; + } + + clear(): void { + this.slots.clear(); + this.connections.clear(); + this.connectionIdCounter = 0; + } +} + +// ============================================================================= +// 工厂函数 | Factory Functions +// ============================================================================= + +/** + * @zh 创建蓝图组合器 + * @en Create blueprint composer + */ +export function createComposer(name: string): IBlueprintComposer { + return new BlueprintComposer(name); +} + +// ============================================================================= +// 组合资产格式 | Composition Asset Format +// ============================================================================= + +/** + * @zh 蓝图组合资产格式 + * @en Blueprint composition asset format + */ +export interface BlueprintCompositionAsset { + /** + * @zh 格式版本 + * @en Format version + */ + version: number; + + /** + * @zh 资产类型标识 + * @en Asset type identifier + */ + type: 'blueprint-composition'; + + /** + * @zh 组合名称 + * @en Composition name + */ + name: string; + + /** + * @zh 槽位数据 + * @en Slot data + */ + slots: Array<{ + id: string; + name: string; + fragmentId: string; + position: { x: number; y: number }; + }>; + + /** + * @zh 连接数据 + * @en Connection data + */ + connections: SlotConnection[]; +} diff --git a/packages/blueprint/src/composition/BlueprintFragment.ts b/packages/blueprint/src/composition/BlueprintFragment.ts new file mode 100644 index 00000000..fd7643a2 --- /dev/null +++ b/packages/blueprint/src/composition/BlueprintFragment.ts @@ -0,0 +1,351 @@ +/** + * @zh 蓝图片段接口和实现 + * @en Blueprint Fragment Interface and Implementation + * + * @zh 定义可重用的蓝图片段,用于组合系统 + * @en Defines reusable blueprint fragments for the composition system + */ + +import type { BlueprintAsset } from '../types/blueprint'; +import type { BlueprintPinType } from '../types/pins'; + +// ============================================================================= +// 暴露引脚定义 | Exposed Pin Definition +// ============================================================================= + +/** + * @zh 暴露引脚定义 + * @en Exposed pin definition + * + * @zh 片段对外暴露的引脚,可与其他片段连接 + * @en Pins exposed by the fragment that can be connected to other fragments + */ +export interface ExposedPin { + /** + * @zh 引脚名称 + * @en Pin name + */ + readonly name: string; + + /** + * @zh 显示名称 + * @en Display name + */ + readonly displayName: string; + + /** + * @zh 引脚类型 + * @en Pin type + */ + readonly type: BlueprintPinType; + + /** + * @zh 引脚方向 + * @en Pin direction + */ + readonly direction: 'input' | 'output'; + + /** + * @zh 描述 + * @en Description + */ + readonly description?: string; + + /** + * @zh 默认值(仅输入引脚) + * @en Default value (input pins only) + */ + readonly defaultValue?: unknown; + + /** + * @zh 关联的内部节点 ID + * @en Associated internal node ID + */ + readonly internalNodeId: string; + + /** + * @zh 关联的内部引脚名称 + * @en Associated internal pin name + */ + readonly internalPinName: string; +} + +// ============================================================================= +// 蓝图片段接口 | Blueprint Fragment Interface +// ============================================================================= + +/** + * @zh 蓝图片段接口 + * @en Blueprint fragment interface + * + * @zh 代表一个可重用的蓝图逻辑单元,如技能、卡牌效果等 + * @en Represents a reusable unit of blueprint logic, such as skills, card effects, etc. + */ +export interface IBlueprintFragment { + /** + * @zh 片段唯一标识 + * @en Fragment unique identifier + */ + readonly id: string; + + /** + * @zh 片段名称 + * @en Fragment name + */ + readonly name: string; + + /** + * @zh 片段描述 + * @en Fragment description + */ + readonly description?: string; + + /** + * @zh 片段分类 + * @en Fragment category + */ + readonly category?: string; + + /** + * @zh 片段标签 + * @en Fragment tags + */ + readonly tags?: string[]; + + /** + * @zh 暴露的输入引脚 + * @en Exposed input pins + */ + readonly inputs: ExposedPin[]; + + /** + * @zh 暴露的输出引脚 + * @en Exposed output pins + */ + readonly outputs: ExposedPin[]; + + /** + * @zh 内部蓝图图 + * @en Internal blueprint graph + */ + readonly graph: BlueprintAsset; + + /** + * @zh 片段版本 + * @en Fragment version + */ + readonly version?: string; + + /** + * @zh 图标名称 + * @en Icon name + */ + readonly icon?: string; + + /** + * @zh 颜色(用于可视化) + * @en Color (for visualization) + */ + readonly color?: string; +} + +// ============================================================================= +// 蓝图片段实现 | Blueprint Fragment Implementation +// ============================================================================= + +/** + * @zh 蓝图片段配置 + * @en Blueprint fragment configuration + */ +export interface BlueprintFragmentConfig { + id: string; + name: string; + description?: string; + category?: string; + tags?: string[]; + inputs?: ExposedPin[]; + outputs?: ExposedPin[]; + graph: BlueprintAsset; + version?: string; + icon?: string; + color?: string; +} + +/** + * @zh 蓝图片段实现 + * @en Blueprint fragment implementation + */ +export class BlueprintFragment implements IBlueprintFragment { + readonly id: string; + readonly name: string; + readonly description?: string; + readonly category?: string; + readonly tags?: string[]; + readonly inputs: ExposedPin[]; + readonly outputs: ExposedPin[]; + readonly graph: BlueprintAsset; + readonly version?: string; + readonly icon?: string; + readonly color?: string; + + constructor(config: BlueprintFragmentConfig) { + this.id = config.id; + this.name = config.name; + this.description = config.description; + this.category = config.category; + this.tags = config.tags; + this.inputs = config.inputs ?? []; + this.outputs = config.outputs ?? []; + this.graph = config.graph; + this.version = config.version; + this.icon = config.icon; + this.color = config.color; + } + + /** + * @zh 获取所有暴露引脚 + * @en Get all exposed pins + */ + getAllExposedPins(): ExposedPin[] { + return [...this.inputs, ...this.outputs]; + } + + /** + * @zh 通过名称查找输入引脚 + * @en Find input pin by name + */ + findInput(name: string): ExposedPin | undefined { + return this.inputs.find(p => p.name === name); + } + + /** + * @zh 通过名称查找输出引脚 + * @en Find output pin by name + */ + findOutput(name: string): ExposedPin | undefined { + return this.outputs.find(p => p.name === name); + } +} + +// ============================================================================= +// 工厂函数 | Factory Functions +// ============================================================================= + +/** + * @zh 创建暴露引脚 + * @en Create exposed pin + */ +export function createExposedPin( + name: string, + type: BlueprintPinType, + direction: 'input' | 'output', + internalNodeId: string, + internalPinName: string, + options?: { + displayName?: string; + description?: string; + defaultValue?: unknown; + } +): ExposedPin { + return { + name, + displayName: options?.displayName ?? name, + type, + direction, + description: options?.description, + defaultValue: options?.defaultValue, + internalNodeId, + internalPinName + }; +} + +/** + * @zh 创建蓝图片段 + * @en Create blueprint fragment + */ +export function createFragment(config: BlueprintFragmentConfig): IBlueprintFragment { + return new BlueprintFragment(config); +} + +// ============================================================================= +// 片段资产格式 | Fragment Asset Format +// ============================================================================= + +/** + * @zh 蓝图片段资产格式 + * @en Blueprint fragment asset format + * + * @zh 用于序列化和反序列化片段 + * @en Used for serializing and deserializing fragments + */ +export interface BlueprintFragmentAsset { + /** + * @zh 格式版本 + * @en Format version + */ + version: number; + + /** + * @zh 资产类型标识 + * @en Asset type identifier + */ + type: 'blueprint-fragment'; + + /** + * @zh 片段数据 + * @en Fragment data + */ + fragment: { + id: string; + name: string; + description?: string; + category?: string; + tags?: string[]; + inputs: ExposedPin[]; + outputs: ExposedPin[]; + version?: string; + icon?: string; + color?: string; + }; + + /** + * @zh 内部蓝图图 + * @en Internal blueprint graph + */ + graph: BlueprintAsset; +} + +/** + * @zh 从资产创建片段 + * @en Create fragment from asset + */ +export function fragmentFromAsset(asset: BlueprintFragmentAsset): IBlueprintFragment { + return new BlueprintFragment({ + ...asset.fragment, + graph: asset.graph + }); +} + +/** + * @zh 将片段转为资产 + * @en Convert fragment to asset + */ +export function fragmentToAsset(fragment: IBlueprintFragment): BlueprintFragmentAsset { + return { + version: 1, + type: 'blueprint-fragment', + fragment: { + id: fragment.id, + name: fragment.name, + description: fragment.description, + category: fragment.category, + tags: fragment.tags, + inputs: fragment.inputs, + outputs: fragment.outputs, + version: fragment.version, + icon: fragment.icon, + color: fragment.color + }, + graph: fragment.graph + }; +} diff --git a/packages/blueprint/src/composition/FragmentRegistry.ts b/packages/blueprint/src/composition/FragmentRegistry.ts new file mode 100644 index 00000000..26a0532e --- /dev/null +++ b/packages/blueprint/src/composition/FragmentRegistry.ts @@ -0,0 +1,208 @@ +/** + * @zh 片段注册表 + * @en Fragment Registry + * + * @zh 管理和查询蓝图片段 + * @en Manages and queries blueprint fragments + */ + +import type { IBlueprintFragment } from './BlueprintFragment'; + +// ============================================================================= +// 片段注册表接口 | Fragment Registry Interface +// ============================================================================= + +/** + * @zh 片段过滤器 + * @en Fragment filter + */ +export interface FragmentFilter { + /** + * @zh 按分类过滤 + * @en Filter by category + */ + category?: string; + + /** + * @zh 按标签过滤(任意匹配) + * @en Filter by tags (any match) + */ + tags?: string[]; + + /** + * @zh 按名称搜索 + * @en Search by name + */ + search?: string; +} + +/** + * @zh 片段注册表接口 + * @en Fragment registry interface + */ +export interface IFragmentRegistry { + /** + * @zh 注册片段 + * @en Register fragment + */ + register(fragment: IBlueprintFragment): void; + + /** + * @zh 注销片段 + * @en Unregister fragment + */ + unregister(id: string): void; + + /** + * @zh 获取片段 + * @en Get fragment + */ + get(id: string): IBlueprintFragment | undefined; + + /** + * @zh 检查片段是否存在 + * @en Check if fragment exists + */ + has(id: string): boolean; + + /** + * @zh 获取所有片段 + * @en Get all fragments + */ + getAll(): IBlueprintFragment[]; + + /** + * @zh 按条件过滤片段 + * @en Filter fragments by criteria + */ + filter(filter: FragmentFilter): IBlueprintFragment[]; + + /** + * @zh 获取所有分类 + * @en Get all categories + */ + getCategories(): string[]; + + /** + * @zh 获取所有标签 + * @en Get all tags + */ + getTags(): string[]; + + /** + * @zh 清空注册表 + * @en Clear registry + */ + clear(): void; +} + +// ============================================================================= +// 片段注册表实现 | Fragment Registry Implementation +// ============================================================================= + +/** + * @zh 片段注册表实现 + * @en Fragment registry implementation + */ +export class FragmentRegistry implements IFragmentRegistry { + private fragments: Map = new Map(); + + register(fragment: IBlueprintFragment): void { + if (this.fragments.has(fragment.id)) { + console.warn(`Fragment '${fragment.id}' already registered, overwriting`); + } + this.fragments.set(fragment.id, fragment); + } + + unregister(id: string): void { + this.fragments.delete(id); + } + + get(id: string): IBlueprintFragment | undefined { + return this.fragments.get(id); + } + + has(id: string): boolean { + return this.fragments.has(id); + } + + getAll(): IBlueprintFragment[] { + return Array.from(this.fragments.values()); + } + + filter(filter: FragmentFilter): IBlueprintFragment[] { + let results = this.getAll(); + + if (filter.category) { + results = results.filter(f => f.category === filter.category); + } + + if (filter.tags && filter.tags.length > 0) { + results = results.filter(f => + f.tags && filter.tags!.some(t => f.tags!.includes(t)) + ); + } + + if (filter.search) { + const searchLower = filter.search.toLowerCase(); + results = results.filter(f => + f.name.toLowerCase().includes(searchLower) || + f.description?.toLowerCase().includes(searchLower) + ); + } + + return results; + } + + getCategories(): string[] { + const categories = new Set(); + for (const fragment of this.fragments.values()) { + if (fragment.category) { + categories.add(fragment.category); + } + } + return Array.from(categories).sort(); + } + + getTags(): string[] { + const tags = new Set(); + for (const fragment of this.fragments.values()) { + if (fragment.tags) { + for (const tag of fragment.tags) { + tags.add(tag); + } + } + } + return Array.from(tags).sort(); + } + + clear(): void { + this.fragments.clear(); + } + + /** + * @zh 获取片段数量 + * @en Get fragment count + */ + get size(): number { + return this.fragments.size; + } +} + +// ============================================================================= +// 单例实例 | Singleton Instance +// ============================================================================= + +/** + * @zh 默认片段注册表实例 + * @en Default fragment registry instance + */ +export const defaultFragmentRegistry = new FragmentRegistry(); + +/** + * @zh 创建片段注册表 + * @en Create fragment registry + */ +export function createFragmentRegistry(): IFragmentRegistry { + return new FragmentRegistry(); +} diff --git a/packages/blueprint/src/composition/index.ts b/packages/blueprint/src/composition/index.ts new file mode 100644 index 00000000..eaabe1c9 --- /dev/null +++ b/packages/blueprint/src/composition/index.ts @@ -0,0 +1,57 @@ +/** + * @zh 蓝图组合系统导出 + * @en Blueprint Composition System Export + */ + +// ============================================================================= +// 片段 | Fragment +// ============================================================================= + +export type { + ExposedPin, + IBlueprintFragment, + BlueprintFragmentConfig, + BlueprintFragmentAsset +} from './BlueprintFragment'; + +export { + BlueprintFragment, + createExposedPin, + createFragment, + fragmentFromAsset, + fragmentToAsset +} from './BlueprintFragment'; + +// ============================================================================= +// 组合器 | Composer +// ============================================================================= + +export type { + FragmentSlot, + SlotConnection, + IBlueprintComposer, + CompositionValidationResult, + CompositionError, + CompositionWarning, + BlueprintCompositionAsset +} from './BlueprintComposer'; + +export { + BlueprintComposer, + createComposer +} from './BlueprintComposer'; + +// ============================================================================= +// 注册表 | Registry +// ============================================================================= + +export type { + FragmentFilter, + IFragmentRegistry +} from './FragmentRegistry'; + +export { + FragmentRegistry, + defaultFragmentRegistry, + createFragmentRegistry +} from './FragmentRegistry'; diff --git a/packages/blueprint/src/index.ts b/packages/blueprint/src/index.ts index dbf05e60..fa68e973 100644 --- a/packages/blueprint/src/index.ts +++ b/packages/blueprint/src/index.ts @@ -12,6 +12,9 @@ export * from './runtime'; // Triggers export * from './triggers'; +// Composition +export * from './composition'; + // Nodes (import to register) import './nodes';