Compare commits

..

5 Commits

Author SHA1 Message Date
github-actions[bot]
f2c3a24404 chore: release packages (#437)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-01-04 17:27:10 +08:00
yhh
3bfb8a1c9b chore: add changeset for node-editor box selection feature 2026-01-04 17:24:20 +08:00
yhh
2ee8d87647 feat(node-editor): add box selection and variable node error states
- Add box selection (drag on empty canvas to select multiple nodes)
- Support Ctrl+drag for additive selection
- Add error state styling for invalid variable references (red border, warning icon)
- Support dynamic node title via data.displayTitle
- Support hiding inputs via data.hiddenInputs array
2026-01-04 17:22:20 +08:00
github-actions[bot]
2d537dc10c chore: release packages (#436)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-01-04 16:25:34 +08:00
YHH
c2acd14fce feat(blueprint): Add Component nodes + ECS refactor + node-editor fixes (#435)
* feat(blueprint): refactor BlueprintComponent as proper ECS Component

- Convert BlueprintComponent from interface to actual ECS Component class
- Add ready-to-use BlueprintSystem that extends EntitySystem
- Remove deprecated legacy APIs (createBlueprintSystem, etc.)
- Update all blueprint documentation (Chinese & English)
- Simplify user API: just add BlueprintSystem and BlueprintComponent

BREAKING CHANGE: BlueprintComponent is now a class extending Component,
not an interface. Use `new BlueprintComponent()` instead of
`createBlueprintComponentData()`.

* chore(blueprint): add changeset for ECS component refactor

* fix(node-editor): fix connections not rendering when node is collapsed

- getPinPosition now returns node header position when pin element is not found
- Added collapsedNodesKey to force re-render connections after collapse/expand
- Input pins connect to left side, output pins to right side of collapsed nodes

* chore(node-editor): add changeset for collapse connection fix

* feat(blueprint): add Add Component nodes for entity-component creation

- Add type-specific Add_ComponentName nodes via ComponentNodeGenerator
- Add generic ECS_AddComponent node for dynamic component creation
- Add ExecutionContext.getComponentClass() for component lookup
- Add registerComponentClass() helper for manual component registration
- Each Add node supports initial property values from @BlueprintProperty

* docs: update changeset with Add Component feature

* feat(blueprint): improve event nodes with Self output and auto-create BeginPlay

- Event Begin Play now outputs Self entity
- Event Tick now outputs Self entity + Delta Seconds
- Event End Play now outputs Self entity
- createEmptyBlueprint() now includes Event Begin Play by default
- Added menuPath to all event nodes for better organization

* docs: update changeset for auto component registration

* feat(blueprint): add variable nodes (Get/Set Variable)

* docs: update changeset with variable nodes
2026-01-04 16:22:59 +08:00
25 changed files with 488 additions and 26 deletions

View File

@@ -1,5 +1,24 @@
# @esengine/node-editor # @esengine/node-editor
## 1.3.0
### Minor Changes
- [`3bfb8a1`](https://github.com/esengine/esengine/commit/3bfb8a1c9baba18373717910d29f266a71c1f63e) Thanks [@esengine](https://github.com/esengine)! - feat(node-editor): add box selection and variable node error states
- Add box selection: drag on empty canvas to select multiple nodes
- Support Ctrl+drag for additive selection (add to existing selection)
- Add error state styling for invalid variable references (red border, warning icon)
- Support dynamic node title via `data.displayTitle`
- Support hiding inputs via `data.hiddenInputs` array
## 1.2.2
### Patch Changes
- [#435](https://github.com/esengine/esengine/pull/435) [`c2acd14`](https://github.com/esengine/esengine/commit/c2acd14fce83af6cd116b3f2e40607229ccc3d6e) Thanks [@esengine](https://github.com/esengine)! - fix(node-editor): 修复节点收缩后连线不显示的问题
- 节点收缩时,连线会连接到节点头部(输入引脚在左侧,输出引脚在右侧)
- 展开后连线会自动恢复到正确位置
## 1.2.1 ## 1.2.1
### Patch Changes ### Patch Changes

View File

@@ -1,6 +1,6 @@
{ {
"name": "@esengine/node-editor", "name": "@esengine/node-editor",
"version": "1.2.1", "version": "1.3.0",
"description": "Universal node-based visual editor for blueprint, shader graph, and state machine", "description": "Universal node-based visual editor for blueprint, shader graph, and state machine",
"main": "dist/index.js", "main": "dist/index.js",
"module": "dist/index.js", "module": "dist/index.js",

View File

@@ -50,6 +50,15 @@ export interface GraphCanvasProps {
/** Canvas context menu callback (画布右键菜单回调) */ /** Canvas context menu callback (画布右键菜单回调) */
onContextMenu?: (position: Position, e: React.MouseEvent) => void; onContextMenu?: (position: Position, e: React.MouseEvent) => void;
/** Canvas mouse down callback for box selection (画布鼠标按下回调,用于框选) */
onMouseDown?: (position: Position, e: React.MouseEvent) => void;
/** Canvas mouse move callback (画布鼠标移动回调) */
onCanvasMouseMove?: (position: Position, e: React.MouseEvent) => void;
/** Canvas mouse up callback (画布鼠标释放回调) */
onCanvasMouseUp?: (position: Position, e: React.MouseEvent) => void;
/** Children to render (要渲染的子元素) */ /** Children to render (要渲染的子元素) */
children?: React.ReactNode; children?: React.ReactNode;
} }
@@ -75,6 +84,9 @@ export const GraphCanvas: React.FC<GraphCanvasProps> = ({
onZoomChange, onZoomChange,
onClick, onClick,
onContextMenu, onContextMenu,
onMouseDown: onMouseDownProp,
onCanvasMouseMove,
onCanvasMouseUp,
children children
}) => { }) => {
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
@@ -132,22 +144,30 @@ export const GraphCanvas: React.FC<GraphCanvasProps> = ({
}, [zoom, pan, minZoom, maxZoom, updateZoom, updatePan]); }, [zoom, pan, minZoom, maxZoom, updateZoom, updatePan]);
/** /**
* Handles mouse down for panning * Handles mouse down for panning or box selection
* 处理鼠标按下开始平移 * 处理鼠标按下开始平移或框选
*/ */
const handleMouseDown = useCallback((e: React.MouseEvent) => { const handleMouseDown = useCallback((e: React.MouseEvent) => {
// Middle mouse button or space + left click for panning // Middle mouse button or Alt + left click for panning
// 中键或空格+左键平移 // 中键或 Alt+左键平移
if (e.button === 1 || (e.button === 0 && e.altKey)) { if (e.button === 1 || (e.button === 0 && e.altKey)) {
e.preventDefault(); e.preventDefault();
setIsPanning(true); setIsPanning(true);
lastMousePos.current = new Position(e.clientX, e.clientY); lastMousePos.current = new Position(e.clientX, e.clientY);
} else if (e.button === 0) {
// Left click on canvas background - start box selection
// 左键点击画布背景 - 开始框选
const target = e.target as HTMLElement;
if (target === containerRef.current || target.classList.contains('ne-canvas-content')) {
const canvasPos = screenToCanvas(e.clientX, e.clientY);
onMouseDownProp?.(canvasPos, e);
}
} }
}, []); }, [screenToCanvas, onMouseDownProp]);
/** /**
* Handles mouse move for panning * Handles mouse move for panning or box selection
* 处理鼠标移动进行平移 * 处理鼠标移动进行平移或框选
*/ */
const handleMouseMove = useCallback((e: React.MouseEvent) => { const handleMouseMove = useCallback((e: React.MouseEvent) => {
if (isPanning) { if (isPanning) {
@@ -157,13 +177,27 @@ export const GraphCanvas: React.FC<GraphCanvasProps> = ({
const newPan = new Position(pan.x + dx, pan.y + dy); const newPan = new Position(pan.x + dx, pan.y + dy);
updatePan(newPan); updatePan(newPan);
} }
}, [isPanning, pan, updatePan]); // Always call canvas mouse move for box selection
// 始终调用画布鼠标移动回调用于框选
const canvasPos = screenToCanvas(e.clientX, e.clientY);
onCanvasMouseMove?.(canvasPos, e);
}, [isPanning, pan, updatePan, screenToCanvas, onCanvasMouseMove]);
/** /**
* Handles mouse up to stop panning * Handles mouse up to stop panning or box selection
* 处理鼠标释放停止平移 * 处理鼠标释放停止平移或框选
*/ */
const handleMouseUp = useCallback(() => { const handleMouseUp = useCallback((e: React.MouseEvent) => {
setIsPanning(false);
const canvasPos = screenToCanvas(e.clientX, e.clientY);
onCanvasMouseUp?.(canvasPos, e);
}, [screenToCanvas, onCanvasMouseUp]);
/**
* Handles mouse leave to stop panning
* 处理鼠标离开停止平移
*/
const handleMouseLeave = useCallback(() => {
setIsPanning(false); setIsPanning(false);
}, []); }, []);
@@ -276,7 +310,7 @@ export const GraphCanvas: React.FC<GraphCanvasProps> = ({
onMouseDown={handleMouseDown} onMouseDown={handleMouseDown}
onMouseMove={handleMouseMove} onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp} onMouseUp={handleMouseUp}
onMouseLeave={handleMouseUp} onMouseLeave={handleMouseLeave}
onClick={handleClick} onClick={handleClick}
onContextMenu={handleContextMenu} onContextMenu={handleContextMenu}
> >

View File

@@ -80,6 +80,16 @@ interface ConnectionDragState {
isValid?: boolean; isValid?: boolean;
} }
/**
* Box selection state
* 框选状态
*/
interface BoxSelectState {
startPos: Position;
currentPos: Position;
additive: boolean;
}
/** /**
* NodeEditor - Complete node graph editor component * NodeEditor - Complete node graph editor component
* NodeEditor - 完整的节点图编辑器组件 * NodeEditor - 完整的节点图编辑器组件
@@ -126,6 +136,11 @@ export const NodeEditor: React.FC<NodeEditorProps> = ({
const [dragState, setDragState] = useState<DragState | null>(null); const [dragState, setDragState] = useState<DragState | null>(null);
const [connectionDrag, setConnectionDrag] = useState<ConnectionDragState | null>(null); const [connectionDrag, setConnectionDrag] = useState<ConnectionDragState | null>(null);
const [hoveredPin, setHoveredPin] = useState<Pin | null>(null); const [hoveredPin, setHoveredPin] = useState<Pin | null>(null);
const [boxSelectState, setBoxSelectState] = useState<BoxSelectState | null>(null);
// Track if box selection just ended to prevent click from clearing selection
// 跟踪框选是否刚刚结束,以防止 click 清除选择
const boxSelectJustEndedRef = useRef(false);
// Force re-render after mount to ensure connections are drawn correctly // Force re-render after mount to ensure connections are drawn correctly
// 挂载后强制重渲染以确保连接线正确绘制 // 挂载后强制重渲染以确保连接线正确绘制
@@ -477,6 +492,12 @@ export const NodeEditor: React.FC<NodeEditorProps> = ({
* 处理画布点击取消选择 * 处理画布点击取消选择
*/ */
const handleCanvasClick = useCallback((_position: Position, _e: React.MouseEvent) => { const handleCanvasClick = useCallback((_position: Position, _e: React.MouseEvent) => {
// Skip if box selection just ended (click fires after mouseup)
// 如果框选刚刚结束则跳过click 在 mouseup 之后触发)
if (boxSelectJustEndedRef.current) {
boxSelectJustEndedRef.current = false;
return;
}
if (!readOnly) { if (!readOnly) {
onSelectionChange?.(new Set(), new Set()); onSelectionChange?.(new Set(), new Set());
} }
@@ -490,6 +511,79 @@ export const NodeEditor: React.FC<NodeEditorProps> = ({
onCanvasContextMenu?.(position, e); onCanvasContextMenu?.(position, e);
}, [onCanvasContextMenu]); }, [onCanvasContextMenu]);
/**
* Handles box selection start
* 处理框选开始
*/
const handleBoxSelectStart = useCallback((position: Position, e: React.MouseEvent) => {
if (readOnly) return;
setBoxSelectState({
startPos: position,
currentPos: position,
additive: e.ctrlKey || e.metaKey
});
}, [readOnly]);
/**
* Handles box selection move
* 处理框选移动
*/
const handleBoxSelectMove = useCallback((position: Position) => {
if (boxSelectState) {
setBoxSelectState(prev => prev ? { ...prev, currentPos: position } : null);
}
}, [boxSelectState]);
/**
* Handles box selection end
* 处理框选结束
*/
const handleBoxSelectEnd = useCallback(() => {
if (!boxSelectState) return;
const { startPos, currentPos, additive } = boxSelectState;
// Calculate selection box bounds
const minX = Math.min(startPos.x, currentPos.x);
const maxX = Math.max(startPos.x, currentPos.x);
const minY = Math.min(startPos.y, currentPos.y);
const maxY = Math.max(startPos.y, currentPos.y);
// Find nodes within the selection box
const nodesInBox: string[] = [];
const nodeWidth = 200; // Approximate node width
const nodeHeight = 100; // Approximate node height
for (const node of graph.nodes) {
const nodeLeft = node.position.x;
const nodeTop = node.position.y;
const nodeRight = nodeLeft + nodeWidth;
const nodeBottom = nodeTop + nodeHeight;
// Check if node intersects with selection box
const intersects = !(nodeRight < minX || nodeLeft > maxX || nodeBottom < minY || nodeTop > maxY);
if (intersects) {
nodesInBox.push(node.id);
}
}
// Update selection
if (additive) {
// Add to existing selection
const newSelection = new Set(selectedNodeIds);
nodesInBox.forEach(id => newSelection.add(id));
onSelectionChange?.(newSelection, new Set());
} else {
// Replace selection
onSelectionChange?.(new Set(nodesInBox), new Set());
}
// Mark that box selection just ended to prevent click from clearing selection
// 标记框选刚刚结束,以防止 click 清除选择
boxSelectJustEndedRef.current = true;
setBoxSelectState(null);
}, [boxSelectState, graph.nodes, selectedNodeIds, onSelectionChange]);
/** /**
* Handles pin value change * Handles pin value change
* 处理引脚值变化 * 处理引脚值变化
@@ -546,6 +640,9 @@ export const NodeEditor: React.FC<NodeEditorProps> = ({
onContextMenu={handleCanvasContextMenu} onContextMenu={handleCanvasContextMenu}
onPanChange={handlePanChange} onPanChange={handlePanChange}
onZoomChange={handleZoomChange} onZoomChange={handleZoomChange}
onMouseDown={handleBoxSelectStart}
onCanvasMouseMove={handleBoxSelectMove}
onCanvasMouseUp={handleBoxSelectEnd}
> >
{/* Connection layer (连接层) */} {/* Connection layer (连接层) */}
<ConnectionLayer <ConnectionLayer
@@ -580,6 +677,19 @@ export const NodeEditor: React.FC<NodeEditorProps> = ({
onToggleCollapse={handleToggleCollapse} onToggleCollapse={handleToggleCollapse}
/> />
))} ))}
{/* Box selection overlay (框选覆盖层) */}
{boxSelectState && (
<div
className="ne-selection-box"
style={{
left: Math.min(boxSelectState.startPos.x, boxSelectState.currentPos.x),
top: Math.min(boxSelectState.startPos.y, boxSelectState.currentPos.y),
width: Math.abs(boxSelectState.currentPos.x - boxSelectState.startPos.x),
height: Math.abs(boxSelectState.currentPos.y - boxSelectState.startPos.y)
}}
/>
)}
</GraphCanvas> </GraphCanvas>
</div> </div>
); );

View File

@@ -64,6 +64,8 @@ export const GraphNodeComponent: React.FC<GraphNodeComponentProps> = ({
return draggingFromPin.canConnectTo(pin); return draggingFromPin.canConnectTo(pin);
}, [draggingFromPin]); }, [draggingFromPin]);
const hasError = Boolean(node.data.invalidVariable);
const classNames = useMemo(() => { const classNames = useMemo(() => {
const classes = ['ne-node']; const classes = ['ne-node'];
if (isSelected) classes.push('selected'); if (isSelected) classes.push('selected');
@@ -71,8 +73,9 @@ export const GraphNodeComponent: React.FC<GraphNodeComponentProps> = ({
if (node.isCollapsed) classes.push('collapsed'); if (node.isCollapsed) classes.push('collapsed');
if (node.category === 'comment') classes.push('comment'); if (node.category === 'comment') classes.push('comment');
if (executionState !== 'idle') classes.push(executionState); if (executionState !== 'idle') classes.push(executionState);
if (hasError) classes.push('has-error');
return classes.join(' '); return classes.join(' ');
}, [isSelected, isDragging, node.isCollapsed, node.category, executionState]); }, [isSelected, isDragging, node.isCollapsed, node.category, executionState, hasError]);
const handleMouseDown = useCallback((e: React.MouseEvent) => { const handleMouseDown = useCallback((e: React.MouseEvent) => {
if (e.button !== 0) return; if (e.button !== 0) return;
@@ -102,8 +105,10 @@ export const GraphNodeComponent: React.FC<GraphNodeComponentProps> = ({
: undefined; : undefined;
// Separate exec pins from data pins // Separate exec pins from data pins
// Also filter out pins listed in data.hiddenInputs
const hiddenInputs = (node.data.hiddenInputs as string[]) || [];
const inputExecPins = node.inputPins.filter(p => p.isExec && !p.hidden); const inputExecPins = node.inputPins.filter(p => p.isExec && !p.hidden);
const inputDataPins = node.inputPins.filter(p => !p.isExec && !p.hidden); const inputDataPins = node.inputPins.filter(p => !p.isExec && !p.hidden && !hiddenInputs.includes(p.name));
const outputExecPins = node.outputPins.filter(p => p.isExec && !p.hidden); const outputExecPins = node.outputPins.filter(p => p.isExec && !p.hidden);
const outputDataPins = node.outputPins.filter(p => !p.isExec && !p.hidden); const outputDataPins = node.outputPins.filter(p => !p.isExec && !p.hidden);
@@ -129,13 +134,17 @@ export const GraphNodeComponent: React.FC<GraphNodeComponentProps> = ({
className={`ne-node-header ${node.category}`} className={`ne-node-header ${node.category}`}
style={headerStyle} style={headerStyle}
> >
{/* Diamond icon for event nodes, or custom icon */} {/* Warning icon for invalid nodes, or diamond/custom icon */}
<span className="ne-node-header-icon"> <span className="ne-node-header-icon">
{node.icon && renderIcon ? renderIcon(node.icon) : null} {hasError ? (
<span className="ne-node-warning-icon" title={`Variable '${node.data.variableName}' not found`}></span>
) : (
node.icon && renderIcon ? renderIcon(node.icon) : null
)}
</span> </span>
<span className="ne-node-header-title"> <span className="ne-node-header-title">
{node.title} {(node.data.displayTitle as string) || node.title}
{node.subtitle && ( {node.subtitle && (
<span className="ne-node-header-subtitle"> <span className="ne-node-header-subtitle">
{node.subtitle} {node.subtitle}

View File

@@ -38,6 +38,24 @@
z-index: var(--ne-z-dragging); z-index: var(--ne-z-dragging);
} }
/* Error state for invalid variable references */
.ne-node.has-error {
border-color: #e74c3c;
box-shadow: 0 0 0 1px #e74c3c,
0 0 12px rgba(231, 76, 60, 0.4),
0 4px 8px rgba(0, 0, 0, 0.4);
}
.ne-node.has-error .ne-node-header {
background: linear-gradient(180deg, #c0392b 0%, #962d22 100%) !important;
}
.ne-node-warning-icon {
color: #f1c40f;
font-size: 14px;
filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.5));
}
/* ==================== Node Header 节点头部 ==================== */ /* ==================== Node Header 节点头部 ==================== */
.ne-node-header { .ne-node-header {
display: flex; display: flex;

View File

@@ -1,5 +1,24 @@
# @esengine/blueprint # @esengine/blueprint
## 4.3.0
### Minor Changes
- [#435](https://github.com/esengine/esengine/pull/435) [`c2acd14`](https://github.com/esengine/esengine/commit/c2acd14fce83af6cd116b3f2e40607229ccc3d6e) Thanks [@esengine](https://github.com/esengine)! - feat(blueprint): 添加 Add Component 节点支持 + 变量节点 + ECS 模式重构
新功能:
- 为每个 @BlueprintExpose 组件自动生成 Add_ComponentName 节点
- Add 节点支持设置初始属性值
- 添加通用 ECS_AddComponent 节点用于动态添加组件
- @BlueprintExpose 装饰的组件自动注册,无需手动调用 registerComponentClass()
- 添加变量节点GetVariable, SetVariable, GetBoolVariable, GetFloatVariable, GetIntVariable, GetStringVariable
重构:
- BlueprintComponent 使用 @ECSComponent 装饰器注册
- BlueprintSystem 继承标准 System 基类
- 简化组件 API优化 VM 生命周期管理
- ExecutionContext.getComponentClass() 自动查找 @BlueprintExpose 注册的组件
## 4.2.0 ## 4.2.0
### Minor Changes ### Minor Changes

View File

@@ -1,6 +1,6 @@
{ {
"name": "@esengine/blueprint", "name": "@esengine/blueprint",
"version": "4.2.0", "version": "4.3.0",
"description": "Visual scripting system - works with any ECS framework (ESEngine, Cocos, Laya, etc.)", "description": "Visual scripting system - works with any ECS framework (ESEngine, Cocos, Laya, etc.)",
"main": "dist/index.js", "main": "dist/index.js",
"module": "dist/index.js", "module": "dist/index.js",

View File

@@ -5,6 +5,7 @@
* @zh 节点分类: * @zh 节点分类:
* - events: 生命周期事件BeginPlay, Tick, EndPlay * - events: 生命周期事件BeginPlay, Tick, EndPlay
* - ecs: ECS 操作Entity, Component, Flow * - ecs: ECS 操作Entity, Component, Flow
* - variables: 变量读写
* - math: 数学运算 * - math: 数学运算
* - time: 时间工具 * - time: 时间工具
* - debug: 调试工具 * - debug: 调试工具
@@ -12,6 +13,7 @@
* @en Node categories: * @en Node categories:
* - events: Lifecycle events (BeginPlay, Tick, EndPlay) * - events: Lifecycle events (BeginPlay, Tick, EndPlay)
* - ecs: ECS operations (Entity, Component, Flow) * - ecs: ECS operations (Entity, Component, Flow)
* - variables: Variable get/set
* - math: Math operations * - math: Math operations
* - time: Time utilities * - time: Time utilities
* - debug: Debug utilities * - debug: Debug utilities
@@ -23,6 +25,9 @@ export * from './events';
// ECS operations | ECS 操作 // ECS operations | ECS 操作
export * from './ecs'; export * from './ecs';
// Variables | 变量
export * from './variables';
// Math operations | 数学运算 // Math operations | 数学运算
export * from './math'; export * from './math';

View File

@@ -0,0 +1,189 @@
/**
* @zh 变量节点 - 读取和设置蓝图变量
* @en Variable Nodes - Get and set blueprint variables
*/
import { BlueprintNodeTemplate, BlueprintNode } from '../../types/nodes';
import { ExecutionContext, ExecutionResult } from '../../runtime/ExecutionContext';
import { INodeExecutor, RegisterNode } from '../../runtime/NodeRegistry';
// ============================================================================
// Get Variable | 获取变量
// ============================================================================
export const GetVariableTemplate: BlueprintNodeTemplate = {
type: 'GetVariable',
title: 'Get Variable',
category: 'variable',
color: '#4a9c6d',
isPure: true,
description: 'Gets the value of a variable (获取变量的值)',
keywords: ['variable', 'get', 'read', 'value'],
menuPath: ['Variable', 'Get Variable'],
inputs: [
{ name: 'variableName', type: 'string', displayName: 'Variable Name', defaultValue: '' }
],
outputs: [
{ name: 'value', type: 'any', displayName: 'Value' }
]
};
@RegisterNode(GetVariableTemplate)
export class GetVariableExecutor implements INodeExecutor {
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
const variableName = context.evaluateInput(node.id, 'variableName', '') as string;
if (!variableName) {
return { outputs: { value: null } };
}
const value = context.getVariable(variableName);
return { outputs: { value } };
}
}
// ============================================================================
// Set Variable | 设置变量
// ============================================================================
export const SetVariableTemplate: BlueprintNodeTemplate = {
type: 'SetVariable',
title: 'Set Variable',
category: 'variable',
color: '#4a9c6d',
description: 'Sets the value of a variable (设置变量的值)',
keywords: ['variable', 'set', 'write', 'assign', 'value'],
menuPath: ['Variable', 'Set Variable'],
inputs: [
{ name: 'exec', type: 'exec', displayName: '' },
{ name: 'variableName', type: 'string', displayName: 'Variable Name', defaultValue: '' },
{ name: 'value', type: 'any', displayName: 'Value' }
],
outputs: [
{ name: 'exec', type: 'exec', displayName: '' },
{ name: 'value', type: 'any', displayName: 'Value' }
]
};
@RegisterNode(SetVariableTemplate)
export class SetVariableExecutor implements INodeExecutor {
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
const variableName = context.evaluateInput(node.id, 'variableName', '') as string;
const value = context.evaluateInput(node.id, 'value', null);
if (!variableName) {
return { outputs: { value: null }, nextExec: 'exec' };
}
context.setVariable(variableName, value);
return { outputs: { value }, nextExec: 'exec' };
}
}
// ============================================================================
// Get Variable By Name (typed variants) | 按名称获取变量(类型变体)
// ============================================================================
export const GetBoolVariableTemplate: BlueprintNodeTemplate = {
type: 'GetBoolVariable',
title: 'Get Bool',
category: 'variable',
color: '#8b1e3f',
isPure: true,
description: 'Gets a boolean variable (获取布尔变量)',
keywords: ['variable', 'get', 'bool', 'boolean'],
menuPath: ['Variable', 'Get Bool'],
inputs: [
{ name: 'variableName', type: 'string', displayName: 'Variable Name', defaultValue: '' }
],
outputs: [
{ name: 'value', type: 'bool', displayName: 'Value' }
]
};
@RegisterNode(GetBoolVariableTemplate)
export class GetBoolVariableExecutor implements INodeExecutor {
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
const variableName = context.evaluateInput(node.id, 'variableName', '') as string;
const value = context.getVariable(variableName);
return { outputs: { value: Boolean(value) } };
}
}
export const GetFloatVariableTemplate: BlueprintNodeTemplate = {
type: 'GetFloatVariable',
title: 'Get Float',
category: 'variable',
color: '#39c5bb',
isPure: true,
description: 'Gets a float variable (获取浮点变量)',
keywords: ['variable', 'get', 'float', 'number'],
menuPath: ['Variable', 'Get Float'],
inputs: [
{ name: 'variableName', type: 'string', displayName: 'Variable Name', defaultValue: '' }
],
outputs: [
{ name: 'value', type: 'float', displayName: 'Value' }
]
};
@RegisterNode(GetFloatVariableTemplate)
export class GetFloatVariableExecutor implements INodeExecutor {
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
const variableName = context.evaluateInput(node.id, 'variableName', '') as string;
const value = context.getVariable(variableName);
return { outputs: { value: Number(value) || 0 } };
}
}
export const GetIntVariableTemplate: BlueprintNodeTemplate = {
type: 'GetIntVariable',
title: 'Get Int',
category: 'variable',
color: '#1c8b8b',
isPure: true,
description: 'Gets an integer variable (获取整数变量)',
keywords: ['variable', 'get', 'int', 'integer', 'number'],
menuPath: ['Variable', 'Get Int'],
inputs: [
{ name: 'variableName', type: 'string', displayName: 'Variable Name', defaultValue: '' }
],
outputs: [
{ name: 'value', type: 'int', displayName: 'Value' }
]
};
@RegisterNode(GetIntVariableTemplate)
export class GetIntVariableExecutor implements INodeExecutor {
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
const variableName = context.evaluateInput(node.id, 'variableName', '') as string;
const value = context.getVariable(variableName);
return { outputs: { value: Math.floor(Number(value) || 0) } };
}
}
export const GetStringVariableTemplate: BlueprintNodeTemplate = {
type: 'GetStringVariable',
title: 'Get String',
category: 'variable',
color: '#e91e8c',
isPure: true,
description: 'Gets a string variable (获取字符串变量)',
keywords: ['variable', 'get', 'string', 'text'],
menuPath: ['Variable', 'Get String'],
inputs: [
{ name: 'variableName', type: 'string', displayName: 'Variable Name', defaultValue: '' }
],
outputs: [
{ name: 'value', type: 'string', displayName: 'Value' }
]
};
@RegisterNode(GetStringVariableTemplate)
export class GetStringVariableExecutor implements INodeExecutor {
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
const variableName = context.evaluateInput(node.id, 'variableName', '') as string;
const value = context.getVariable(variableName);
return { outputs: { value: String(value ?? '') } };
}
}

View File

@@ -0,0 +1,6 @@
/**
* @zh 变量节点导出
* @en Variable nodes export
*/
export * from './VariableNodes';

View File

@@ -1,5 +1,12 @@
# @esengine/fsm # @esengine/fsm
## 7.0.0
### Patch Changes
- Updated dependencies [[`c2acd14`](https://github.com/esengine/esengine/commit/c2acd14fce83af6cd116b3f2e40607229ccc3d6e)]:
- @esengine/blueprint@4.3.0
## 6.0.0 ## 6.0.0
### Patch Changes ### Patch Changes

View File

@@ -1,6 +1,6 @@
{ {
"name": "@esengine/fsm", "name": "@esengine/fsm",
"version": "6.0.0", "version": "7.0.0",
"description": "Finite State Machine for ECS Framework / ECS 框架的有限状态机", "description": "Finite State Machine for ECS Framework / ECS 框架的有限状态机",
"type": "module", "type": "module",
"main": "./dist/index.js", "main": "./dist/index.js",

View File

@@ -1,5 +1,12 @@
# @esengine/network # @esengine/network
## 8.0.0
### Patch Changes
- Updated dependencies [[`c2acd14`](https://github.com/esengine/esengine/commit/c2acd14fce83af6cd116b3f2e40607229ccc3d6e)]:
- @esengine/blueprint@4.3.0
## 7.0.0 ## 7.0.0
### Patch Changes ### Patch Changes

View File

@@ -1,6 +1,6 @@
{ {
"name": "@esengine/network", "name": "@esengine/network",
"version": "7.0.0", "version": "8.0.0",
"description": "Network synchronization for multiplayer games", "description": "Network synchronization for multiplayer games",
"esengine": { "esengine": {
"plugin": true, "plugin": true,

View File

@@ -1,5 +1,12 @@
# @esengine/pathfinding # @esengine/pathfinding
## 7.0.0
### Patch Changes
- Updated dependencies [[`c2acd14`](https://github.com/esengine/esengine/commit/c2acd14fce83af6cd116b3f2e40607229ccc3d6e)]:
- @esengine/blueprint@4.3.0
## 6.0.0 ## 6.0.0
### Patch Changes ### Patch Changes

View File

@@ -1,6 +1,6 @@
{ {
"name": "@esengine/pathfinding", "name": "@esengine/pathfinding",
"version": "6.0.0", "version": "7.0.0",
"description": "寻路系统 | Pathfinding System - A*, Grid, NavMesh", "description": "寻路系统 | Pathfinding System - A*, Grid, NavMesh",
"type": "module", "type": "module",
"main": "./dist/index.js", "main": "./dist/index.js",

View File

@@ -1,5 +1,12 @@
# @esengine/procgen # @esengine/procgen
## 7.0.0
### Patch Changes
- Updated dependencies [[`c2acd14`](https://github.com/esengine/esengine/commit/c2acd14fce83af6cd116b3f2e40607229ccc3d6e)]:
- @esengine/blueprint@4.3.0
## 6.0.0 ## 6.0.0
### Patch Changes ### Patch Changes

View File

@@ -1,6 +1,6 @@
{ {
"name": "@esengine/procgen", "name": "@esengine/procgen",
"version": "6.0.0", "version": "7.0.0",
"description": "Procedural generation tools for ECS Framework / ECS 框架的程序化生成工具", "description": "Procedural generation tools for ECS Framework / ECS 框架的程序化生成工具",
"type": "module", "type": "module",
"main": "./dist/index.js", "main": "./dist/index.js",

View File

@@ -1,5 +1,12 @@
# @esengine/spatial # @esengine/spatial
## 7.0.0
### Patch Changes
- Updated dependencies [[`c2acd14`](https://github.com/esengine/esengine/commit/c2acd14fce83af6cd116b3f2e40607229ccc3d6e)]:
- @esengine/blueprint@4.3.0
## 6.0.0 ## 6.0.0
### Patch Changes ### Patch Changes

View File

@@ -1,6 +1,6 @@
{ {
"name": "@esengine/spatial", "name": "@esengine/spatial",
"version": "6.0.0", "version": "7.0.0",
"description": "Spatial query and indexing system for ECS Framework / ECS 框架的空间查询和索引系统", "description": "Spatial query and indexing system for ECS Framework / ECS 框架的空间查询和索引系统",
"type": "module", "type": "module",
"main": "./dist/index.js", "main": "./dist/index.js",

View File

@@ -1,5 +1,12 @@
# @esengine/timer # @esengine/timer
## 7.0.0
### Patch Changes
- Updated dependencies [[`c2acd14`](https://github.com/esengine/esengine/commit/c2acd14fce83af6cd116b3f2e40607229ccc3d6e)]:
- @esengine/blueprint@4.3.0
## 6.0.0 ## 6.0.0
### Patch Changes ### Patch Changes

View File

@@ -1,6 +1,6 @@
{ {
"name": "@esengine/timer", "name": "@esengine/timer",
"version": "6.0.0", "version": "7.0.0",
"description": "Timer and cooldown system for ECS Framework / ECS 框架的定时器和冷却系统", "description": "Timer and cooldown system for ECS Framework / ECS 框架的定时器和冷却系统",
"type": "module", "type": "module",
"main": "./dist/index.js", "main": "./dist/index.js",

View File

@@ -1,5 +1,16 @@
# @esengine/demos # @esengine/demos
## 1.0.13
### Patch Changes
- Updated dependencies []:
- @esengine/fsm@7.0.0
- @esengine/pathfinding@7.0.0
- @esengine/procgen@7.0.0
- @esengine/spatial@7.0.0
- @esengine/timer@7.0.0
## 1.0.12 ## 1.0.12
### Patch Changes ### Patch Changes

View File

@@ -1,6 +1,6 @@
{ {
"name": "@esengine/demos", "name": "@esengine/demos",
"version": "1.0.12", "version": "1.0.13",
"private": true, "private": true,
"description": "Demo tests for ESEngine modules documentation", "description": "Demo tests for ESEngine modules documentation",
"type": "module", "type": "module",