Compare commits
5 Commits
@esengine/
...
@esengine/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f2c3a24404 | ||
|
|
3bfb8a1c9b | ||
|
|
2ee8d87647 | ||
|
|
2d537dc10c | ||
|
|
c2acd14fce |
@@ -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
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|
||||||
|
|||||||
@@ -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 ?? '') } };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
/**
|
||||||
|
* @zh 变量节点导出
|
||||||
|
* @en Variable nodes export
|
||||||
|
*/
|
||||||
|
|
||||||
|
export * from './VariableNodes';
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user