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:
YHH
2025-11-29 23:00:48 +08:00
committed by GitHub
parent f03b73b58e
commit 359886c72f
198 changed files with 33879 additions and 13121 deletions

View File

@@ -0,0 +1,2 @@
export * from './models';
export * from './value-objects';

View 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}`;
}
}

View 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, [], [], {});
}
}

View 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
};
}
}

View 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
};
}
}

View 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';

View 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');
}

View 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);
}

View File

@@ -0,0 +1,8 @@
export { Position } from './Position';
export {
PinType,
type PinDirection,
type PinCategory,
type PinShape,
getDefaultPinShape
} from './PinType';