Feature/physics and tilemap enhancement (#247)
* feat(behavior-tree,tilemap): 修复编辑器连线缩放问题并增强插件系统 * feat(node-editor,blueprint): 新增通用节点编辑器和蓝图可视化脚本系统 * feat(editor,tilemap): 优化编辑器UI样式和Tilemap编辑器功能 * fix: 修复CodeQL安全警告和CI类型检查错误 * fix: 修复CodeQL安全警告和CI类型检查错误 * fix: 修复CodeQL安全警告和CI类型检查错误
This commit is contained in:
156
packages/node-editor/src/domain/models/Connection.ts
Normal file
156
packages/node-editor/src/domain/models/Connection.ts
Normal file
@@ -0,0 +1,156 @@
|
||||
import { PinCategory } from '../value-objects/PinType';
|
||||
|
||||
/**
|
||||
* Connection - Represents a link between two pins
|
||||
* 连接 - 表示两个引脚之间的链接
|
||||
*/
|
||||
export class Connection {
|
||||
private readonly _id: string;
|
||||
private readonly _fromNodeId: string;
|
||||
private readonly _fromPinId: string;
|
||||
private readonly _toNodeId: string;
|
||||
private readonly _toPinId: string;
|
||||
private readonly _category: PinCategory;
|
||||
|
||||
constructor(
|
||||
id: string,
|
||||
fromNodeId: string,
|
||||
fromPinId: string,
|
||||
toNodeId: string,
|
||||
toPinId: string,
|
||||
category: PinCategory
|
||||
) {
|
||||
this._id = id;
|
||||
this._fromNodeId = fromNodeId;
|
||||
this._fromPinId = fromPinId;
|
||||
this._toNodeId = toNodeId;
|
||||
this._toPinId = toPinId;
|
||||
this._category = category;
|
||||
}
|
||||
|
||||
get id(): string {
|
||||
return this._id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Source node ID (output side)
|
||||
* 源节点ID(输出端)
|
||||
*/
|
||||
get fromNodeId(): string {
|
||||
return this._fromNodeId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Source pin ID (output side)
|
||||
* 源引脚ID(输出端)
|
||||
*/
|
||||
get fromPinId(): string {
|
||||
return this._fromPinId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Target node ID (input side)
|
||||
* 目标节点ID(输入端)
|
||||
*/
|
||||
get toNodeId(): string {
|
||||
return this._toNodeId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Target pin ID (input side)
|
||||
* 目标引脚ID(输入端)
|
||||
*/
|
||||
get toPinId(): string {
|
||||
return this._toPinId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Connection category determines the wire color
|
||||
* 连接类别决定连线颜色
|
||||
*/
|
||||
get category(): PinCategory {
|
||||
return this._category;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this is an execution flow connection
|
||||
* 是否是执行流连接
|
||||
*/
|
||||
get isExec(): boolean {
|
||||
return this._category === 'exec';
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this connection involves a specific node
|
||||
* 检查此连接是否涉及特定节点
|
||||
*/
|
||||
involvesNode(nodeId: string): boolean {
|
||||
return this._fromNodeId === nodeId || this._toNodeId === nodeId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this connection involves a specific pin
|
||||
* 检查此连接是否涉及特定引脚
|
||||
*/
|
||||
involvesPin(pinId: string): boolean {
|
||||
return this._fromPinId === pinId || this._toPinId === pinId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this connection matches the given endpoints
|
||||
* 检查此连接是否匹配给定的端点
|
||||
*/
|
||||
matches(fromPinId: string, toPinId: string): boolean {
|
||||
return this._fromPinId === fromPinId && this._toPinId === toPinId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks equality with another connection
|
||||
* 检查与另一个连接是否相等
|
||||
*/
|
||||
equals(other: Connection): boolean {
|
||||
return (
|
||||
this._fromNodeId === other._fromNodeId &&
|
||||
this._fromPinId === other._fromPinId &&
|
||||
this._toNodeId === other._toNodeId &&
|
||||
this._toPinId === other._toPinId
|
||||
);
|
||||
}
|
||||
|
||||
toJSON(): Record<string, unknown> {
|
||||
return {
|
||||
id: this._id,
|
||||
fromNodeId: this._fromNodeId,
|
||||
fromPinId: this._fromPinId,
|
||||
toNodeId: this._toNodeId,
|
||||
toPinId: this._toPinId,
|
||||
category: this._category
|
||||
};
|
||||
}
|
||||
|
||||
static fromJSON(json: {
|
||||
id: string;
|
||||
fromNodeId: string;
|
||||
fromPinId: string;
|
||||
toNodeId: string;
|
||||
toPinId: string;
|
||||
category: PinCategory;
|
||||
}): Connection {
|
||||
return new Connection(
|
||||
json.id,
|
||||
json.fromNodeId,
|
||||
json.fromPinId,
|
||||
json.toNodeId,
|
||||
json.toPinId,
|
||||
json.category
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a connection ID from pin IDs
|
||||
* 从引脚ID创建连接ID
|
||||
*/
|
||||
static createId(fromPinId: string, toPinId: string): string {
|
||||
return `${fromPinId}->${toPinId}`;
|
||||
}
|
||||
}
|
||||
271
packages/node-editor/src/domain/models/Graph.ts
Normal file
271
packages/node-editor/src/domain/models/Graph.ts
Normal file
@@ -0,0 +1,271 @@
|
||||
import { GraphNode } from './GraphNode';
|
||||
import { Connection } from './Connection';
|
||||
import { Pin } from './Pin';
|
||||
import { Position } from '../value-objects/Position';
|
||||
|
||||
/**
|
||||
* Graph - Aggregate root for the node graph
|
||||
* 图 - 节点图的聚合根
|
||||
*
|
||||
* This class is immutable - all modification methods return new instances.
|
||||
* 此类是不可变的 - 所有修改方法返回新实例
|
||||
*/
|
||||
export class Graph {
|
||||
private readonly _id: string;
|
||||
private readonly _name: string;
|
||||
private readonly _nodes: Map<string, GraphNode>;
|
||||
private readonly _connections: Connection[];
|
||||
private readonly _metadata: Record<string, unknown>;
|
||||
|
||||
constructor(
|
||||
id: string,
|
||||
name: string,
|
||||
nodes: GraphNode[] = [],
|
||||
connections: Connection[] = [],
|
||||
metadata: Record<string, unknown> = {}
|
||||
) {
|
||||
this._id = id;
|
||||
this._name = name;
|
||||
this._nodes = new Map(nodes.map(n => [n.id, n]));
|
||||
this._connections = [...connections];
|
||||
this._metadata = { ...metadata };
|
||||
}
|
||||
|
||||
get id(): string {
|
||||
return this._id;
|
||||
}
|
||||
|
||||
get name(): string {
|
||||
return this._name;
|
||||
}
|
||||
|
||||
get nodes(): GraphNode[] {
|
||||
return Array.from(this._nodes.values());
|
||||
}
|
||||
|
||||
get connections(): Connection[] {
|
||||
return [...this._connections];
|
||||
}
|
||||
|
||||
get metadata(): Record<string, unknown> {
|
||||
return { ...this._metadata };
|
||||
}
|
||||
|
||||
get nodeCount(): number {
|
||||
return this._nodes.size;
|
||||
}
|
||||
|
||||
get connectionCount(): number {
|
||||
return this._connections.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a node by ID
|
||||
* 通过ID获取节点
|
||||
*/
|
||||
getNode(nodeId: string): GraphNode | undefined {
|
||||
return this._nodes.get(nodeId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a pin by its full ID
|
||||
* 通过完整ID获取引脚
|
||||
*/
|
||||
getPin(pinId: string): Pin | undefined {
|
||||
for (const node of this._nodes.values()) {
|
||||
const pin = node.getPin(pinId);
|
||||
if (pin) return pin;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all connections involving a node
|
||||
* 获取涉及某节点的所有连接
|
||||
*/
|
||||
getNodeConnections(nodeId: string): Connection[] {
|
||||
return this._connections.filter(c => c.involvesNode(nodeId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all connections to/from a specific pin
|
||||
* 获取特定引脚的所有连接
|
||||
*/
|
||||
getPinConnections(pinId: string): Connection[] {
|
||||
return this._connections.filter(c => c.involvesPin(pinId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a pin is connected
|
||||
* 检查引脚是否已连接
|
||||
*/
|
||||
isPinConnected(pinId: string): boolean {
|
||||
return this._connections.some(c => c.involvesPin(pinId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new node to the graph (immutable)
|
||||
* 向图中添加新节点(不可变)
|
||||
*/
|
||||
addNode(node: GraphNode): Graph {
|
||||
if (this._nodes.has(node.id)) {
|
||||
throw new Error(`Node with ID "${node.id}" already exists`);
|
||||
}
|
||||
const newNodes = [...this.nodes, node];
|
||||
return new Graph(this._id, this._name, newNodes, this._connections, this._metadata);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a node and its connections (immutable)
|
||||
* 移除节点及其连接(不可变)
|
||||
*/
|
||||
removeNode(nodeId: string): Graph {
|
||||
if (!this._nodes.has(nodeId)) {
|
||||
return this;
|
||||
}
|
||||
const newNodes = this.nodes.filter(n => n.id !== nodeId);
|
||||
const newConnections = this._connections.filter(c => !c.involvesNode(nodeId));
|
||||
return new Graph(this._id, this._name, newNodes, newConnections, this._metadata);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a node (immutable)
|
||||
* 更新节点(不可变)
|
||||
*/
|
||||
updateNode(nodeId: string, updater: (node: GraphNode) => GraphNode): Graph {
|
||||
const node = this._nodes.get(nodeId);
|
||||
if (!node) return this;
|
||||
|
||||
const updatedNode = updater(node);
|
||||
const newNodes = this.nodes.map(n => n.id === nodeId ? updatedNode : n);
|
||||
return new Graph(this._id, this._name, newNodes, this._connections, this._metadata);
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves a node to a new position (immutable)
|
||||
* 移动节点到新位置(不可变)
|
||||
*/
|
||||
moveNode(nodeId: string, newPosition: Position): Graph {
|
||||
return this.updateNode(nodeId, node => node.moveTo(newPosition));
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a connection between two pins (immutable)
|
||||
* 在两个引脚之间添加连接(不可变)
|
||||
*/
|
||||
addConnection(connection: Connection): Graph {
|
||||
// Validate connection
|
||||
// 验证连接
|
||||
const fromPin = this.getPin(connection.fromPinId);
|
||||
const toPin = this.getPin(connection.toPinId);
|
||||
|
||||
if (!fromPin || !toPin) {
|
||||
throw new Error('Invalid connection: pin not found');
|
||||
}
|
||||
|
||||
if (!fromPin.canConnectTo(toPin)) {
|
||||
throw new Error('Invalid connection: incompatible pin types');
|
||||
}
|
||||
|
||||
// Check for duplicate connections
|
||||
// 检查重复连接
|
||||
const exists = this._connections.some(c =>
|
||||
c.matches(connection.fromPinId, connection.toPinId)
|
||||
);
|
||||
if (exists) {
|
||||
return this;
|
||||
}
|
||||
|
||||
// Remove existing connection to input pin if it doesn't allow multiple
|
||||
// 如果输入引脚不允许多连接,移除现有连接
|
||||
let newConnections = [...this._connections];
|
||||
if (!toPin.allowMultiple) {
|
||||
newConnections = newConnections.filter(c => c.toPinId !== connection.toPinId);
|
||||
}
|
||||
|
||||
newConnections.push(connection);
|
||||
return new Graph(this._id, this._name, this.nodes, newConnections, this._metadata);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a connection (immutable)
|
||||
* 移除连接(不可变)
|
||||
*/
|
||||
removeConnection(connectionId: string): Graph {
|
||||
const newConnections = this._connections.filter(c => c.id !== connectionId);
|
||||
if (newConnections.length === this._connections.length) {
|
||||
return this;
|
||||
}
|
||||
return new Graph(this._id, this._name, this.nodes, newConnections, this._metadata);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all connections to/from a pin (immutable)
|
||||
* 移除引脚的所有连接(不可变)
|
||||
*/
|
||||
disconnectPin(pinId: string): Graph {
|
||||
const newConnections = this._connections.filter(c => !c.involvesPin(pinId));
|
||||
if (newConnections.length === this._connections.length) {
|
||||
return this;
|
||||
}
|
||||
return new Graph(this._id, this._name, this.nodes, newConnections, this._metadata);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates graph metadata (immutable)
|
||||
* 更新图元数据(不可变)
|
||||
*/
|
||||
setMetadata(metadata: Record<string, unknown>): Graph {
|
||||
return new Graph(this._id, this._name, this.nodes, this._connections, {
|
||||
...this._metadata,
|
||||
...metadata
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new graph with updated name (immutable)
|
||||
* 创建具有更新名称的新图(不可变)
|
||||
*/
|
||||
rename(newName: string): Graph {
|
||||
return new Graph(this._id, newName, this.nodes, this._connections, this._metadata);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the graph structure
|
||||
* 验证图结构
|
||||
*/
|
||||
validate(): string[] {
|
||||
const errors: string[] = [];
|
||||
|
||||
// Check for orphan connections
|
||||
// 检查孤立连接
|
||||
for (const conn of this._connections) {
|
||||
if (!this.getPin(conn.fromPinId)) {
|
||||
errors.push(`Connection "${conn.id}" references non-existent source pin`);
|
||||
}
|
||||
if (!this.getPin(conn.toPinId)) {
|
||||
errors.push(`Connection "${conn.id}" references non-existent target pin`);
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
toJSON(): Record<string, unknown> {
|
||||
return {
|
||||
id: this._id,
|
||||
name: this._name,
|
||||
nodes: this.nodes.map(n => n.toJSON()),
|
||||
connections: this._connections.map(c => c.toJSON()),
|
||||
metadata: this._metadata
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an empty graph
|
||||
* 创建空图
|
||||
*/
|
||||
static empty(id: string, name: string): Graph {
|
||||
return new Graph(id, name, [], [], {});
|
||||
}
|
||||
}
|
||||
267
packages/node-editor/src/domain/models/GraphNode.ts
Normal file
267
packages/node-editor/src/domain/models/GraphNode.ts
Normal file
@@ -0,0 +1,267 @@
|
||||
import { Position } from '../value-objects/Position';
|
||||
import { Pin, PinDefinition } from './Pin';
|
||||
|
||||
/**
|
||||
* Node category determines the visual style of the node header
|
||||
* 节点类别决定节点头部的视觉样式
|
||||
*/
|
||||
export type NodeCategory =
|
||||
| 'event' // Event node - triggers execution (事件节点 - 触发执行)
|
||||
| 'function' // Function call node (函数调用节点)
|
||||
| 'pure' // Pure function - no execution pins (纯函数 - 无执行引脚)
|
||||
| 'flow' // Flow control - branch, loop, etc. (流程控制 - 分支、循环等)
|
||||
| 'variable' // Variable get/set (变量读写)
|
||||
| 'literal' // Literal value input (字面量输入)
|
||||
| 'comment' // Comment node (注释节点)
|
||||
| 'custom'; // Custom category with user-defined color (自定义类别)
|
||||
|
||||
/**
|
||||
* Node template definition for creating nodes
|
||||
* 用于创建节点的节点模板定义
|
||||
*/
|
||||
export interface NodeTemplate {
|
||||
/** Unique template identifier (唯一模板标识符) */
|
||||
id: string;
|
||||
|
||||
/** Display title (显示标题) */
|
||||
title: string;
|
||||
|
||||
/** Optional subtitle (可选副标题) */
|
||||
subtitle?: string;
|
||||
|
||||
/** Node category for styling (节点类别用于样式) */
|
||||
category: NodeCategory;
|
||||
|
||||
/** Custom header color override (自定义头部颜色覆盖) */
|
||||
headerColor?: string;
|
||||
|
||||
/** Icon name from icon library (图标库中的图标名称) */
|
||||
icon?: string;
|
||||
|
||||
/** Input pin definitions (输入引脚定义) */
|
||||
inputPins: Omit<PinDefinition, 'direction'>[];
|
||||
|
||||
/** Output pin definitions (输出引脚定义) */
|
||||
outputPins: Omit<PinDefinition, 'direction'>[];
|
||||
|
||||
/** Whether the node can be collapsed (节点是否可折叠) */
|
||||
collapsible?: boolean;
|
||||
|
||||
/** Whether to show the title bar (是否显示标题栏) */
|
||||
showHeader?: boolean;
|
||||
|
||||
/** Minimum width in pixels (最小宽度,像素) */
|
||||
minWidth?: number;
|
||||
|
||||
/** Category path for node palette (节点面板的分类路径) */
|
||||
path?: string[];
|
||||
|
||||
/** Search keywords (搜索关键词) */
|
||||
keywords?: string[];
|
||||
|
||||
/** Description for documentation (文档描述) */
|
||||
description?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* GraphNode - Represents a node instance in the graph
|
||||
* 图节点 - 表示图中的节点实例
|
||||
*/
|
||||
export class GraphNode {
|
||||
private readonly _id: string;
|
||||
private readonly _templateId: string;
|
||||
private _position: Position;
|
||||
private readonly _category: NodeCategory;
|
||||
private readonly _title: string;
|
||||
private readonly _subtitle?: string;
|
||||
private readonly _icon?: string;
|
||||
private readonly _headerColor?: string;
|
||||
private readonly _inputPins: Pin[];
|
||||
private readonly _outputPins: Pin[];
|
||||
private _isCollapsed: boolean;
|
||||
private _comment?: string;
|
||||
private _data: Record<string, unknown>;
|
||||
|
||||
constructor(
|
||||
id: string,
|
||||
template: NodeTemplate,
|
||||
position: Position,
|
||||
data: Record<string, unknown> = {}
|
||||
) {
|
||||
this._id = id;
|
||||
this._templateId = template.id;
|
||||
this._position = position;
|
||||
this._category = template.category;
|
||||
this._title = template.title;
|
||||
this._subtitle = template.subtitle;
|
||||
this._icon = template.icon;
|
||||
this._headerColor = template.headerColor;
|
||||
this._isCollapsed = false;
|
||||
this._data = { ...data };
|
||||
|
||||
// Create input pins (创建输入引脚)
|
||||
this._inputPins = template.inputPins.map((def, index) =>
|
||||
new Pin(
|
||||
`${id}_in_${index}`,
|
||||
id,
|
||||
{ ...def, direction: 'input' }
|
||||
)
|
||||
);
|
||||
|
||||
// Create output pins (创建输出引脚)
|
||||
this._outputPins = template.outputPins.map((def, index) =>
|
||||
new Pin(
|
||||
`${id}_out_${index}`,
|
||||
id,
|
||||
{ ...def, direction: 'output' }
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
get id(): string {
|
||||
return this._id;
|
||||
}
|
||||
|
||||
get templateId(): string {
|
||||
return this._templateId;
|
||||
}
|
||||
|
||||
get position(): Position {
|
||||
return this._position;
|
||||
}
|
||||
|
||||
get category(): NodeCategory {
|
||||
return this._category;
|
||||
}
|
||||
|
||||
get title(): string {
|
||||
return this._title;
|
||||
}
|
||||
|
||||
get subtitle(): string | undefined {
|
||||
return this._subtitle;
|
||||
}
|
||||
|
||||
get icon(): string | undefined {
|
||||
return this._icon;
|
||||
}
|
||||
|
||||
get headerColor(): string | undefined {
|
||||
return this._headerColor;
|
||||
}
|
||||
|
||||
get inputPins(): readonly Pin[] {
|
||||
return this._inputPins;
|
||||
}
|
||||
|
||||
get outputPins(): readonly Pin[] {
|
||||
return this._outputPins;
|
||||
}
|
||||
|
||||
get allPins(): readonly Pin[] {
|
||||
return [...this._inputPins, ...this._outputPins];
|
||||
}
|
||||
|
||||
get isCollapsed(): boolean {
|
||||
return this._isCollapsed;
|
||||
}
|
||||
|
||||
get comment(): string | undefined {
|
||||
return this._comment;
|
||||
}
|
||||
|
||||
get data(): Record<string, unknown> {
|
||||
return { ...this._data };
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a pin by its ID
|
||||
* 通过ID获取引脚
|
||||
*/
|
||||
getPin(pinId: string): Pin | undefined {
|
||||
return this.allPins.find(p => p.id === pinId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a pin by its name
|
||||
* 通过名称获取引脚
|
||||
*/
|
||||
getPinByName(name: string, direction: 'input' | 'output'): Pin | undefined {
|
||||
const pins = direction === 'input' ? this._inputPins : this._outputPins;
|
||||
return pins.find(p => p.name === name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the execution input pin if exists
|
||||
* 获取执行输入引脚(如果存在)
|
||||
*/
|
||||
getExecInput(): Pin | undefined {
|
||||
return this._inputPins.find(p => p.isExec);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all execution output pins
|
||||
* 获取所有执行输出引脚
|
||||
*/
|
||||
getExecOutputs(): Pin[] {
|
||||
return this._outputPins.filter(p => p.isExec);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new node with updated position (immutable)
|
||||
* 创建具有更新位置的新节点(不可变)
|
||||
*/
|
||||
moveTo(newPosition: Position): GraphNode {
|
||||
const node = this.clone();
|
||||
node._position = newPosition;
|
||||
return node;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new node with collapse state toggled (immutable)
|
||||
* 创建切换折叠状态的新节点(不可变)
|
||||
*/
|
||||
toggleCollapse(): GraphNode {
|
||||
const node = this.clone();
|
||||
node._isCollapsed = !node._isCollapsed;
|
||||
return node;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new node with updated comment (immutable)
|
||||
* 创建具有更新注释的新节点(不可变)
|
||||
*/
|
||||
setComment(comment: string | undefined): GraphNode {
|
||||
const node = this.clone();
|
||||
node._comment = comment;
|
||||
return node;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new node with updated data (immutable)
|
||||
* 创建具有更新数据的新节点(不可变)
|
||||
*/
|
||||
updateData(data: Record<string, unknown>): GraphNode {
|
||||
const node = this.clone();
|
||||
node._data = { ...node._data, ...data };
|
||||
return node;
|
||||
}
|
||||
|
||||
private clone(): GraphNode {
|
||||
const cloned = Object.create(GraphNode.prototype) as GraphNode;
|
||||
Object.assign(cloned, this);
|
||||
cloned._data = { ...this._data };
|
||||
return cloned;
|
||||
}
|
||||
|
||||
toJSON(): Record<string, unknown> {
|
||||
return {
|
||||
id: this._id,
|
||||
templateId: this._templateId,
|
||||
position: this._position.toJSON(),
|
||||
isCollapsed: this._isCollapsed,
|
||||
comment: this._comment,
|
||||
data: this._data
|
||||
};
|
||||
}
|
||||
}
|
||||
187
packages/node-editor/src/domain/models/Pin.ts
Normal file
187
packages/node-editor/src/domain/models/Pin.ts
Normal file
@@ -0,0 +1,187 @@
|
||||
import { PinType, PinDirection, PinCategory, PinShape } from '../value-objects/PinType';
|
||||
|
||||
/**
|
||||
* Pin definition for node templates
|
||||
* 节点模板的引脚定义
|
||||
*/
|
||||
export interface PinDefinition {
|
||||
/** Unique identifier within the node (节点内的唯一标识符) */
|
||||
name: string;
|
||||
|
||||
/** Display name shown in UI (UI中显示的名称) */
|
||||
displayName: string;
|
||||
|
||||
/** Pin direction (引脚方向) */
|
||||
direction: PinDirection;
|
||||
|
||||
/** Pin data type category (引脚数据类型分类) */
|
||||
category: PinCategory;
|
||||
|
||||
/** Subtype for struct/enum (结构体/枚举的子类型) */
|
||||
subType?: string;
|
||||
|
||||
/** Whether this pin accepts array type (是否接受数组类型) */
|
||||
isArray?: boolean;
|
||||
|
||||
/** Default value when not connected (未连接时的默认值) */
|
||||
defaultValue?: unknown;
|
||||
|
||||
/** Whether multiple connections are allowed (是否允许多个连接) */
|
||||
allowMultiple?: boolean;
|
||||
|
||||
/** Whether this pin is hidden by default (是否默认隐藏) */
|
||||
hidden?: boolean;
|
||||
|
||||
/** Custom color override (自定义颜色覆盖) */
|
||||
color?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pin - Represents a connection point on a node
|
||||
* 引脚 - 表示节点上的连接点
|
||||
*/
|
||||
export class Pin {
|
||||
private readonly _id: string;
|
||||
private readonly _nodeId: string;
|
||||
private readonly _name: string;
|
||||
private readonly _displayName: string;
|
||||
private readonly _direction: PinDirection;
|
||||
private readonly _type: PinType;
|
||||
private readonly _defaultValue: unknown;
|
||||
private readonly _allowMultiple: boolean;
|
||||
private readonly _hidden: boolean;
|
||||
private readonly _color?: string;
|
||||
|
||||
constructor(
|
||||
id: string,
|
||||
nodeId: string,
|
||||
definition: PinDefinition
|
||||
) {
|
||||
this._id = id;
|
||||
this._nodeId = nodeId;
|
||||
this._name = definition.name;
|
||||
this._displayName = definition.displayName;
|
||||
this._direction = definition.direction;
|
||||
this._type = new PinType(
|
||||
definition.category,
|
||||
definition.subType,
|
||||
definition.isArray ?? false
|
||||
);
|
||||
this._defaultValue = definition.defaultValue;
|
||||
this._allowMultiple = definition.allowMultiple ?? (definition.category === 'exec' && definition.direction === 'output');
|
||||
this._hidden = definition.hidden ?? false;
|
||||
this._color = definition.color;
|
||||
}
|
||||
|
||||
get id(): string {
|
||||
return this._id;
|
||||
}
|
||||
|
||||
get nodeId(): string {
|
||||
return this._nodeId;
|
||||
}
|
||||
|
||||
get name(): string {
|
||||
return this._name;
|
||||
}
|
||||
|
||||
get displayName(): string {
|
||||
return this._displayName;
|
||||
}
|
||||
|
||||
get direction(): PinDirection {
|
||||
return this._direction;
|
||||
}
|
||||
|
||||
get type(): PinType {
|
||||
return this._type;
|
||||
}
|
||||
|
||||
get category(): PinCategory {
|
||||
return this._type.category;
|
||||
}
|
||||
|
||||
get shape(): PinShape {
|
||||
return this._type.shape;
|
||||
}
|
||||
|
||||
get defaultValue(): unknown {
|
||||
return this._defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether multiple connections are allowed
|
||||
* 是否允许多个连接
|
||||
*/
|
||||
get allowMultiple(): boolean {
|
||||
return this._allowMultiple;
|
||||
}
|
||||
|
||||
get hidden(): boolean {
|
||||
return this._hidden;
|
||||
}
|
||||
|
||||
get color(): string | undefined {
|
||||
return this._color;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this is an execution flow pin
|
||||
* 是否是执行流引脚
|
||||
*/
|
||||
get isExec(): boolean {
|
||||
return this._type.category === 'exec';
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this is an input pin
|
||||
* 是否是输入引脚
|
||||
*/
|
||||
get isInput(): boolean {
|
||||
return this._direction === 'input';
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this is an output pin
|
||||
* 是否是输出引脚
|
||||
*/
|
||||
get isOutput(): boolean {
|
||||
return this._direction === 'output';
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this pin can connect to another pin
|
||||
* 检查此引脚是否可以连接到另一个引脚
|
||||
*/
|
||||
canConnectTo(other: Pin): boolean {
|
||||
// Cannot connect to self (不能连接到自己)
|
||||
if (this._nodeId === other._nodeId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Must be opposite directions (必须是相反方向)
|
||||
if (this._direction === other._direction) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check type compatibility (检查类型兼容性)
|
||||
return this._type.canConnectTo(other._type);
|
||||
}
|
||||
|
||||
toJSON(): PinDefinition & { id: string; nodeId: string } {
|
||||
return {
|
||||
id: this._id,
|
||||
nodeId: this._nodeId,
|
||||
name: this._name,
|
||||
displayName: this._displayName,
|
||||
direction: this._direction,
|
||||
category: this._type.category,
|
||||
subType: this._type.subType,
|
||||
isArray: this._type.isArray,
|
||||
defaultValue: this._defaultValue,
|
||||
allowMultiple: this._allowMultiple,
|
||||
hidden: this._hidden,
|
||||
color: this._color
|
||||
};
|
||||
}
|
||||
}
|
||||
4
packages/node-editor/src/domain/models/index.ts
Normal file
4
packages/node-editor/src/domain/models/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export { Pin, type PinDefinition } from './Pin';
|
||||
export { GraphNode, type NodeTemplate, type NodeCategory } from './GraphNode';
|
||||
export { Connection } from './Connection';
|
||||
export { Graph } from './Graph';
|
||||
Reference in New Issue
Block a user