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
|
// Triggers
|
||||||
export * from './triggers';
|
export * from './triggers';
|
||||||
|
|
||||||
|
// Composition
|
||||||
|
export * from './composition';
|
||||||
|
|
||||||
// Nodes (import to register)
|
// Nodes (import to register)
|
||||||
import './nodes';
|
import './nodes';
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user