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:
2
packages/node-editor/src/domain/index.ts
Normal file
2
packages/node-editor/src/domain/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './models';
|
||||
export * from './value-objects';
|
||||
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';
|
||||
175
packages/node-editor/src/domain/value-objects/PinType.ts
Normal file
175
packages/node-editor/src/domain/value-objects/PinType.ts
Normal file
@@ -0,0 +1,175 @@
|
||||
/**
|
||||
* Pin direction - input or output
|
||||
* 引脚方向 - 输入或输出
|
||||
*/
|
||||
export type PinDirection = 'input' | 'output';
|
||||
|
||||
/**
|
||||
* Pin data type categories for visual programming
|
||||
* 可视化编程的引脚数据类型分类
|
||||
*
|
||||
* These types cover common use cases in:
|
||||
* 这些类型涵盖以下常见用例:
|
||||
* - Blueprint visual scripting (蓝图可视化脚本)
|
||||
* - Shader graph editors (着色器图编辑器)
|
||||
* - State machine editors (状态机编辑器)
|
||||
* - Animation graph editors (动画图编辑器)
|
||||
*/
|
||||
export type PinCategory =
|
||||
| 'exec' // Execution flow pin (执行流引脚)
|
||||
| 'bool' // Boolean value (布尔值)
|
||||
| 'int' // Integer number (整数)
|
||||
| 'float' // Floating point number (浮点数)
|
||||
| 'string' // Text string (字符串)
|
||||
| 'vector2' // 2D vector (二维向量)
|
||||
| 'vector3' // 3D vector (三维向量)
|
||||
| 'vector4' // 4D vector / Color (四维向量/颜色)
|
||||
| 'color' // RGBA color (RGBA颜色)
|
||||
| 'object' // Object reference (对象引用)
|
||||
| 'array' // Array of values (数组)
|
||||
| 'map' // Key-value map (键值映射)
|
||||
| 'struct' // Custom struct (自定义结构体)
|
||||
| 'enum' // Enumeration (枚举)
|
||||
| 'delegate' // Event delegate (事件委托)
|
||||
| 'any'; // Wildcard type (通配符类型)
|
||||
|
||||
/**
|
||||
* Pin shape for rendering
|
||||
* 引脚渲染形状
|
||||
*/
|
||||
export type PinShape =
|
||||
| 'circle' // Standard data pin (标准数据引脚)
|
||||
| 'triangle' // Execution flow pin (执行流引脚)
|
||||
| 'diamond' // Array/special pin (数组/特殊引脚)
|
||||
| 'square'; // Struct pin (结构体引脚)
|
||||
|
||||
/**
|
||||
* Gets the default shape for a pin category
|
||||
* 获取引脚类型的默认形状
|
||||
*/
|
||||
export function getDefaultPinShape(category: PinCategory): PinShape {
|
||||
switch (category) {
|
||||
case 'exec':
|
||||
return 'triangle';
|
||||
case 'array':
|
||||
case 'map':
|
||||
return 'diamond';
|
||||
case 'struct':
|
||||
return 'square';
|
||||
default:
|
||||
return 'circle';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pin type value object with validation
|
||||
* 带验证的引脚类型值对象
|
||||
*/
|
||||
export class PinType {
|
||||
private readonly _category: PinCategory;
|
||||
private readonly _subType?: string;
|
||||
private readonly _isArray: boolean;
|
||||
|
||||
constructor(category: PinCategory, subType?: string, isArray = false) {
|
||||
this._category = category;
|
||||
this._subType = subType;
|
||||
this._isArray = isArray;
|
||||
}
|
||||
|
||||
get category(): PinCategory {
|
||||
return this._category;
|
||||
}
|
||||
|
||||
/**
|
||||
* Subtype for complex types like struct or enum
|
||||
* 复杂类型(如结构体或枚举)的子类型
|
||||
*/
|
||||
get subType(): string | undefined {
|
||||
return this._subType;
|
||||
}
|
||||
|
||||
get isArray(): boolean {
|
||||
return this._isArray;
|
||||
}
|
||||
|
||||
get shape(): PinShape {
|
||||
if (this._isArray) return 'diamond';
|
||||
return getDefaultPinShape(this._category);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this type can connect to another type
|
||||
* 检查此类型是否可以连接到另一个类型
|
||||
*/
|
||||
canConnectTo(other: PinType): boolean {
|
||||
// Any type can connect to anything
|
||||
// any 类型可以连接任何类型
|
||||
if (this._category === 'any' || other._category === 'any') {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Exec pins can only connect to exec pins
|
||||
// exec 引脚只能连接 exec 引脚
|
||||
if (this._category === 'exec' || other._category === 'exec') {
|
||||
return this._category === other._category;
|
||||
}
|
||||
|
||||
// Same category can connect
|
||||
// 相同类型可以连接
|
||||
if (this._category === other._category) {
|
||||
// For struct/enum, subtype must match
|
||||
// 对于结构体/枚举,子类型必须匹配
|
||||
if (this._category === 'struct' || this._category === 'enum') {
|
||||
return this._subType === other._subType;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Numeric type coercion (数值类型转换)
|
||||
const numericTypes: PinCategory[] = ['int', 'float'];
|
||||
if (numericTypes.includes(this._category) && numericTypes.includes(other._category)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Vector type coercion (向量类型转换)
|
||||
const vectorTypes: PinCategory[] = ['vector2', 'vector3', 'vector4', 'color'];
|
||||
if (vectorTypes.includes(this._category) && vectorTypes.includes(other._category)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
equals(other: PinType): boolean {
|
||||
return (
|
||||
this._category === other._category &&
|
||||
this._subType === other._subType &&
|
||||
this._isArray === other._isArray
|
||||
);
|
||||
}
|
||||
|
||||
toJSON(): { category: PinCategory; subType?: string; isArray: boolean } {
|
||||
return {
|
||||
category: this._category,
|
||||
subType: this._subType,
|
||||
isArray: this._isArray
|
||||
};
|
||||
}
|
||||
|
||||
static fromJSON(json: { category: PinCategory; subType?: string; isArray?: boolean }): PinType {
|
||||
return new PinType(json.category, json.subType, json.isArray ?? false);
|
||||
}
|
||||
|
||||
// Common type constants (常用类型常量)
|
||||
static readonly EXEC = new PinType('exec');
|
||||
static readonly BOOL = new PinType('bool');
|
||||
static readonly INT = new PinType('int');
|
||||
static readonly FLOAT = new PinType('float');
|
||||
static readonly STRING = new PinType('string');
|
||||
static readonly VECTOR2 = new PinType('vector2');
|
||||
static readonly VECTOR3 = new PinType('vector3');
|
||||
static readonly VECTOR4 = new PinType('vector4');
|
||||
static readonly COLOR = new PinType('color');
|
||||
static readonly OBJECT = new PinType('object');
|
||||
static readonly ANY = new PinType('any');
|
||||
}
|
||||
93
packages/node-editor/src/domain/value-objects/Position.ts
Normal file
93
packages/node-editor/src/domain/value-objects/Position.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* Position - Immutable 2D position value object
|
||||
* 位置 - 不可变的二维位置值对象
|
||||
*/
|
||||
export class Position {
|
||||
private readonly _x: number;
|
||||
private readonly _y: number;
|
||||
|
||||
constructor(x: number, y: number) {
|
||||
this._x = x;
|
||||
this._y = y;
|
||||
}
|
||||
|
||||
get x(): number {
|
||||
return this._x;
|
||||
}
|
||||
|
||||
get y(): number {
|
||||
return this._y;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new Position by adding offset
|
||||
* 通过添加偏移量创建新的位置
|
||||
*/
|
||||
add(offset: Position): Position {
|
||||
return new Position(this._x + offset._x, this._y + offset._y);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new Position by subtracting another position
|
||||
* 通过减去另一个位置创建新的位置
|
||||
*/
|
||||
subtract(other: Position): Position {
|
||||
return new Position(this._x - other._x, this._y - other._y);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new Position by scaling
|
||||
* 通过缩放创建新的位置
|
||||
*/
|
||||
scale(factor: number): Position {
|
||||
return new Position(this._x * factor, this._y * factor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates distance to another position
|
||||
* 计算到另一个位置的距离
|
||||
*/
|
||||
distanceTo(other: Position): number {
|
||||
const dx = this._x - other._x;
|
||||
const dy = this._y - other._y;
|
||||
return Math.sqrt(dx * dx + dy * dy);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks equality with another position
|
||||
* 检查与另一个位置是否相等
|
||||
*/
|
||||
equals(other: Position): boolean {
|
||||
return this._x === other._x && this._y === other._y;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a copy of this position
|
||||
* 创建此位置的副本
|
||||
*/
|
||||
clone(): Position {
|
||||
return new Position(this._x, this._y);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts to plain object for serialization
|
||||
* 转换为普通对象用于序列化
|
||||
*/
|
||||
toJSON(): { x: number; y: number } {
|
||||
return { x: this._x, y: this._y };
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates Position from plain object
|
||||
* 从普通对象创建位置
|
||||
*/
|
||||
static fromJSON(json: { x: number; y: number }): Position {
|
||||
return new Position(json.x, json.y);
|
||||
}
|
||||
|
||||
/**
|
||||
* Zero position constant
|
||||
* 零位置常量
|
||||
*/
|
||||
static readonly ZERO = new Position(0, 0);
|
||||
}
|
||||
8
packages/node-editor/src/domain/value-objects/index.ts
Normal file
8
packages/node-editor/src/domain/value-objects/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export { Position } from './Position';
|
||||
export {
|
||||
PinType,
|
||||
type PinDirection,
|
||||
type PinCategory,
|
||||
type PinShape,
|
||||
getDefaultPinShape
|
||||
} from './PinType';
|
||||
Reference in New Issue
Block a user