feat(blueprint): 蓝图组合系统 (#326)
* feat(blueprint): 添加蓝图组合系统 - 添加 IBlueprintFragment 接口和实现,支持可重用蓝图片段 - 添加 IBlueprintComposer 接口和实现,支持片段组合 - 添加 FragmentRegistry 片段注册表 - 支持暴露引脚连接和编译成完整蓝图 - 支持片段资产序列化格式 * fix(blueprint): 移除未使用的 ExposedPin 导入
This commit is contained in:
575
packages/blueprint/src/composition/BlueprintComposer.ts
Normal file
575
packages/blueprint/src/composition/BlueprintComposer.ts
Normal file
@@ -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<string, FragmentSlot> = new Map();
|
||||
private connections: Map<string, SlotConnection> = 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<string, Map<string, string>>();
|
||||
|
||||
// Copy nodes from each fragment with new IDs
|
||||
let nodeIdCounter = 0;
|
||||
for (const slot of this.slots.values()) {
|
||||
const slotNodeMap = new Map<string, string>();
|
||||
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[];
|
||||
}
|
||||
351
packages/blueprint/src/composition/BlueprintFragment.ts
Normal file
351
packages/blueprint/src/composition/BlueprintFragment.ts
Normal file
@@ -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
|
||||
};
|
||||
}
|
||||
208
packages/blueprint/src/composition/FragmentRegistry.ts
Normal file
208
packages/blueprint/src/composition/FragmentRegistry.ts
Normal file
@@ -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<string, IBlueprintFragment> = 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<string>();
|
||||
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<string>();
|
||||
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();
|
||||
}
|
||||
57
packages/blueprint/src/composition/index.ts
Normal file
57
packages/blueprint/src/composition/index.ts
Normal file
@@ -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';
|
||||
@@ -12,6 +12,9 @@ export * from './runtime';
|
||||
// Triggers
|
||||
export * from './triggers';
|
||||
|
||||
// Composition
|
||||
export * from './composition';
|
||||
|
||||
// Nodes (import to register)
|
||||
import './nodes';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user