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:
85
packages/blueprint/package.json
Normal file
85
packages/blueprint/package.json
Normal file
@@ -0,0 +1,85 @@
|
||||
{
|
||||
"name": "@esengine/blueprint",
|
||||
"version": "1.0.0",
|
||||
"description": "Visual scripting system for ECS Framework",
|
||||
"main": "dist/index.js",
|
||||
"module": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js"
|
||||
},
|
||||
"./editor": {
|
||||
"types": "./dist/editor/index.d.ts",
|
||||
"import": "./dist/editor/index.js"
|
||||
},
|
||||
"./plugin.json": "./plugin.json"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"plugin.json"
|
||||
],
|
||||
"scripts": {
|
||||
"clean": "rimraf dist tsconfig.tsbuildinfo",
|
||||
"build": "vite build",
|
||||
"build:watch": "vite build --watch",
|
||||
"type-check": "tsc --noEmit"
|
||||
},
|
||||
"keywords": [
|
||||
"ecs",
|
||||
"blueprint",
|
||||
"visual-scripting",
|
||||
"game-engine",
|
||||
"node-editor"
|
||||
],
|
||||
"author": "yhh",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@esengine/ecs-framework": ">=2.0.0",
|
||||
"@esengine/editor-runtime": "workspace:*",
|
||||
"@esengine/node-editor": "workspace:*",
|
||||
"lucide-react": "^0.545.0",
|
||||
"react": "^18.3.1",
|
||||
"zustand": "^4.5.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@esengine/editor-runtime": {
|
||||
"optional": true
|
||||
},
|
||||
"@esengine/node-editor": {
|
||||
"optional": true
|
||||
},
|
||||
"lucide-react": {
|
||||
"optional": true
|
||||
},
|
||||
"react": {
|
||||
"optional": true
|
||||
},
|
||||
"zustand": {
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.19.17",
|
||||
"@types/react": "^18.3.12",
|
||||
"@vitejs/plugin-react": "^4.7.0",
|
||||
"rimraf": "^5.0.0",
|
||||
"typescript": "^5.8.3",
|
||||
"vite": "^6.0.7",
|
||||
"vite-plugin-dts": "^3.7.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"tslib": "^2.8.1"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://registry.npmjs.org/"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/esengine/ecs-framework.git",
|
||||
"directory": "packages/blueprint"
|
||||
}
|
||||
}
|
||||
30
packages/blueprint/plugin.json
Normal file
30
packages/blueprint/plugin.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"id": "@esengine/blueprint",
|
||||
"name": "Blueprint System",
|
||||
"version": "1.0.0",
|
||||
"description": "Visual scripting system for creating game logic without code",
|
||||
"category": "scripting",
|
||||
"loadingPhase": "default",
|
||||
"enabledByDefault": true,
|
||||
"canContainContent": true,
|
||||
"isEnginePlugin": false,
|
||||
"modules": [
|
||||
{
|
||||
"name": "BlueprintRuntime",
|
||||
"type": "runtime",
|
||||
"entry": "./src/runtime.ts"
|
||||
},
|
||||
{
|
||||
"name": "BlueprintEditor",
|
||||
"type": "editor",
|
||||
"entry": "./src/editor/index.ts"
|
||||
}
|
||||
],
|
||||
"dependencies": [
|
||||
{
|
||||
"id": "@esengine/core",
|
||||
"version": ">=1.0.0"
|
||||
}
|
||||
],
|
||||
"icon": "Workflow"
|
||||
}
|
||||
180
packages/blueprint/src/editor/BlueprintPlugin.ts
Normal file
180
packages/blueprint/src/editor/BlueprintPlugin.ts
Normal file
@@ -0,0 +1,180 @@
|
||||
/**
|
||||
* Blueprint Editor Plugin - Integrates blueprint editor with the editor
|
||||
* 蓝图编辑器插件 - 将蓝图编辑器与编辑器集成
|
||||
*/
|
||||
|
||||
import {
|
||||
type ServiceContainer,
|
||||
type IPluginLoader,
|
||||
type IEditorModuleLoader,
|
||||
type PluginDescriptor,
|
||||
type PanelDescriptor,
|
||||
type MenuItemDescriptor,
|
||||
type FileActionHandler,
|
||||
type FileCreationTemplate,
|
||||
PanelPosition,
|
||||
FileSystem,
|
||||
createLogger,
|
||||
MessageHub,
|
||||
IMessageHub
|
||||
} from '@esengine/editor-runtime';
|
||||
import { BlueprintEditorPanel } from './components/BlueprintEditorPanel';
|
||||
import { useBlueprintEditorStore } from './stores/blueprintEditorStore';
|
||||
import { createEmptyBlueprint, validateBlueprintAsset } from '../types/blueprint';
|
||||
|
||||
const logger = createLogger('BlueprintEditorModule');
|
||||
|
||||
/**
|
||||
* Blueprint 编辑器模块
|
||||
* Blueprint editor module
|
||||
*/
|
||||
class BlueprintEditorModule implements IEditorModuleLoader {
|
||||
private services?: ServiceContainer;
|
||||
|
||||
async install(services: ServiceContainer): Promise<void> {
|
||||
this.services = services;
|
||||
logger.info('Blueprint editor module installed');
|
||||
}
|
||||
|
||||
async uninstall(): Promise<void> {
|
||||
logger.info('Blueprint editor module uninstalled');
|
||||
}
|
||||
|
||||
getPanels(): PanelDescriptor[] {
|
||||
return [
|
||||
{
|
||||
id: 'panel-blueprint-editor',
|
||||
title: 'Blueprint Editor',
|
||||
position: PanelPosition.Center,
|
||||
defaultSize: 800,
|
||||
resizable: true,
|
||||
closable: true,
|
||||
icon: 'Workflow',
|
||||
order: 20,
|
||||
isDynamic: true,
|
||||
component: BlueprintEditorPanel
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
getMenuItems(): MenuItemDescriptor[] {
|
||||
return [
|
||||
{
|
||||
id: 'blueprint-new',
|
||||
label: 'New Blueprint',
|
||||
parentId: 'file',
|
||||
shortcut: 'Ctrl+Shift+B',
|
||||
execute: () => {
|
||||
useBlueprintEditorStore.getState().createNewBlueprint('New Blueprint');
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'view-blueprint-editor',
|
||||
label: 'Blueprint Editor',
|
||||
parentId: 'view',
|
||||
shortcut: 'Ctrl+B'
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
getFileActionHandlers(): FileActionHandler[] {
|
||||
const services = this.services;
|
||||
return [
|
||||
{
|
||||
extensions: ['bp'],
|
||||
onDoubleClick: async (filePath: string) => {
|
||||
try {
|
||||
// 使用 FileSystem API 读取文件
|
||||
const content = await FileSystem.readTextFile(filePath);
|
||||
const data = JSON.parse(content);
|
||||
|
||||
if (validateBlueprintAsset(data)) {
|
||||
useBlueprintEditorStore.getState().loadBlueprint(data, filePath);
|
||||
logger.info('Loaded blueprint:', filePath);
|
||||
|
||||
// 打开蓝图编辑器面板
|
||||
if (services) {
|
||||
const messageHub = services.resolve<MessageHub>(IMessageHub);
|
||||
if (messageHub) {
|
||||
const fileName = filePath.split(/[\\/]/).pop() || 'Blueprint';
|
||||
messageHub.publish('dynamic-panel:open', {
|
||||
panelId: 'panel-blueprint-editor',
|
||||
title: `Blueprint - ${fileName}`
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logger.error('Invalid blueprint file:', filePath);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Failed to load blueprint:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
getFileCreationTemplates(): FileCreationTemplate[] {
|
||||
return [
|
||||
{
|
||||
id: 'create-blueprint',
|
||||
label: 'Blueprint',
|
||||
extension: 'bp',
|
||||
icon: 'Workflow',
|
||||
category: 'scripting',
|
||||
getContent: (fileName: string) => {
|
||||
const name = fileName.replace('.bp', '');
|
||||
const blueprint = createEmptyBlueprint(name);
|
||||
return JSON.stringify(blueprint, null, 2);
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
async onEditorReady(): Promise<void> {
|
||||
logger.info('Editor ready');
|
||||
}
|
||||
|
||||
async onProjectOpen(_projectPath: string): Promise<void> {
|
||||
logger.info('Project opened');
|
||||
}
|
||||
|
||||
async onProjectClose(): Promise<void> {
|
||||
useBlueprintEditorStore.getState().createNewBlueprint('New Blueprint');
|
||||
logger.info('Project closed');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Plugin descriptor
|
||||
* 插件描述符
|
||||
*/
|
||||
const descriptor: PluginDescriptor = {
|
||||
id: '@esengine/blueprint',
|
||||
name: 'Blueprint Visual Scripting',
|
||||
version: '1.0.0',
|
||||
description: 'Visual scripting system for creating game logic without code',
|
||||
category: 'scripting',
|
||||
icon: 'Workflow',
|
||||
enabledByDefault: true,
|
||||
canContainContent: true,
|
||||
isEnginePlugin: true,
|
||||
isCore: false,
|
||||
modules: [
|
||||
{
|
||||
name: 'BlueprintEditor',
|
||||
type: 'editor',
|
||||
loadingPhase: 'default',
|
||||
panels: ['panel-blueprint-editor']
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
/**
|
||||
* Blueprint Plugin Export
|
||||
* 蓝图插件导出
|
||||
*/
|
||||
export const BlueprintPlugin: IPluginLoader = {
|
||||
descriptor,
|
||||
editorModule: new BlueprintEditorModule()
|
||||
};
|
||||
383
packages/blueprint/src/editor/components/BlueprintCanvas.tsx
Normal file
383
packages/blueprint/src/editor/components/BlueprintCanvas.tsx
Normal file
@@ -0,0 +1,383 @@
|
||||
/**
|
||||
* Blueprint Canvas - Main canvas for editing blueprints using NodeEditor
|
||||
* 蓝图画布 - 使用 NodeEditor 编辑蓝图的主画布
|
||||
*/
|
||||
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import {
|
||||
NodeEditor,
|
||||
Graph,
|
||||
GraphNode,
|
||||
Position,
|
||||
Connection,
|
||||
NodeContextMenu,
|
||||
ConfirmDialog,
|
||||
type NodeTemplate,
|
||||
type NodeCategory,
|
||||
type PinCategory
|
||||
} from '@esengine/node-editor';
|
||||
import { useBlueprintEditorStore } from '../stores/blueprintEditorStore';
|
||||
import { NodeRegistry } from '../../runtime/NodeRegistry';
|
||||
import type { BlueprintNode, BlueprintConnection, BlueprintNodeTemplate } from '../../types/nodes';
|
||||
import type { BlueprintPinDefinition } from '../../types/pins';
|
||||
|
||||
interface ContextMenuState {
|
||||
isOpen: boolean;
|
||||
screenPosition: { x: number; y: number };
|
||||
canvasPosition: Position;
|
||||
}
|
||||
|
||||
interface DeleteDialogState {
|
||||
isOpen: boolean;
|
||||
nodeId: string;
|
||||
nodeTitle: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Map blueprint pin type to node-editor PinCategory
|
||||
*/
|
||||
function mapPinCategory(type: string): PinCategory {
|
||||
switch (type) {
|
||||
case 'exec':
|
||||
return 'exec';
|
||||
case 'boolean':
|
||||
case 'bool':
|
||||
return 'bool';
|
||||
case 'integer':
|
||||
case 'int':
|
||||
return 'int';
|
||||
case 'float':
|
||||
case 'number':
|
||||
return 'float';
|
||||
case 'string':
|
||||
return 'string';
|
||||
case 'vector2':
|
||||
return 'vector2';
|
||||
case 'vector3':
|
||||
return 'vector3';
|
||||
case 'vector4':
|
||||
return 'vector4';
|
||||
case 'color':
|
||||
return 'color';
|
||||
case 'object':
|
||||
case 'reference':
|
||||
return 'object';
|
||||
case 'array':
|
||||
return 'array';
|
||||
case 'struct':
|
||||
return 'struct';
|
||||
case 'enum':
|
||||
return 'enum';
|
||||
default:
|
||||
return 'any';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Map blueprint category to node-editor NodeCategory
|
||||
*/
|
||||
function mapNodeCategory(category?: string): NodeCategory {
|
||||
switch (category) {
|
||||
case 'event':
|
||||
return 'event';
|
||||
case 'function':
|
||||
return 'function';
|
||||
case 'pure':
|
||||
return 'pure';
|
||||
case 'flow':
|
||||
return 'flow';
|
||||
case 'variable':
|
||||
return 'variable';
|
||||
case 'literal':
|
||||
return 'literal';
|
||||
case 'comment':
|
||||
return 'comment';
|
||||
default:
|
||||
return 'function';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert blueprint node template to node-editor template
|
||||
*/
|
||||
function convertNodeTemplate(bpTemplate: BlueprintNodeTemplate): NodeTemplate {
|
||||
return {
|
||||
id: bpTemplate.type,
|
||||
title: bpTemplate.title,
|
||||
category: mapNodeCategory(bpTemplate.category),
|
||||
icon: bpTemplate.icon,
|
||||
inputPins: bpTemplate.inputs.map((p: BlueprintPinDefinition) => ({
|
||||
name: p.name,
|
||||
displayName: p.displayName || p.name,
|
||||
category: mapPinCategory(p.type),
|
||||
defaultValue: p.defaultValue
|
||||
})),
|
||||
outputPins: bpTemplate.outputs.map((p: BlueprintPinDefinition) => ({
|
||||
name: p.name,
|
||||
displayName: p.displayName || p.name,
|
||||
category: mapPinCategory(p.type)
|
||||
}))
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert blueprint node to graph node
|
||||
*/
|
||||
function convertToGraphNode(node: BlueprintNode): GraphNode | null {
|
||||
const bpTemplate = NodeRegistry.instance.getTemplate(node.type);
|
||||
if (!bpTemplate) return null;
|
||||
|
||||
const template = convertNodeTemplate(bpTemplate);
|
||||
return new GraphNode(
|
||||
node.id,
|
||||
template,
|
||||
new Position(node.position.x, node.position.y),
|
||||
node.data
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert blueprint connection to graph connection
|
||||
*/
|
||||
function convertToGraphConnection(
|
||||
conn: BlueprintConnection,
|
||||
nodes: BlueprintNode[],
|
||||
graphNodes: GraphNode[]
|
||||
): Connection | null {
|
||||
const fromNode = nodes.find(n => n.id === conn.fromNodeId);
|
||||
const toNode = nodes.find(n => n.id === conn.toNodeId);
|
||||
if (!fromNode || !toNode) return null;
|
||||
|
||||
const fromTemplate = NodeRegistry.instance.getTemplate(fromNode.type);
|
||||
if (!fromTemplate) return null;
|
||||
|
||||
const fromPin = fromTemplate.outputs.find(p => p.name === conn.fromPin);
|
||||
if (!fromPin) return null;
|
||||
|
||||
// Find graph nodes to get the actual pin IDs
|
||||
const fromGraphNode = graphNodes.find(n => n.id === conn.fromNodeId);
|
||||
const toGraphNode = graphNodes.find(n => n.id === conn.toNodeId);
|
||||
if (!fromGraphNode || !toGraphNode) return null;
|
||||
|
||||
// Find pins by name
|
||||
const fromGraphPin = fromGraphNode.outputPins.find(p => p.name === conn.fromPin);
|
||||
const toGraphPin = toGraphNode.inputPins.find(p => p.name === conn.toPin);
|
||||
if (!fromGraphPin || !toGraphPin) return null;
|
||||
|
||||
return new Connection(
|
||||
conn.id,
|
||||
conn.fromNodeId,
|
||||
fromGraphPin.id,
|
||||
conn.toNodeId,
|
||||
toGraphPin.id,
|
||||
mapPinCategory(fromPin.type)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Blueprint Canvas Component using NodeEditor
|
||||
*/
|
||||
export const BlueprintCanvas: React.FC = () => {
|
||||
const {
|
||||
blueprint,
|
||||
selectedNodeIds,
|
||||
selectNodes,
|
||||
updateNodePosition,
|
||||
addNode,
|
||||
addConnection,
|
||||
removeNode,
|
||||
removeConnection
|
||||
} = useBlueprintEditorStore();
|
||||
|
||||
const [selectedConnections, setSelectedConnections] = useState<Set<string>>(new Set());
|
||||
const [contextMenu, setContextMenu] = useState<ContextMenuState>({
|
||||
isOpen: false,
|
||||
screenPosition: { x: 0, y: 0 },
|
||||
canvasPosition: new Position(0, 0)
|
||||
});
|
||||
const [deleteDialog, setDeleteDialog] = useState<DeleteDialogState>({
|
||||
isOpen: false,
|
||||
nodeId: '',
|
||||
nodeTitle: ''
|
||||
});
|
||||
|
||||
// Convert blueprint to Graph
|
||||
const graph = useMemo(() => {
|
||||
if (!blueprint) return Graph.empty('blueprint', 'Blueprint');
|
||||
|
||||
const graphNodes: GraphNode[] = [];
|
||||
for (const node of blueprint.nodes) {
|
||||
const graphNode = convertToGraphNode(node);
|
||||
if (graphNode) {
|
||||
graphNodes.push(graphNode);
|
||||
}
|
||||
}
|
||||
|
||||
const graphConnections: Connection[] = [];
|
||||
for (const conn of blueprint.connections) {
|
||||
const graphConn = convertToGraphConnection(conn, blueprint.nodes, graphNodes);
|
||||
if (graphConn) {
|
||||
graphConnections.push(graphConn);
|
||||
}
|
||||
}
|
||||
|
||||
return new Graph('blueprint', blueprint.metadata.name, graphNodes, graphConnections);
|
||||
}, [blueprint]);
|
||||
|
||||
// Handle graph changes
|
||||
const handleGraphChange = useCallback((newGraph: Graph) => {
|
||||
if (!blueprint) return;
|
||||
|
||||
// Update node positions
|
||||
for (const graphNode of newGraph.nodes) {
|
||||
const oldNode = blueprint.nodes.find(n => n.id === graphNode.id);
|
||||
if (oldNode) {
|
||||
if (oldNode.position.x !== graphNode.position.x || oldNode.position.y !== graphNode.position.y) {
|
||||
updateNodePosition(graphNode.id, graphNode.position.x, graphNode.position.y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle new connections
|
||||
for (const graphConn of newGraph.connections) {
|
||||
const exists = blueprint.connections.some(c => c.id === graphConn.id);
|
||||
if (!exists) {
|
||||
// Extract pin names from graph connection
|
||||
const fromNode = newGraph.getNode(graphConn.fromNodeId);
|
||||
const toNode = newGraph.getNode(graphConn.toNodeId);
|
||||
if (fromNode && toNode) {
|
||||
const fromPin = fromNode.outputPins.find(p => p.id === graphConn.fromPinId);
|
||||
const toPin = toNode.inputPins.find(p => p.id === graphConn.toPinId);
|
||||
if (fromPin && toPin) {
|
||||
addConnection({
|
||||
id: graphConn.id,
|
||||
fromNodeId: graphConn.fromNodeId,
|
||||
fromPin: fromPin.name,
|
||||
toNodeId: graphConn.toNodeId,
|
||||
toPin: toPin.name
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle removed connections
|
||||
for (const oldConn of blueprint.connections) {
|
||||
const exists = newGraph.connections.some(c => c.id === oldConn.id);
|
||||
if (!exists) {
|
||||
removeConnection(oldConn.id);
|
||||
}
|
||||
}
|
||||
}, [blueprint, updateNodePosition, addConnection, removeConnection]);
|
||||
|
||||
// Handle selection changes
|
||||
const handleSelectionChange = useCallback((nodeIds: Set<string>, connectionIds: Set<string>) => {
|
||||
selectNodes(Array.from(nodeIds));
|
||||
setSelectedConnections(connectionIds);
|
||||
}, [selectNodes]);
|
||||
|
||||
// Handle canvas context menu - open node selection menu
|
||||
const handleCanvasContextMenu = useCallback((position: Position, e: React.MouseEvent) => {
|
||||
setContextMenu({
|
||||
isOpen: true,
|
||||
screenPosition: { x: e.clientX, y: e.clientY },
|
||||
canvasPosition: position
|
||||
});
|
||||
}, []);
|
||||
|
||||
// Handle template selection from context menu
|
||||
const handleSelectTemplate = useCallback((template: NodeTemplate, position: Position) => {
|
||||
addNode({
|
||||
id: '',
|
||||
type: template.id,
|
||||
position: { x: position.x, y: position.y },
|
||||
data: {}
|
||||
});
|
||||
}, [addNode]);
|
||||
|
||||
// Close context menu
|
||||
const handleCloseContextMenu = useCallback(() => {
|
||||
setContextMenu(prev => ({ ...prev, isOpen: false }));
|
||||
}, []);
|
||||
|
||||
// Handle node context menu
|
||||
const handleNodeContextMenu = useCallback((node: GraphNode, e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
setDeleteDialog({
|
||||
isOpen: true,
|
||||
nodeId: node.id,
|
||||
nodeTitle: node.title
|
||||
});
|
||||
}, []);
|
||||
|
||||
// Handle delete confirmation
|
||||
const handleConfirmDelete = useCallback(() => {
|
||||
if (deleteDialog.nodeId) {
|
||||
removeNode(deleteDialog.nodeId);
|
||||
}
|
||||
setDeleteDialog({ isOpen: false, nodeId: '', nodeTitle: '' });
|
||||
}, [deleteDialog.nodeId, removeNode]);
|
||||
|
||||
// Handle delete cancel
|
||||
const handleCancelDelete = useCallback(() => {
|
||||
setDeleteDialog({ isOpen: false, nodeId: '', nodeTitle: '' });
|
||||
}, []);
|
||||
|
||||
// Get available templates
|
||||
const templates = useMemo(() => {
|
||||
const allTemplates = NodeRegistry.instance.getAllTemplates();
|
||||
return allTemplates.map(t => convertNodeTemplate(t));
|
||||
}, []);
|
||||
|
||||
if (!blueprint) {
|
||||
return (
|
||||
<div style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: '#1a1a2e',
|
||||
color: '#666'
|
||||
}}>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<p style={{ fontSize: '16px', marginBottom: '8px' }}>No blueprint loaded</p>
|
||||
<p style={{ fontSize: '12px', opacity: 0.7 }}>Create a new blueprint or open an existing one</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<NodeEditor
|
||||
graph={graph}
|
||||
templates={templates}
|
||||
selectedNodeIds={new Set(selectedNodeIds)}
|
||||
selectedConnectionIds={selectedConnections}
|
||||
onGraphChange={handleGraphChange}
|
||||
onSelectionChange={handleSelectionChange}
|
||||
onCanvasContextMenu={handleCanvasContextMenu}
|
||||
onNodeContextMenu={handleNodeContextMenu}
|
||||
/>
|
||||
<NodeContextMenu
|
||||
isOpen={contextMenu.isOpen}
|
||||
position={contextMenu.screenPosition}
|
||||
canvasPosition={contextMenu.canvasPosition}
|
||||
templates={templates}
|
||||
onSelectTemplate={handleSelectTemplate}
|
||||
onClose={handleCloseContextMenu}
|
||||
/>
|
||||
<ConfirmDialog
|
||||
isOpen={deleteDialog.isOpen}
|
||||
title="Delete Node"
|
||||
message={`Are you sure you want to delete "${deleteDialog.nodeTitle}"?`}
|
||||
confirmText="Delete"
|
||||
cancelText="Cancel"
|
||||
type="danger"
|
||||
onConfirm={handleConfirmDelete}
|
||||
onCancel={handleCancelDelete}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* Blueprint Editor Panel - Main panel for blueprint editing
|
||||
* 蓝图编辑器面板 - 蓝图编辑的主面板
|
||||
*/
|
||||
|
||||
import React, { useEffect } from 'react';
|
||||
import { BlueprintCanvas } from './BlueprintCanvas';
|
||||
import { useBlueprintEditorStore } from '../stores/blueprintEditorStore';
|
||||
|
||||
// Import nodes to register them
|
||||
// 导入节点以注册它们
|
||||
import '../../nodes';
|
||||
|
||||
/**
|
||||
* Panel container styles
|
||||
* 面板容器样式
|
||||
*/
|
||||
const panelStyles: React.CSSProperties = {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
backgroundColor: '#1a1a2e',
|
||||
color: '#fff',
|
||||
overflow: 'hidden'
|
||||
};
|
||||
|
||||
/**
|
||||
* Blueprint Editor Panel Component
|
||||
* 蓝图编辑器面板组件
|
||||
*/
|
||||
export const BlueprintEditorPanel: React.FC = () => {
|
||||
const { blueprint, createNewBlueprint } = useBlueprintEditorStore();
|
||||
|
||||
// Create a default blueprint if none exists
|
||||
// 如果不存在则创建默认蓝图
|
||||
useEffect(() => {
|
||||
if (!blueprint) {
|
||||
createNewBlueprint('New Blueprint');
|
||||
}
|
||||
}, [blueprint, createNewBlueprint]);
|
||||
|
||||
return (
|
||||
<div style={panelStyles}>
|
||||
<BlueprintCanvas />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
7
packages/blueprint/src/editor/components/index.ts
Normal file
7
packages/blueprint/src/editor/components/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* Blueprint Editor Components
|
||||
* 蓝图编辑器组件
|
||||
*/
|
||||
|
||||
export * from './BlueprintCanvas';
|
||||
export * from './BlueprintEditorPanel';
|
||||
8
packages/blueprint/src/editor/index.ts
Normal file
8
packages/blueprint/src/editor/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Blueprint Editor Module
|
||||
* 蓝图编辑器模块
|
||||
*/
|
||||
|
||||
export * from './components';
|
||||
export * from './stores';
|
||||
export * from './BlueprintPlugin';
|
||||
256
packages/blueprint/src/editor/stores/blueprintEditorStore.ts
Normal file
256
packages/blueprint/src/editor/stores/blueprintEditorStore.ts
Normal file
@@ -0,0 +1,256 @@
|
||||
/**
|
||||
* Blueprint Editor Store - State management for blueprint editor
|
||||
* 蓝图编辑器状态管理
|
||||
*/
|
||||
|
||||
import { create } from 'zustand';
|
||||
import { BlueprintAsset, createEmptyBlueprint } from '../../types/blueprint';
|
||||
import { BlueprintNode, BlueprintConnection } from '../../types/nodes';
|
||||
|
||||
/**
|
||||
* Blueprint editor state interface
|
||||
* 蓝图编辑器状态接口
|
||||
*/
|
||||
interface BlueprintEditorState {
|
||||
/** Current blueprint being edited (当前编辑的蓝图) */
|
||||
blueprint: BlueprintAsset | null;
|
||||
|
||||
/** Selected node IDs (选中的节点ID) */
|
||||
selectedNodeIds: string[];
|
||||
|
||||
/** Currently dragging node (当前拖拽的节点) */
|
||||
draggingNodeId: string | null;
|
||||
|
||||
/** Canvas pan offset (画布平移偏移) */
|
||||
panOffset: { x: number; y: number };
|
||||
|
||||
/** Canvas zoom level (画布缩放级别) */
|
||||
zoom: number;
|
||||
|
||||
/** Whether the blueprint has unsaved changes (是否有未保存的更改) */
|
||||
isDirty: boolean;
|
||||
|
||||
/** Current file path if saved (当前文件路径) */
|
||||
filePath: string | null;
|
||||
|
||||
// Actions (操作)
|
||||
/** Create new blueprint (创建新蓝图) */
|
||||
createNewBlueprint: (name: string) => void;
|
||||
|
||||
/** Load blueprint from asset (从资产加载蓝图) */
|
||||
loadBlueprint: (asset: BlueprintAsset, filePath?: string) => void;
|
||||
|
||||
/** Add a node (添加节点) */
|
||||
addNode: (node: BlueprintNode) => void;
|
||||
|
||||
/** Remove a node (移除节点) */
|
||||
removeNode: (nodeId: string) => void;
|
||||
|
||||
/** Update node position (更新节点位置) */
|
||||
updateNodePosition: (nodeId: string, x: number, y: number) => void;
|
||||
|
||||
/** Update node data (更新节点数据) */
|
||||
updateNodeData: (nodeId: string, data: Record<string, unknown>) => void;
|
||||
|
||||
/** Add connection (添加连接) */
|
||||
addConnection: (connection: BlueprintConnection) => void;
|
||||
|
||||
/** Remove connection (移除连接) */
|
||||
removeConnection: (connectionId: string) => void;
|
||||
|
||||
/** Select nodes (选择节点) */
|
||||
selectNodes: (nodeIds: string[]) => void;
|
||||
|
||||
/** Clear selection (清除选择) */
|
||||
clearSelection: () => void;
|
||||
|
||||
/** Set pan offset (设置平移偏移) */
|
||||
setPanOffset: (x: number, y: number) => void;
|
||||
|
||||
/** Set zoom level (设置缩放级别) */
|
||||
setZoom: (zoom: number) => void;
|
||||
|
||||
/** Mark as dirty (标记为已修改) */
|
||||
markDirty: () => void;
|
||||
|
||||
/** Mark as clean (标记为未修改) */
|
||||
markClean: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate unique ID for nodes and connections
|
||||
* 为节点和连接生成唯一ID
|
||||
*/
|
||||
function generateId(): string {
|
||||
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Blueprint editor store
|
||||
* 蓝图编辑器状态存储
|
||||
*/
|
||||
export const useBlueprintEditorStore = create<BlueprintEditorState>((set, get) => ({
|
||||
blueprint: null,
|
||||
selectedNodeIds: [],
|
||||
draggingNodeId: null,
|
||||
panOffset: { x: 0, y: 0 },
|
||||
zoom: 1,
|
||||
isDirty: false,
|
||||
filePath: null,
|
||||
|
||||
createNewBlueprint: (name: string) => {
|
||||
const blueprint = createEmptyBlueprint(name);
|
||||
set({
|
||||
blueprint,
|
||||
selectedNodeIds: [],
|
||||
panOffset: { x: 0, y: 0 },
|
||||
zoom: 1,
|
||||
isDirty: false,
|
||||
filePath: null
|
||||
});
|
||||
},
|
||||
|
||||
loadBlueprint: (asset: BlueprintAsset, filePath?: string) => {
|
||||
set({
|
||||
blueprint: asset,
|
||||
selectedNodeIds: [],
|
||||
panOffset: { x: 0, y: 0 },
|
||||
zoom: 1,
|
||||
isDirty: false,
|
||||
filePath: filePath ?? null
|
||||
});
|
||||
},
|
||||
|
||||
addNode: (node: BlueprintNode) => {
|
||||
const { blueprint } = get();
|
||||
if (!blueprint) return;
|
||||
|
||||
const newNode = { ...node, id: node.id || generateId() };
|
||||
set({
|
||||
blueprint: {
|
||||
...blueprint,
|
||||
nodes: [...blueprint.nodes, newNode],
|
||||
metadata: { ...blueprint.metadata, modifiedAt: Date.now() }
|
||||
},
|
||||
isDirty: true
|
||||
});
|
||||
},
|
||||
|
||||
removeNode: (nodeId: string) => {
|
||||
const { blueprint } = get();
|
||||
if (!blueprint) return;
|
||||
|
||||
set({
|
||||
blueprint: {
|
||||
...blueprint,
|
||||
nodes: blueprint.nodes.filter(n => n.id !== nodeId),
|
||||
connections: blueprint.connections.filter(
|
||||
c => c.fromNodeId !== nodeId && c.toNodeId !== nodeId
|
||||
),
|
||||
metadata: { ...blueprint.metadata, modifiedAt: Date.now() }
|
||||
},
|
||||
selectedNodeIds: get().selectedNodeIds.filter(id => id !== nodeId),
|
||||
isDirty: true
|
||||
});
|
||||
},
|
||||
|
||||
updateNodePosition: (nodeId: string, x: number, y: number) => {
|
||||
const { blueprint } = get();
|
||||
if (!blueprint) return;
|
||||
|
||||
set({
|
||||
blueprint: {
|
||||
...blueprint,
|
||||
nodes: blueprint.nodes.map(n =>
|
||||
n.id === nodeId ? { ...n, position: { x, y } } : n
|
||||
),
|
||||
metadata: { ...blueprint.metadata, modifiedAt: Date.now() }
|
||||
},
|
||||
isDirty: true
|
||||
});
|
||||
},
|
||||
|
||||
updateNodeData: (nodeId: string, data: Record<string, unknown>) => {
|
||||
const { blueprint } = get();
|
||||
if (!blueprint) return;
|
||||
|
||||
set({
|
||||
blueprint: {
|
||||
...blueprint,
|
||||
nodes: blueprint.nodes.map(n =>
|
||||
n.id === nodeId ? { ...n, data: { ...n.data, ...data } } : n
|
||||
),
|
||||
metadata: { ...blueprint.metadata, modifiedAt: Date.now() }
|
||||
},
|
||||
isDirty: true
|
||||
});
|
||||
},
|
||||
|
||||
addConnection: (connection: BlueprintConnection) => {
|
||||
const { blueprint } = get();
|
||||
if (!blueprint) return;
|
||||
|
||||
const newConnection = { ...connection, id: connection.id || generateId() };
|
||||
|
||||
// Check for existing connection to the same input pin
|
||||
// 检查是否已存在到同一输入引脚的连接
|
||||
const existingIndex = blueprint.connections.findIndex(
|
||||
c => c.toNodeId === connection.toNodeId && c.toPin === connection.toPin
|
||||
);
|
||||
|
||||
const newConnections = [...blueprint.connections];
|
||||
if (existingIndex >= 0) {
|
||||
// Replace existing connection (替换现有连接)
|
||||
newConnections[existingIndex] = newConnection;
|
||||
} else {
|
||||
newConnections.push(newConnection);
|
||||
}
|
||||
|
||||
set({
|
||||
blueprint: {
|
||||
...blueprint,
|
||||
connections: newConnections,
|
||||
metadata: { ...blueprint.metadata, modifiedAt: Date.now() }
|
||||
},
|
||||
isDirty: true
|
||||
});
|
||||
},
|
||||
|
||||
removeConnection: (connectionId: string) => {
|
||||
const { blueprint } = get();
|
||||
if (!blueprint) return;
|
||||
|
||||
set({
|
||||
blueprint: {
|
||||
...blueprint,
|
||||
connections: blueprint.connections.filter(c => c.id !== connectionId),
|
||||
metadata: { ...blueprint.metadata, modifiedAt: Date.now() }
|
||||
},
|
||||
isDirty: true
|
||||
});
|
||||
},
|
||||
|
||||
selectNodes: (nodeIds: string[]) => {
|
||||
set({ selectedNodeIds: nodeIds });
|
||||
},
|
||||
|
||||
clearSelection: () => {
|
||||
set({ selectedNodeIds: [] });
|
||||
},
|
||||
|
||||
setPanOffset: (x: number, y: number) => {
|
||||
set({ panOffset: { x, y } });
|
||||
},
|
||||
|
||||
setZoom: (zoom: number) => {
|
||||
set({ zoom: Math.max(0.1, Math.min(2, zoom)) });
|
||||
},
|
||||
|
||||
markDirty: () => {
|
||||
set({ isDirty: true });
|
||||
},
|
||||
|
||||
markClean: () => {
|
||||
set({ isDirty: false });
|
||||
}
|
||||
}));
|
||||
6
packages/blueprint/src/editor/stores/index.ts
Normal file
6
packages/blueprint/src/editor/stores/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Blueprint Editor Stores
|
||||
* 蓝图编辑器状态存储
|
||||
*/
|
||||
|
||||
export * from './blueprintEditorStore';
|
||||
31
packages/blueprint/src/index.ts
Normal file
31
packages/blueprint/src/index.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* @esengine/blueprint - Visual scripting system for ECS Framework
|
||||
* 蓝图可视化脚本系统
|
||||
*/
|
||||
|
||||
// Types
|
||||
export * from './types';
|
||||
|
||||
// Runtime
|
||||
export * from './runtime';
|
||||
|
||||
// Nodes (import to register)
|
||||
import './nodes';
|
||||
|
||||
// Re-export commonly used items
|
||||
export { NodeRegistry, RegisterNode } from './runtime/NodeRegistry';
|
||||
export { BlueprintVM } from './runtime/BlueprintVM';
|
||||
export {
|
||||
createBlueprintComponentData,
|
||||
initializeBlueprintVM,
|
||||
startBlueprint,
|
||||
stopBlueprint,
|
||||
tickBlueprint,
|
||||
cleanupBlueprint
|
||||
} from './runtime/BlueprintComponent';
|
||||
export {
|
||||
createBlueprintSystem,
|
||||
triggerBlueprintEvent,
|
||||
triggerCustomBlueprintEvent
|
||||
} from './runtime/BlueprintSystem';
|
||||
export { createEmptyBlueprint, validateBlueprintAsset } from './types/blueprint';
|
||||
91
packages/blueprint/src/nodes/debug/Print.ts
Normal file
91
packages/blueprint/src/nodes/debug/Print.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* Print Node - Outputs a message for debugging
|
||||
* 打印节点 - 输出调试消息
|
||||
*/
|
||||
|
||||
import { BlueprintNodeTemplate, BlueprintNode } from '../../types/nodes';
|
||||
import { ExecutionContext, ExecutionResult } from '../../runtime/ExecutionContext';
|
||||
import { INodeExecutor, RegisterNode } from '../../runtime/NodeRegistry';
|
||||
|
||||
/**
|
||||
* Print node template
|
||||
* Print 节点模板
|
||||
*/
|
||||
export const PrintTemplate: BlueprintNodeTemplate = {
|
||||
type: 'Print',
|
||||
title: 'Print String',
|
||||
category: 'debug',
|
||||
color: '#785EF0',
|
||||
description: 'Prints a message to the console for debugging (打印消息到控制台用于调试)',
|
||||
keywords: ['log', 'debug', 'console', 'output', 'print'],
|
||||
inputs: [
|
||||
{
|
||||
name: 'exec',
|
||||
type: 'exec',
|
||||
displayName: ''
|
||||
},
|
||||
{
|
||||
name: 'message',
|
||||
type: 'string',
|
||||
displayName: 'Message',
|
||||
defaultValue: 'Hello Blueprint!'
|
||||
},
|
||||
{
|
||||
name: 'printToScreen',
|
||||
type: 'bool',
|
||||
displayName: 'Print to Screen',
|
||||
defaultValue: true
|
||||
},
|
||||
{
|
||||
name: 'duration',
|
||||
type: 'float',
|
||||
displayName: 'Duration',
|
||||
defaultValue: 2.0
|
||||
}
|
||||
],
|
||||
outputs: [
|
||||
{
|
||||
name: 'exec',
|
||||
type: 'exec',
|
||||
displayName: ''
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
/**
|
||||
* Print node executor
|
||||
* Print 节点执行器
|
||||
*/
|
||||
@RegisterNode(PrintTemplate)
|
||||
export class PrintExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
|
||||
const message = context.evaluateInput(node.id, 'message', 'Hello Blueprint!');
|
||||
const printToScreen = context.evaluateInput(node.id, 'printToScreen', true);
|
||||
const duration = context.evaluateInput(node.id, 'duration', 2.0);
|
||||
|
||||
// Console output
|
||||
// 控制台输出
|
||||
console.log(`[Blueprint] ${message}`);
|
||||
|
||||
// Screen output via event (handled by runtime)
|
||||
// 通过事件输出到屏幕(由运行时处理)
|
||||
if (printToScreen) {
|
||||
const event = new CustomEvent('blueprint:print', {
|
||||
detail: {
|
||||
message: String(message),
|
||||
duration: Number(duration),
|
||||
entityId: context.entity.id,
|
||||
entityName: context.entity.name
|
||||
}
|
||||
});
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
window.dispatchEvent(event);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
nextExec: 'exec'
|
||||
};
|
||||
}
|
||||
}
|
||||
6
packages/blueprint/src/nodes/debug/index.ts
Normal file
6
packages/blueprint/src/nodes/debug/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Debug Nodes - Tools for debugging blueprints
|
||||
* 调试节点 - 蓝图调试工具
|
||||
*/
|
||||
|
||||
export * from './Print';
|
||||
44
packages/blueprint/src/nodes/events/EventBeginPlay.ts
Normal file
44
packages/blueprint/src/nodes/events/EventBeginPlay.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* Event Begin Play Node - Triggered when the blueprint starts
|
||||
* 开始播放事件节点 - 蓝图启动时触发
|
||||
*/
|
||||
|
||||
import { BlueprintNodeTemplate, BlueprintNode } from '../../types/nodes';
|
||||
import { ExecutionContext, ExecutionResult } from '../../runtime/ExecutionContext';
|
||||
import { INodeExecutor, RegisterNode } from '../../runtime/NodeRegistry';
|
||||
|
||||
/**
|
||||
* EventBeginPlay node template
|
||||
* EventBeginPlay 节点模板
|
||||
*/
|
||||
export const EventBeginPlayTemplate: BlueprintNodeTemplate = {
|
||||
type: 'EventBeginPlay',
|
||||
title: 'Event Begin Play',
|
||||
category: 'event',
|
||||
color: '#CC0000',
|
||||
description: 'Triggered once when the blueprint starts executing (蓝图开始执行时触发一次)',
|
||||
keywords: ['start', 'begin', 'init', 'event'],
|
||||
inputs: [],
|
||||
outputs: [
|
||||
{
|
||||
name: 'exec',
|
||||
type: 'exec',
|
||||
displayName: ''
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
/**
|
||||
* EventBeginPlay node executor
|
||||
* EventBeginPlay 节点执行器
|
||||
*/
|
||||
@RegisterNode(EventBeginPlayTemplate)
|
||||
export class EventBeginPlayExecutor implements INodeExecutor {
|
||||
execute(_node: BlueprintNode, _context: ExecutionContext): ExecutionResult {
|
||||
// Event nodes just trigger execution flow
|
||||
// 事件节点只触发执行流
|
||||
return {
|
||||
nextExec: 'exec'
|
||||
};
|
||||
}
|
||||
}
|
||||
42
packages/blueprint/src/nodes/events/EventEndPlay.ts
Normal file
42
packages/blueprint/src/nodes/events/EventEndPlay.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* Event End Play Node - Triggered when the blueprint stops
|
||||
* 结束播放事件节点 - 蓝图停止时触发
|
||||
*/
|
||||
|
||||
import { BlueprintNodeTemplate, BlueprintNode } from '../../types/nodes';
|
||||
import { ExecutionContext, ExecutionResult } from '../../runtime/ExecutionContext';
|
||||
import { INodeExecutor, RegisterNode } from '../../runtime/NodeRegistry';
|
||||
|
||||
/**
|
||||
* EventEndPlay node template
|
||||
* EventEndPlay 节点模板
|
||||
*/
|
||||
export const EventEndPlayTemplate: BlueprintNodeTemplate = {
|
||||
type: 'EventEndPlay',
|
||||
title: 'Event End Play',
|
||||
category: 'event',
|
||||
color: '#CC0000',
|
||||
description: 'Triggered once when the blueprint stops executing (蓝图停止执行时触发一次)',
|
||||
keywords: ['stop', 'end', 'destroy', 'event'],
|
||||
inputs: [],
|
||||
outputs: [
|
||||
{
|
||||
name: 'exec',
|
||||
type: 'exec',
|
||||
displayName: ''
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
/**
|
||||
* EventEndPlay node executor
|
||||
* EventEndPlay 节点执行器
|
||||
*/
|
||||
@RegisterNode(EventEndPlayTemplate)
|
||||
export class EventEndPlayExecutor implements INodeExecutor {
|
||||
execute(_node: BlueprintNode, _context: ExecutionContext): ExecutionResult {
|
||||
return {
|
||||
nextExec: 'exec'
|
||||
};
|
||||
}
|
||||
}
|
||||
50
packages/blueprint/src/nodes/events/EventTick.ts
Normal file
50
packages/blueprint/src/nodes/events/EventTick.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* Event Tick Node - Triggered every frame
|
||||
* 每帧事件节点 - 每帧触发
|
||||
*/
|
||||
|
||||
import { BlueprintNodeTemplate, BlueprintNode } from '../../types/nodes';
|
||||
import { ExecutionContext, ExecutionResult } from '../../runtime/ExecutionContext';
|
||||
import { INodeExecutor, RegisterNode } from '../../runtime/NodeRegistry';
|
||||
|
||||
/**
|
||||
* EventTick node template
|
||||
* EventTick 节点模板
|
||||
*/
|
||||
export const EventTickTemplate: BlueprintNodeTemplate = {
|
||||
type: 'EventTick',
|
||||
title: 'Event Tick',
|
||||
category: 'event',
|
||||
color: '#CC0000',
|
||||
description: 'Triggered every frame during execution (执行期间每帧触发)',
|
||||
keywords: ['update', 'frame', 'tick', 'event'],
|
||||
inputs: [],
|
||||
outputs: [
|
||||
{
|
||||
name: 'exec',
|
||||
type: 'exec',
|
||||
displayName: ''
|
||||
},
|
||||
{
|
||||
name: 'deltaTime',
|
||||
type: 'float',
|
||||
displayName: 'Delta Seconds'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
/**
|
||||
* EventTick node executor
|
||||
* EventTick 节点执行器
|
||||
*/
|
||||
@RegisterNode(EventTickTemplate)
|
||||
export class EventTickExecutor implements INodeExecutor {
|
||||
execute(_node: BlueprintNode, context: ExecutionContext): ExecutionResult {
|
||||
return {
|
||||
nextExec: 'exec',
|
||||
outputs: {
|
||||
deltaTime: context.deltaTime
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
8
packages/blueprint/src/nodes/events/index.ts
Normal file
8
packages/blueprint/src/nodes/events/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Event Nodes - Entry points for blueprint execution
|
||||
* 事件节点 - 蓝图执行的入口点
|
||||
*/
|
||||
|
||||
export * from './EventBeginPlay';
|
||||
export * from './EventTick';
|
||||
export * from './EventEndPlay';
|
||||
11
packages/blueprint/src/nodes/index.ts
Normal file
11
packages/blueprint/src/nodes/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Blueprint Nodes - All node definitions and executors
|
||||
* 蓝图节点 - 所有节点定义和执行器
|
||||
*/
|
||||
|
||||
// Import all nodes to trigger registration
|
||||
// 导入所有节点以触发注册
|
||||
export * from './events';
|
||||
export * from './debug';
|
||||
export * from './time';
|
||||
export * from './math';
|
||||
122
packages/blueprint/src/nodes/math/MathOperations.ts
Normal file
122
packages/blueprint/src/nodes/math/MathOperations.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
/**
|
||||
* Math Operation Nodes - Basic arithmetic operations
|
||||
* 数学运算节点 - 基础算术运算
|
||||
*/
|
||||
|
||||
import { BlueprintNodeTemplate, BlueprintNode } from '../../types/nodes';
|
||||
import { ExecutionContext, ExecutionResult } from '../../runtime/ExecutionContext';
|
||||
import { INodeExecutor, RegisterNode } from '../../runtime/NodeRegistry';
|
||||
|
||||
// Add Node (加法节点)
|
||||
export const AddTemplate: BlueprintNodeTemplate = {
|
||||
type: 'Add',
|
||||
title: 'Add',
|
||||
category: 'math',
|
||||
color: '#4CAF50',
|
||||
description: 'Adds two numbers together (将两个数字相加)',
|
||||
keywords: ['add', 'plus', 'sum', '+', 'math'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'a', type: 'float', displayName: 'A', defaultValue: 0 },
|
||||
{ name: 'b', type: 'float', displayName: 'B', defaultValue: 0 }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'result', type: 'float', displayName: 'Result' }
|
||||
]
|
||||
};
|
||||
|
||||
@RegisterNode(AddTemplate)
|
||||
export class AddExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
|
||||
const a = Number(context.evaluateInput(node.id, 'a', 0));
|
||||
const b = Number(context.evaluateInput(node.id, 'b', 0));
|
||||
return { outputs: { result: a + b } };
|
||||
}
|
||||
}
|
||||
|
||||
// Subtract Node (减法节点)
|
||||
export const SubtractTemplate: BlueprintNodeTemplate = {
|
||||
type: 'Subtract',
|
||||
title: 'Subtract',
|
||||
category: 'math',
|
||||
color: '#4CAF50',
|
||||
description: 'Subtracts B from A (从 A 减去 B)',
|
||||
keywords: ['subtract', 'minus', '-', 'math'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'a', type: 'float', displayName: 'A', defaultValue: 0 },
|
||||
{ name: 'b', type: 'float', displayName: 'B', defaultValue: 0 }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'result', type: 'float', displayName: 'Result' }
|
||||
]
|
||||
};
|
||||
|
||||
@RegisterNode(SubtractTemplate)
|
||||
export class SubtractExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
|
||||
const a = Number(context.evaluateInput(node.id, 'a', 0));
|
||||
const b = Number(context.evaluateInput(node.id, 'b', 0));
|
||||
return { outputs: { result: a - b } };
|
||||
}
|
||||
}
|
||||
|
||||
// Multiply Node (乘法节点)
|
||||
export const MultiplyTemplate: BlueprintNodeTemplate = {
|
||||
type: 'Multiply',
|
||||
title: 'Multiply',
|
||||
category: 'math',
|
||||
color: '#4CAF50',
|
||||
description: 'Multiplies two numbers (将两个数字相乘)',
|
||||
keywords: ['multiply', 'times', '*', 'math'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'a', type: 'float', displayName: 'A', defaultValue: 0 },
|
||||
{ name: 'b', type: 'float', displayName: 'B', defaultValue: 1 }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'result', type: 'float', displayName: 'Result' }
|
||||
]
|
||||
};
|
||||
|
||||
@RegisterNode(MultiplyTemplate)
|
||||
export class MultiplyExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
|
||||
const a = Number(context.evaluateInput(node.id, 'a', 0));
|
||||
const b = Number(context.evaluateInput(node.id, 'b', 1));
|
||||
return { outputs: { result: a * b } };
|
||||
}
|
||||
}
|
||||
|
||||
// Divide Node (除法节点)
|
||||
export const DivideTemplate: BlueprintNodeTemplate = {
|
||||
type: 'Divide',
|
||||
title: 'Divide',
|
||||
category: 'math',
|
||||
color: '#4CAF50',
|
||||
description: 'Divides A by B (A 除以 B)',
|
||||
keywords: ['divide', '/', 'math'],
|
||||
isPure: true,
|
||||
inputs: [
|
||||
{ name: 'a', type: 'float', displayName: 'A', defaultValue: 0 },
|
||||
{ name: 'b', type: 'float', displayName: 'B', defaultValue: 1 }
|
||||
],
|
||||
outputs: [
|
||||
{ name: 'result', type: 'float', displayName: 'Result' }
|
||||
]
|
||||
};
|
||||
|
||||
@RegisterNode(DivideTemplate)
|
||||
export class DivideExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
|
||||
const a = Number(context.evaluateInput(node.id, 'a', 0));
|
||||
const b = Number(context.evaluateInput(node.id, 'b', 1));
|
||||
|
||||
// Prevent division by zero (防止除零)
|
||||
if (b === 0) {
|
||||
return { outputs: { result: 0 } };
|
||||
}
|
||||
|
||||
return { outputs: { result: a / b } };
|
||||
}
|
||||
}
|
||||
6
packages/blueprint/src/nodes/math/index.ts
Normal file
6
packages/blueprint/src/nodes/math/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* Math Nodes - Mathematical operation nodes
|
||||
* 数学节点 - 数学运算节点
|
||||
*/
|
||||
|
||||
export * from './MathOperations';
|
||||
57
packages/blueprint/src/nodes/time/Delay.ts
Normal file
57
packages/blueprint/src/nodes/time/Delay.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* Delay Node - Pauses execution for a specified duration
|
||||
* 延迟节点 - 暂停执行指定的时长
|
||||
*/
|
||||
|
||||
import { BlueprintNodeTemplate, BlueprintNode } from '../../types/nodes';
|
||||
import { ExecutionContext, ExecutionResult } from '../../runtime/ExecutionContext';
|
||||
import { INodeExecutor, RegisterNode } from '../../runtime/NodeRegistry';
|
||||
|
||||
/**
|
||||
* Delay node template
|
||||
* Delay 节点模板
|
||||
*/
|
||||
export const DelayTemplate: BlueprintNodeTemplate = {
|
||||
type: 'Delay',
|
||||
title: 'Delay',
|
||||
category: 'flow',
|
||||
color: '#FFFFFF',
|
||||
description: 'Pauses execution for a specified number of seconds (暂停执行指定的秒数)',
|
||||
keywords: ['wait', 'delay', 'pause', 'sleep', 'timer'],
|
||||
inputs: [
|
||||
{
|
||||
name: 'exec',
|
||||
type: 'exec',
|
||||
displayName: ''
|
||||
},
|
||||
{
|
||||
name: 'duration',
|
||||
type: 'float',
|
||||
displayName: 'Duration',
|
||||
defaultValue: 1.0
|
||||
}
|
||||
],
|
||||
outputs: [
|
||||
{
|
||||
name: 'exec',
|
||||
type: 'exec',
|
||||
displayName: 'Completed'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
/**
|
||||
* Delay node executor
|
||||
* Delay 节点执行器
|
||||
*/
|
||||
@RegisterNode(DelayTemplate)
|
||||
export class DelayExecutor implements INodeExecutor {
|
||||
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult {
|
||||
const duration = context.evaluateInput(node.id, 'duration', 1.0) as number;
|
||||
|
||||
return {
|
||||
nextExec: 'exec',
|
||||
delay: duration
|
||||
};
|
||||
}
|
||||
}
|
||||
45
packages/blueprint/src/nodes/time/GetDeltaTime.ts
Normal file
45
packages/blueprint/src/nodes/time/GetDeltaTime.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* Get Delta Time Node - Returns the time since last frame
|
||||
* 获取增量时间节点 - 返回上一帧以来的时间
|
||||
*/
|
||||
|
||||
import { BlueprintNodeTemplate, BlueprintNode } from '../../types/nodes';
|
||||
import { ExecutionContext, ExecutionResult } from '../../runtime/ExecutionContext';
|
||||
import { INodeExecutor, RegisterNode } from '../../runtime/NodeRegistry';
|
||||
|
||||
/**
|
||||
* GetDeltaTime node template
|
||||
* GetDeltaTime 节点模板
|
||||
*/
|
||||
export const GetDeltaTimeTemplate: BlueprintNodeTemplate = {
|
||||
type: 'GetDeltaTime',
|
||||
title: 'Get Delta Time',
|
||||
category: 'time',
|
||||
color: '#4FC3F7',
|
||||
description: 'Returns the time elapsed since the last frame in seconds (返回上一帧以来经过的时间,单位秒)',
|
||||
keywords: ['delta', 'time', 'frame', 'dt'],
|
||||
isPure: true,
|
||||
inputs: [],
|
||||
outputs: [
|
||||
{
|
||||
name: 'deltaTime',
|
||||
type: 'float',
|
||||
displayName: 'Delta Seconds'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
/**
|
||||
* GetDeltaTime node executor
|
||||
* GetDeltaTime 节点执行器
|
||||
*/
|
||||
@RegisterNode(GetDeltaTimeTemplate)
|
||||
export class GetDeltaTimeExecutor implements INodeExecutor {
|
||||
execute(_node: BlueprintNode, context: ExecutionContext): ExecutionResult {
|
||||
return {
|
||||
outputs: {
|
||||
deltaTime: context.deltaTime
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
45
packages/blueprint/src/nodes/time/GetTime.ts
Normal file
45
packages/blueprint/src/nodes/time/GetTime.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* Get Time Node - Returns the total time since blueprint started
|
||||
* 获取时间节点 - 返回蓝图启动以来的总时间
|
||||
*/
|
||||
|
||||
import { BlueprintNodeTemplate, BlueprintNode } from '../../types/nodes';
|
||||
import { ExecutionContext, ExecutionResult } from '../../runtime/ExecutionContext';
|
||||
import { INodeExecutor, RegisterNode } from '../../runtime/NodeRegistry';
|
||||
|
||||
/**
|
||||
* GetTime node template
|
||||
* GetTime 节点模板
|
||||
*/
|
||||
export const GetTimeTemplate: BlueprintNodeTemplate = {
|
||||
type: 'GetTime',
|
||||
title: 'Get Game Time',
|
||||
category: 'time',
|
||||
color: '#4FC3F7',
|
||||
description: 'Returns the total time since the blueprint started in seconds (返回蓝图启动以来的总时间,单位秒)',
|
||||
keywords: ['time', 'total', 'elapsed', 'game'],
|
||||
isPure: true,
|
||||
inputs: [],
|
||||
outputs: [
|
||||
{
|
||||
name: 'time',
|
||||
type: 'float',
|
||||
displayName: 'Seconds'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
/**
|
||||
* GetTime node executor
|
||||
* GetTime 节点执行器
|
||||
*/
|
||||
@RegisterNode(GetTimeTemplate)
|
||||
export class GetTimeExecutor implements INodeExecutor {
|
||||
execute(_node: BlueprintNode, context: ExecutionContext): ExecutionResult {
|
||||
return {
|
||||
outputs: {
|
||||
time: context.time
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
8
packages/blueprint/src/nodes/time/index.ts
Normal file
8
packages/blueprint/src/nodes/time/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Time Nodes - Time-related utility nodes
|
||||
* 时间节点 - 时间相关的工具节点
|
||||
*/
|
||||
|
||||
export * from './GetDeltaTime';
|
||||
export * from './GetTime';
|
||||
export * from './Delay';
|
||||
116
packages/blueprint/src/runtime/BlueprintComponent.ts
Normal file
116
packages/blueprint/src/runtime/BlueprintComponent.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
/**
|
||||
* Blueprint Component - Attaches a blueprint to an entity
|
||||
* 蓝图组件 - 将蓝图附加到实体
|
||||
*/
|
||||
|
||||
import { BlueprintAsset } from '../types/blueprint';
|
||||
import { BlueprintVM } from './BlueprintVM';
|
||||
import { IEntity, IScene } from './ExecutionContext';
|
||||
|
||||
/**
|
||||
* Component interface for ECS integration
|
||||
* 用于 ECS 集成的组件接口
|
||||
*/
|
||||
export interface IBlueprintComponent {
|
||||
/** Entity ID this component belongs to (此组件所属的实体ID) */
|
||||
entityId: number | null;
|
||||
|
||||
/** Blueprint asset reference (蓝图资产引用) */
|
||||
blueprintAsset: BlueprintAsset | null;
|
||||
|
||||
/** Blueprint asset path for serialization (用于序列化的蓝图资产路径) */
|
||||
blueprintPath: string;
|
||||
|
||||
/** Auto-start execution when entity is created (实体创建时自动开始执行) */
|
||||
autoStart: boolean;
|
||||
|
||||
/** Enable debug mode for VM (启用 VM 调试模式) */
|
||||
debug: boolean;
|
||||
|
||||
/** Runtime VM instance (运行时 VM 实例) */
|
||||
vm: BlueprintVM | null;
|
||||
|
||||
/** Whether the blueprint has started (蓝图是否已启动) */
|
||||
isStarted: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a blueprint component data object
|
||||
* 创建蓝图组件数据对象
|
||||
*/
|
||||
export function createBlueprintComponentData(): IBlueprintComponent {
|
||||
return {
|
||||
entityId: null,
|
||||
blueprintAsset: null,
|
||||
blueprintPath: '',
|
||||
autoStart: true,
|
||||
debug: false,
|
||||
vm: null,
|
||||
isStarted: false
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the VM for a blueprint component
|
||||
* 为蓝图组件初始化 VM
|
||||
*/
|
||||
export function initializeBlueprintVM(
|
||||
component: IBlueprintComponent,
|
||||
entity: IEntity,
|
||||
scene: IScene
|
||||
): void {
|
||||
if (!component.blueprintAsset) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create VM instance
|
||||
// 创建 VM 实例
|
||||
component.vm = new BlueprintVM(component.blueprintAsset, entity, scene);
|
||||
component.vm.debug = component.debug;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start blueprint execution
|
||||
* 开始蓝图执行
|
||||
*/
|
||||
export function startBlueprint(component: IBlueprintComponent): void {
|
||||
if (component.vm && !component.isStarted) {
|
||||
component.vm.start();
|
||||
component.isStarted = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop blueprint execution
|
||||
* 停止蓝图执行
|
||||
*/
|
||||
export function stopBlueprint(component: IBlueprintComponent): void {
|
||||
if (component.vm && component.isStarted) {
|
||||
component.vm.stop();
|
||||
component.isStarted = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update blueprint execution
|
||||
* 更新蓝图执行
|
||||
*/
|
||||
export function tickBlueprint(component: IBlueprintComponent, deltaTime: number): void {
|
||||
if (component.vm && component.isStarted) {
|
||||
component.vm.tick(deltaTime);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up blueprint resources
|
||||
* 清理蓝图资源
|
||||
*/
|
||||
export function cleanupBlueprint(component: IBlueprintComponent): void {
|
||||
if (component.vm) {
|
||||
if (component.isStarted) {
|
||||
component.vm.stop();
|
||||
}
|
||||
component.vm = null;
|
||||
component.isStarted = false;
|
||||
}
|
||||
}
|
||||
121
packages/blueprint/src/runtime/BlueprintSystem.ts
Normal file
121
packages/blueprint/src/runtime/BlueprintSystem.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
/**
|
||||
* Blueprint Execution System - Manages blueprint lifecycle and execution
|
||||
* 蓝图执行系统 - 管理蓝图生命周期和执行
|
||||
*/
|
||||
|
||||
import {
|
||||
IBlueprintComponent,
|
||||
initializeBlueprintVM,
|
||||
startBlueprint,
|
||||
tickBlueprint,
|
||||
cleanupBlueprint
|
||||
} from './BlueprintComponent';
|
||||
import { IEntity, IScene } from './ExecutionContext';
|
||||
|
||||
/**
|
||||
* Blueprint system interface for engine integration
|
||||
* 用于引擎集成的蓝图系统接口
|
||||
*/
|
||||
export interface IBlueprintSystem {
|
||||
/** Process entities with blueprint components (处理带有蓝图组件的实体) */
|
||||
process(entities: IBlueprintEntity[], deltaTime: number): void;
|
||||
|
||||
/** Called when entity is added to system (实体添加到系统时调用) */
|
||||
onEntityAdded(entity: IBlueprintEntity): void;
|
||||
|
||||
/** Called when entity is removed from system (实体从系统移除时调用) */
|
||||
onEntityRemoved(entity: IBlueprintEntity): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Entity with blueprint component
|
||||
* 带有蓝图组件的实体
|
||||
*/
|
||||
export interface IBlueprintEntity extends IEntity {
|
||||
/** Blueprint component data (蓝图组件数据) */
|
||||
blueprintComponent: IBlueprintComponent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a blueprint execution system
|
||||
* 创建蓝图执行系统
|
||||
*/
|
||||
export function createBlueprintSystem(scene: IScene): IBlueprintSystem {
|
||||
return {
|
||||
process(entities: IBlueprintEntity[], deltaTime: number): void {
|
||||
for (const entity of entities) {
|
||||
const component = entity.blueprintComponent;
|
||||
|
||||
// Skip if no blueprint asset loaded
|
||||
// 如果没有加载蓝图资产则跳过
|
||||
if (!component.blueprintAsset) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Initialize VM if needed
|
||||
// 如果需要则初始化 VM
|
||||
if (!component.vm) {
|
||||
initializeBlueprintVM(component, entity, scene);
|
||||
}
|
||||
|
||||
// Auto-start if enabled
|
||||
// 如果启用则自动启动
|
||||
if (component.autoStart && !component.isStarted) {
|
||||
startBlueprint(component);
|
||||
}
|
||||
|
||||
// Tick the blueprint
|
||||
// 更新蓝图
|
||||
tickBlueprint(component, deltaTime);
|
||||
}
|
||||
},
|
||||
|
||||
onEntityAdded(entity: IBlueprintEntity): void {
|
||||
const component = entity.blueprintComponent;
|
||||
|
||||
if (component.blueprintAsset) {
|
||||
initializeBlueprintVM(component, entity, scene);
|
||||
|
||||
if (component.autoStart) {
|
||||
startBlueprint(component);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
onEntityRemoved(entity: IBlueprintEntity): void {
|
||||
cleanupBlueprint(entity.blueprintComponent);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility to manually trigger blueprint events
|
||||
* 手动触发蓝图事件的工具
|
||||
*/
|
||||
export function triggerBlueprintEvent(
|
||||
entity: IBlueprintEntity,
|
||||
eventType: string,
|
||||
data?: Record<string, unknown>
|
||||
): void {
|
||||
const vm = entity.blueprintComponent.vm;
|
||||
|
||||
if (vm && entity.blueprintComponent.isStarted) {
|
||||
vm.triggerEvent(eventType, data);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility to trigger custom events by name
|
||||
* 按名称触发自定义事件的工具
|
||||
*/
|
||||
export function triggerCustomBlueprintEvent(
|
||||
entity: IBlueprintEntity,
|
||||
eventName: string,
|
||||
data?: Record<string, unknown>
|
||||
): void {
|
||||
const vm = entity.blueprintComponent.vm;
|
||||
|
||||
if (vm && entity.blueprintComponent.isStarted) {
|
||||
vm.triggerCustomEvent(eventName, data);
|
||||
}
|
||||
}
|
||||
335
packages/blueprint/src/runtime/BlueprintVM.ts
Normal file
335
packages/blueprint/src/runtime/BlueprintVM.ts
Normal file
@@ -0,0 +1,335 @@
|
||||
/**
|
||||
* Blueprint Virtual Machine - Executes blueprint graphs
|
||||
* 蓝图虚拟机 - 执行蓝图图
|
||||
*/
|
||||
|
||||
import { BlueprintNode } from '../types/nodes';
|
||||
import { BlueprintAsset } from '../types/blueprint';
|
||||
import { ExecutionContext, ExecutionResult, IEntity, IScene } from './ExecutionContext';
|
||||
import { NodeRegistry } from './NodeRegistry';
|
||||
|
||||
/**
|
||||
* Pending execution frame (for delayed/async execution)
|
||||
* 待处理的执行帧(用于延迟/异步执行)
|
||||
*/
|
||||
interface PendingExecution {
|
||||
nodeId: string;
|
||||
execPin: string;
|
||||
resumeTime: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Event trigger types
|
||||
* 事件触发类型
|
||||
*/
|
||||
export type EventType =
|
||||
| 'BeginPlay'
|
||||
| 'Tick'
|
||||
| 'EndPlay'
|
||||
| 'Collision'
|
||||
| 'TriggerEnter'
|
||||
| 'TriggerExit'
|
||||
| 'Custom';
|
||||
|
||||
/**
|
||||
* Blueprint Virtual Machine
|
||||
* 蓝图虚拟机
|
||||
*/
|
||||
export class BlueprintVM {
|
||||
/** Execution context (执行上下文) */
|
||||
private _context: ExecutionContext;
|
||||
|
||||
/** Pending executions (delayed nodes) (待处理的执行) */
|
||||
private _pendingExecutions: PendingExecution[] = [];
|
||||
|
||||
/** Event node cache by type (按类型缓存的事件节点) */
|
||||
private _eventNodes: Map<string, BlueprintNode[]> = new Map();
|
||||
|
||||
/** Whether the VM is running (VM 是否运行中) */
|
||||
private _isRunning: boolean = false;
|
||||
|
||||
/** Current execution time (当前执行时间) */
|
||||
private _currentTime: number = 0;
|
||||
|
||||
/** Maximum execution steps per frame (每帧最大执行步骤) */
|
||||
private _maxStepsPerFrame: number = 1000;
|
||||
|
||||
/** Debug mode (调试模式) */
|
||||
debug: boolean = false;
|
||||
|
||||
constructor(blueprint: BlueprintAsset, entity: IEntity, scene: IScene) {
|
||||
this._context = new ExecutionContext(blueprint, entity, scene);
|
||||
this._cacheEventNodes();
|
||||
}
|
||||
|
||||
get context(): ExecutionContext {
|
||||
return this._context;
|
||||
}
|
||||
|
||||
get isRunning(): boolean {
|
||||
return this._isRunning;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cache event nodes by type for quick lookup
|
||||
* 按类型缓存事件节点以便快速查找
|
||||
*/
|
||||
private _cacheEventNodes(): void {
|
||||
for (const node of this._context.blueprint.nodes) {
|
||||
// Event nodes start with "Event"
|
||||
// 事件节点以 "Event" 开头
|
||||
if (node.type.startsWith('Event')) {
|
||||
const eventType = node.type;
|
||||
if (!this._eventNodes.has(eventType)) {
|
||||
this._eventNodes.set(eventType, []);
|
||||
}
|
||||
this._eventNodes.get(eventType)!.push(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the VM
|
||||
* 启动 VM
|
||||
*/
|
||||
start(): void {
|
||||
this._isRunning = true;
|
||||
this._currentTime = 0;
|
||||
|
||||
// Trigger BeginPlay event
|
||||
// 触发 BeginPlay 事件
|
||||
this.triggerEvent('EventBeginPlay');
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the VM
|
||||
* 停止 VM
|
||||
*/
|
||||
stop(): void {
|
||||
// Trigger EndPlay event
|
||||
// 触发 EndPlay 事件
|
||||
this.triggerEvent('EventEndPlay');
|
||||
|
||||
this._isRunning = false;
|
||||
this._pendingExecutions = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Pause the VM
|
||||
* 暂停 VM
|
||||
*/
|
||||
pause(): void {
|
||||
this._isRunning = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resume the VM
|
||||
* 恢复 VM
|
||||
*/
|
||||
resume(): void {
|
||||
this._isRunning = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the VM (called every frame)
|
||||
* 更新 VM(每帧调用)
|
||||
*/
|
||||
tick(deltaTime: number): void {
|
||||
if (!this._isRunning) return;
|
||||
|
||||
this._currentTime += deltaTime;
|
||||
this._context.deltaTime = deltaTime;
|
||||
this._context.time = this._currentTime;
|
||||
|
||||
// Process pending delayed executions
|
||||
// 处理待处理的延迟执行
|
||||
this._processPendingExecutions();
|
||||
|
||||
// Trigger Tick event
|
||||
// 触发 Tick 事件
|
||||
this.triggerEvent('EventTick');
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger an event by type
|
||||
* 按类型触发事件
|
||||
*/
|
||||
triggerEvent(eventType: string, data?: Record<string, unknown>): void {
|
||||
const eventNodes = this._eventNodes.get(eventType);
|
||||
if (!eventNodes) return;
|
||||
|
||||
for (const node of eventNodes) {
|
||||
this._executeFromNode(node, 'exec', data);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger a custom event by name
|
||||
* 按名称触发自定义事件
|
||||
*/
|
||||
triggerCustomEvent(eventName: string, data?: Record<string, unknown>): void {
|
||||
const eventNodes = this._eventNodes.get('EventCustom');
|
||||
if (!eventNodes) return;
|
||||
|
||||
for (const node of eventNodes) {
|
||||
if (node.data.eventName === eventName) {
|
||||
this._executeFromNode(node, 'exec', data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute from a starting node
|
||||
* 从起始节点执行
|
||||
*/
|
||||
private _executeFromNode(
|
||||
startNode: BlueprintNode,
|
||||
startPin: string,
|
||||
eventData?: Record<string, unknown>
|
||||
): void {
|
||||
// Clear output cache for new execution
|
||||
// 为新执行清除输出缓存
|
||||
this._context.clearOutputCache();
|
||||
|
||||
// Set event data as node outputs
|
||||
// 设置事件数据为节点输出
|
||||
if (eventData) {
|
||||
this._context.setOutputs(startNode.id, eventData);
|
||||
}
|
||||
|
||||
// Follow execution chain
|
||||
// 跟随执行链
|
||||
let currentNodeId: string | null = startNode.id;
|
||||
let currentPin: string = startPin;
|
||||
let steps = 0;
|
||||
|
||||
while (currentNodeId && steps < this._maxStepsPerFrame) {
|
||||
steps++;
|
||||
|
||||
// Get connected nodes from current exec pin
|
||||
// 从当前执行引脚获取连接的节点
|
||||
const connections = this._context.getConnectionsFromPin(currentNodeId, currentPin);
|
||||
|
||||
if (connections.length === 0) {
|
||||
// No more connections, end execution
|
||||
// 没有更多连接,结束执行
|
||||
break;
|
||||
}
|
||||
|
||||
// Execute connected node
|
||||
// 执行连接的节点
|
||||
const nextConn = connections[0];
|
||||
const result = this._executeNode(nextConn.toNodeId);
|
||||
|
||||
if (result.error) {
|
||||
console.error(`Blueprint error in node ${nextConn.toNodeId}: ${result.error}`);
|
||||
break;
|
||||
}
|
||||
|
||||
if (result.delay && result.delay > 0) {
|
||||
// Schedule delayed execution
|
||||
// 安排延迟执行
|
||||
this._pendingExecutions.push({
|
||||
nodeId: nextConn.toNodeId,
|
||||
execPin: result.nextExec ?? 'exec',
|
||||
resumeTime: this._currentTime + result.delay
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
if (result.yield) {
|
||||
// Yield execution until next frame
|
||||
// 暂停执行直到下一帧
|
||||
break;
|
||||
}
|
||||
|
||||
if (result.nextExec === null) {
|
||||
// Explicitly stop execution
|
||||
// 显式停止执行
|
||||
break;
|
||||
}
|
||||
|
||||
// Continue to next node
|
||||
// 继续到下一个节点
|
||||
currentNodeId = nextConn.toNodeId;
|
||||
currentPin = result.nextExec ?? 'exec';
|
||||
}
|
||||
|
||||
if (steps >= this._maxStepsPerFrame) {
|
||||
console.warn('Blueprint execution exceeded maximum steps, possible infinite loop');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a single node
|
||||
* 执行单个节点
|
||||
*/
|
||||
private _executeNode(nodeId: string): ExecutionResult {
|
||||
const node = this._context.getNode(nodeId);
|
||||
if (!node) {
|
||||
return { error: `Node not found: ${nodeId}` };
|
||||
}
|
||||
|
||||
const executor = NodeRegistry.instance.getExecutor(node.type);
|
||||
if (!executor) {
|
||||
return { error: `No executor for node type: ${node.type}` };
|
||||
}
|
||||
|
||||
try {
|
||||
if (this.debug) {
|
||||
console.log(`[Blueprint] Executing: ${node.type} (${nodeId})`);
|
||||
}
|
||||
|
||||
const result = executor.execute(node, this._context);
|
||||
|
||||
// Cache outputs
|
||||
// 缓存输出
|
||||
if (result.outputs) {
|
||||
this._context.setOutputs(nodeId, result.outputs);
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
return { error: `Execution error: ${error}` };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process pending delayed executions
|
||||
* 处理待处理的延迟执行
|
||||
*/
|
||||
private _processPendingExecutions(): void {
|
||||
const stillPending: PendingExecution[] = [];
|
||||
|
||||
for (const pending of this._pendingExecutions) {
|
||||
if (this._currentTime >= pending.resumeTime) {
|
||||
// Resume execution
|
||||
// 恢复执行
|
||||
const node = this._context.getNode(pending.nodeId);
|
||||
if (node) {
|
||||
this._executeFromNode(node, pending.execPin);
|
||||
}
|
||||
} else {
|
||||
stillPending.push(pending);
|
||||
}
|
||||
}
|
||||
|
||||
this._pendingExecutions = stillPending;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get instance variables for serialization
|
||||
* 获取实例变量用于序列化
|
||||
*/
|
||||
getInstanceVariables(): Map<string, unknown> {
|
||||
return this._context.getInstanceVariables();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set instance variables from serialization
|
||||
* 从序列化设置实例变量
|
||||
*/
|
||||
setInstanceVariables(variables: Map<string, unknown>): void {
|
||||
this._context.setInstanceVariables(variables);
|
||||
}
|
||||
}
|
||||
294
packages/blueprint/src/runtime/ExecutionContext.ts
Normal file
294
packages/blueprint/src/runtime/ExecutionContext.ts
Normal file
@@ -0,0 +1,294 @@
|
||||
/**
|
||||
* Execution Context - Runtime context for blueprint execution
|
||||
* 执行上下文 - 蓝图执行的运行时上下文
|
||||
*/
|
||||
|
||||
import { BlueprintNode, BlueprintConnection } from '../types/nodes';
|
||||
import { BlueprintAsset } from '../types/blueprint';
|
||||
|
||||
/**
|
||||
* Result of node execution
|
||||
* 节点执行的结果
|
||||
*/
|
||||
export interface ExecutionResult {
|
||||
/**
|
||||
* Next exec pin to follow (null to stop, undefined to continue default)
|
||||
* 下一个要执行的引脚(null 停止,undefined 继续默认)
|
||||
*/
|
||||
nextExec?: string | null;
|
||||
|
||||
/**
|
||||
* Output values by pin name
|
||||
* 按引脚名称的输出值
|
||||
*/
|
||||
outputs?: Record<string, unknown>;
|
||||
|
||||
/**
|
||||
* Whether to yield execution (for async operations)
|
||||
* 是否暂停执行(用于异步操作)
|
||||
*/
|
||||
yield?: boolean;
|
||||
|
||||
/**
|
||||
* Delay before continuing (in seconds)
|
||||
* 继续前的延迟(秒)
|
||||
*/
|
||||
delay?: number;
|
||||
|
||||
/**
|
||||
* Error message if execution failed
|
||||
* 执行失败时的错误消息
|
||||
*/
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Entity interface (minimal for decoupling)
|
||||
* 实体接口(最小化以解耦)
|
||||
*/
|
||||
export interface IEntity {
|
||||
id: number;
|
||||
name: string;
|
||||
active: boolean;
|
||||
getComponent<T>(type: new (...args: unknown[]) => T): T | null;
|
||||
addComponent<T>(component: T): T;
|
||||
removeComponent<T>(type: new (...args: unknown[]) => T): void;
|
||||
hasComponent<T>(type: new (...args: unknown[]) => T): boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scene interface (minimal for decoupling)
|
||||
* 场景接口(最小化以解耦)
|
||||
*/
|
||||
export interface IScene {
|
||||
createEntity(name?: string): IEntity;
|
||||
destroyEntity(entity: IEntity): void;
|
||||
findEntityByName(name: string): IEntity | null;
|
||||
findEntitiesByTag(tag: number): IEntity[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Execution context provides access to runtime services
|
||||
* 执行上下文提供对运行时服务的访问
|
||||
*/
|
||||
export class ExecutionContext {
|
||||
/** Current blueprint asset (当前蓝图资产) */
|
||||
readonly blueprint: BlueprintAsset;
|
||||
|
||||
/** Owner entity (所有者实体) */
|
||||
readonly entity: IEntity;
|
||||
|
||||
/** Current scene (当前场景) */
|
||||
readonly scene: IScene;
|
||||
|
||||
/** Frame delta time (帧增量时间) */
|
||||
deltaTime: number = 0;
|
||||
|
||||
/** Total time since start (开始以来的总时间) */
|
||||
time: number = 0;
|
||||
|
||||
/** Instance variables (实例变量) */
|
||||
private _instanceVariables: Map<string, unknown> = new Map();
|
||||
|
||||
/** Local variables (per-execution) (局部变量,每次执行) */
|
||||
private _localVariables: Map<string, unknown> = new Map();
|
||||
|
||||
/** Global variables (shared) (全局变量,共享) */
|
||||
private static _globalVariables: Map<string, unknown> = new Map();
|
||||
|
||||
/** Node output cache for current execution (当前执行的节点输出缓存) */
|
||||
private _outputCache: Map<string, Record<string, unknown>> = new Map();
|
||||
|
||||
/** Connection lookup by target (按目标的连接查找) */
|
||||
private _connectionsByTarget: Map<string, BlueprintConnection[]> = new Map();
|
||||
|
||||
/** Connection lookup by source (按源的连接查找) */
|
||||
private _connectionsBySource: Map<string, BlueprintConnection[]> = new Map();
|
||||
|
||||
constructor(blueprint: BlueprintAsset, entity: IEntity, scene: IScene) {
|
||||
this.blueprint = blueprint;
|
||||
this.entity = entity;
|
||||
this.scene = scene;
|
||||
|
||||
// Initialize instance variables with defaults
|
||||
// 使用默认值初始化实例变量
|
||||
for (const variable of blueprint.variables) {
|
||||
if (variable.scope === 'instance') {
|
||||
this._instanceVariables.set(variable.name, variable.defaultValue);
|
||||
}
|
||||
}
|
||||
|
||||
// Build connection lookup maps
|
||||
// 构建连接查找映射
|
||||
this._buildConnectionMaps();
|
||||
}
|
||||
|
||||
private _buildConnectionMaps(): void {
|
||||
for (const conn of this.blueprint.connections) {
|
||||
// By target
|
||||
const targetKey = `${conn.toNodeId}.${conn.toPin}`;
|
||||
if (!this._connectionsByTarget.has(targetKey)) {
|
||||
this._connectionsByTarget.set(targetKey, []);
|
||||
}
|
||||
this._connectionsByTarget.get(targetKey)!.push(conn);
|
||||
|
||||
// By source
|
||||
const sourceKey = `${conn.fromNodeId}.${conn.fromPin}`;
|
||||
if (!this._connectionsBySource.has(sourceKey)) {
|
||||
this._connectionsBySource.set(sourceKey, []);
|
||||
}
|
||||
this._connectionsBySource.get(sourceKey)!.push(conn);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a node by ID
|
||||
* 通过ID获取节点
|
||||
*/
|
||||
getNode(nodeId: string): BlueprintNode | undefined {
|
||||
return this.blueprint.nodes.find(n => n.id === nodeId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get connections to a target pin
|
||||
* 获取到目标引脚的连接
|
||||
*/
|
||||
getConnectionsToPin(nodeId: string, pinName: string): BlueprintConnection[] {
|
||||
return this._connectionsByTarget.get(`${nodeId}.${pinName}`) ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get connections from a source pin
|
||||
* 获取从源引脚的连接
|
||||
*/
|
||||
getConnectionsFromPin(nodeId: string, pinName: string): BlueprintConnection[] {
|
||||
return this._connectionsBySource.get(`${nodeId}.${pinName}`) ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate an input pin value (follows connections or uses default)
|
||||
* 计算输入引脚值(跟随连接或使用默认值)
|
||||
*/
|
||||
evaluateInput(nodeId: string, pinName: string, defaultValue?: unknown): unknown {
|
||||
const connections = this.getConnectionsToPin(nodeId, pinName);
|
||||
|
||||
if (connections.length === 0) {
|
||||
// Use default from node data or provided default
|
||||
// 使用节点数据的默认值或提供的默认值
|
||||
const node = this.getNode(nodeId);
|
||||
return node?.data[pinName] ?? defaultValue;
|
||||
}
|
||||
|
||||
// Get value from connected output
|
||||
// 从连接的输出获取值
|
||||
const conn = connections[0];
|
||||
const cachedOutputs = this._outputCache.get(conn.fromNodeId);
|
||||
|
||||
if (cachedOutputs && conn.fromPin in cachedOutputs) {
|
||||
return cachedOutputs[conn.fromPin];
|
||||
}
|
||||
|
||||
// Need to execute the source node first (lazy evaluation)
|
||||
// 需要先执行源节点(延迟求值)
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set output values for a node (cached for current execution)
|
||||
* 设置节点的输出值(为当前执行缓存)
|
||||
*/
|
||||
setOutputs(nodeId: string, outputs: Record<string, unknown>): void {
|
||||
this._outputCache.set(nodeId, outputs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cached outputs for a node
|
||||
* 获取节点的缓存输出
|
||||
*/
|
||||
getOutputs(nodeId: string): Record<string, unknown> | undefined {
|
||||
return this._outputCache.get(nodeId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear output cache (call at start of new execution)
|
||||
* 清除输出缓存(在新执行开始时调用)
|
||||
*/
|
||||
clearOutputCache(): void {
|
||||
this._outputCache.clear();
|
||||
this._localVariables.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a variable value
|
||||
* 获取变量值
|
||||
*/
|
||||
getVariable(name: string): unknown {
|
||||
// Check local first, then instance, then global
|
||||
// 先检查局部,然后实例,然后全局
|
||||
if (this._localVariables.has(name)) {
|
||||
return this._localVariables.get(name);
|
||||
}
|
||||
if (this._instanceVariables.has(name)) {
|
||||
return this._instanceVariables.get(name);
|
||||
}
|
||||
if (ExecutionContext._globalVariables.has(name)) {
|
||||
return ExecutionContext._globalVariables.get(name);
|
||||
}
|
||||
|
||||
// Return default from variable definition
|
||||
// 返回变量定义的默认值
|
||||
const varDef = this.blueprint.variables.find(v => v.name === name);
|
||||
return varDef?.defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a variable value
|
||||
* 设置变量值
|
||||
*/
|
||||
setVariable(name: string, value: unknown): void {
|
||||
const varDef = this.blueprint.variables.find(v => v.name === name);
|
||||
|
||||
if (!varDef) {
|
||||
// Treat unknown variables as local
|
||||
// 将未知变量视为局部变量
|
||||
this._localVariables.set(name, value);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (varDef.scope) {
|
||||
case 'local':
|
||||
this._localVariables.set(name, value);
|
||||
break;
|
||||
case 'instance':
|
||||
this._instanceVariables.set(name, value);
|
||||
break;
|
||||
case 'global':
|
||||
ExecutionContext._globalVariables.set(name, value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all instance variables (for serialization)
|
||||
* 获取所有实例变量(用于序列化)
|
||||
*/
|
||||
getInstanceVariables(): Map<string, unknown> {
|
||||
return new Map(this._instanceVariables);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set instance variables (for deserialization)
|
||||
* 设置实例变量(用于反序列化)
|
||||
*/
|
||||
setInstanceVariables(variables: Map<string, unknown>): void {
|
||||
this._instanceVariables = new Map(variables);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear global variables (for scene reset)
|
||||
* 清除全局变量(用于场景重置)
|
||||
*/
|
||||
static clearGlobalVariables(): void {
|
||||
ExecutionContext._globalVariables.clear();
|
||||
}
|
||||
}
|
||||
151
packages/blueprint/src/runtime/NodeRegistry.ts
Normal file
151
packages/blueprint/src/runtime/NodeRegistry.ts
Normal file
@@ -0,0 +1,151 @@
|
||||
/**
|
||||
* Node Registry - Manages node templates and executors
|
||||
* 节点注册表 - 管理节点模板和执行器
|
||||
*/
|
||||
|
||||
import { BlueprintNodeTemplate, BlueprintNode } from '../types/nodes';
|
||||
import { ExecutionContext, ExecutionResult } from './ExecutionContext';
|
||||
|
||||
/**
|
||||
* Node executor interface - implements the logic for a node type
|
||||
* 节点执行器接口 - 实现节点类型的逻辑
|
||||
*/
|
||||
export interface INodeExecutor {
|
||||
/**
|
||||
* Execute the node
|
||||
* 执行节点
|
||||
*
|
||||
* @param node - Node instance (节点实例)
|
||||
* @param context - Execution context (执行上下文)
|
||||
* @returns Execution result (执行结果)
|
||||
*/
|
||||
execute(node: BlueprintNode, context: ExecutionContext): ExecutionResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* Node definition combines template with executor
|
||||
* 节点定义组合模板和执行器
|
||||
*/
|
||||
export interface NodeDefinition {
|
||||
template: BlueprintNodeTemplate;
|
||||
executor: INodeExecutor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Node Registry - singleton that holds all registered node types
|
||||
* 节点注册表 - 持有所有注册节点类型的单例
|
||||
*/
|
||||
export class NodeRegistry {
|
||||
private static _instance: NodeRegistry;
|
||||
private _nodes: Map<string, NodeDefinition> = new Map();
|
||||
|
||||
private constructor() {}
|
||||
|
||||
static get instance(): NodeRegistry {
|
||||
if (!NodeRegistry._instance) {
|
||||
NodeRegistry._instance = new NodeRegistry();
|
||||
}
|
||||
return NodeRegistry._instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a node type
|
||||
* 注册节点类型
|
||||
*/
|
||||
register(template: BlueprintNodeTemplate, executor: INodeExecutor): void {
|
||||
if (this._nodes.has(template.type)) {
|
||||
console.warn(`Node type "${template.type}" is already registered, overwriting`);
|
||||
}
|
||||
this._nodes.set(template.type, { template, executor });
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a node definition by type
|
||||
* 通过类型获取节点定义
|
||||
*/
|
||||
get(type: string): NodeDefinition | undefined {
|
||||
return this._nodes.get(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get node template by type
|
||||
* 通过类型获取节点模板
|
||||
*/
|
||||
getTemplate(type: string): BlueprintNodeTemplate | undefined {
|
||||
return this._nodes.get(type)?.template;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get node executor by type
|
||||
* 通过类型获取节点执行器
|
||||
*/
|
||||
getExecutor(type: string): INodeExecutor | undefined {
|
||||
return this._nodes.get(type)?.executor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a node type is registered
|
||||
* 检查节点类型是否已注册
|
||||
*/
|
||||
has(type: string): boolean {
|
||||
return this._nodes.has(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all registered templates
|
||||
* 获取所有注册的模板
|
||||
*/
|
||||
getAllTemplates(): BlueprintNodeTemplate[] {
|
||||
return Array.from(this._nodes.values()).map(d => d.template);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get templates by category
|
||||
* 按类别获取模板
|
||||
*/
|
||||
getTemplatesByCategory(category: string): BlueprintNodeTemplate[] {
|
||||
return this.getAllTemplates().filter(t => t.category === category);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search templates by keyword
|
||||
* 按关键词搜索模板
|
||||
*/
|
||||
searchTemplates(keyword: string): BlueprintNodeTemplate[] {
|
||||
const lower = keyword.toLowerCase();
|
||||
return this.getAllTemplates().filter(t =>
|
||||
t.title.toLowerCase().includes(lower) ||
|
||||
t.type.toLowerCase().includes(lower) ||
|
||||
t.keywords?.some(k => k.toLowerCase().includes(lower)) ||
|
||||
t.description?.toLowerCase().includes(lower)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all registrations (for testing)
|
||||
* 清除所有注册(用于测试)
|
||||
*/
|
||||
clear(): void {
|
||||
this._nodes.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decorator for registering node executors
|
||||
* 用于注册节点执行器的装饰器
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* @RegisterNode(EventTickTemplate)
|
||||
* class EventTickExecutor implements INodeExecutor {
|
||||
* execute(node, context) { ... }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function RegisterNode(template: BlueprintNodeTemplate) {
|
||||
return function<T extends new () => INodeExecutor>(constructor: T) {
|
||||
const executor = new constructor();
|
||||
NodeRegistry.instance.register(template, executor);
|
||||
return constructor;
|
||||
};
|
||||
}
|
||||
10
packages/blueprint/src/runtime/index.ts
Normal file
10
packages/blueprint/src/runtime/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Blueprint Runtime - Execution engine for blueprints
|
||||
* 蓝图运行时 - 蓝图执行引擎
|
||||
*/
|
||||
|
||||
export * from './ExecutionContext';
|
||||
export * from './NodeRegistry';
|
||||
export * from './BlueprintVM';
|
||||
export * from './BlueprintComponent';
|
||||
export * from './BlueprintSystem';
|
||||
125
packages/blueprint/src/types/blueprint.ts
Normal file
125
packages/blueprint/src/types/blueprint.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
/**
|
||||
* Blueprint Asset Types
|
||||
* 蓝图资产类型
|
||||
*/
|
||||
|
||||
import { BlueprintNode, BlueprintConnection } from './nodes';
|
||||
|
||||
/**
|
||||
* Variable scope determines lifetime and accessibility
|
||||
* 变量作用域决定生命周期和可访问性
|
||||
*/
|
||||
export type VariableScope =
|
||||
| 'local' // Per-execution (每次执行)
|
||||
| 'instance' // Per-entity (每个实体)
|
||||
| 'global'; // Shared across all (全局共享)
|
||||
|
||||
/**
|
||||
* Blueprint variable definition
|
||||
* 蓝图变量定义
|
||||
*/
|
||||
export interface BlueprintVariable {
|
||||
/** Variable name (变量名) */
|
||||
name: string;
|
||||
|
||||
/** Variable type (变量类型) */
|
||||
type: string;
|
||||
|
||||
/** Default value (默认值) */
|
||||
defaultValue: unknown;
|
||||
|
||||
/** Variable scope (变量作用域) */
|
||||
scope: VariableScope;
|
||||
|
||||
/** Category for organization (分类) */
|
||||
category?: string;
|
||||
|
||||
/** Description tooltip (描述提示) */
|
||||
tooltip?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Blueprint asset metadata
|
||||
* 蓝图资产元数据
|
||||
*/
|
||||
export interface BlueprintMetadata {
|
||||
/** Blueprint name (蓝图名称) */
|
||||
name: string;
|
||||
|
||||
/** Description (描述) */
|
||||
description?: string;
|
||||
|
||||
/** Category for organization (分类) */
|
||||
category?: string;
|
||||
|
||||
/** Author (作者) */
|
||||
author?: string;
|
||||
|
||||
/** Creation timestamp (创建时间戳) */
|
||||
createdAt?: number;
|
||||
|
||||
/** Last modified timestamp (最后修改时间戳) */
|
||||
modifiedAt?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Blueprint asset format - saved to .bp files
|
||||
* 蓝图资产格式 - 保存为 .bp 文件
|
||||
*/
|
||||
export interface BlueprintAsset {
|
||||
/** Format version (格式版本) */
|
||||
version: number;
|
||||
|
||||
/** Asset type identifier (资产类型标识符) */
|
||||
type: 'blueprint';
|
||||
|
||||
/** Metadata (元数据) */
|
||||
metadata: BlueprintMetadata;
|
||||
|
||||
/** Variable definitions (变量定义) */
|
||||
variables: BlueprintVariable[];
|
||||
|
||||
/** Node instances (节点实例) */
|
||||
nodes: BlueprintNode[];
|
||||
|
||||
/** Connections between nodes (节点之间的连接) */
|
||||
connections: BlueprintConnection[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an empty blueprint asset
|
||||
* 创建空的蓝图资产
|
||||
*/
|
||||
export function createEmptyBlueprint(name: string): BlueprintAsset {
|
||||
return {
|
||||
version: 1,
|
||||
type: 'blueprint',
|
||||
metadata: {
|
||||
name,
|
||||
createdAt: Date.now(),
|
||||
modifiedAt: Date.now()
|
||||
},
|
||||
variables: [],
|
||||
nodes: [],
|
||||
connections: []
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a blueprint asset structure
|
||||
* 验证蓝图资产结构
|
||||
*/
|
||||
export function validateBlueprintAsset(asset: unknown): asset is BlueprintAsset {
|
||||
if (!asset || typeof asset !== 'object') return false;
|
||||
|
||||
const bp = asset as BlueprintAsset;
|
||||
|
||||
return (
|
||||
typeof bp.version === 'number' &&
|
||||
bp.type === 'blueprint' &&
|
||||
typeof bp.metadata === 'object' &&
|
||||
Array.isArray(bp.variables) &&
|
||||
Array.isArray(bp.nodes) &&
|
||||
Array.isArray(bp.connections)
|
||||
);
|
||||
}
|
||||
3
packages/blueprint/src/types/index.ts
Normal file
3
packages/blueprint/src/types/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './pins';
|
||||
export * from './nodes';
|
||||
export * from './blueprint';
|
||||
138
packages/blueprint/src/types/nodes.ts
Normal file
138
packages/blueprint/src/types/nodes.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
/**
|
||||
* Blueprint Node Types
|
||||
* 蓝图节点类型
|
||||
*/
|
||||
|
||||
import { BlueprintPinDefinition } from './pins';
|
||||
|
||||
/**
|
||||
* Node category for visual styling and organization
|
||||
* 节点类别,用于视觉样式和组织
|
||||
*/
|
||||
export type BlueprintNodeCategory =
|
||||
| 'event' // Event nodes - red (事件节点 - 红色)
|
||||
| 'flow' // Flow control - gray (流程控制 - 灰色)
|
||||
| 'entity' // Entity operations - blue (实体操作 - 蓝色)
|
||||
| 'component' // Component access - cyan (组件访问 - 青色)
|
||||
| 'math' // Math operations - green (数学运算 - 绿色)
|
||||
| 'logic' // Logic operations - red (逻辑运算 - 红色)
|
||||
| 'variable' // Variable access - purple (变量访问 - 紫色)
|
||||
| 'input' // Input handling - orange (输入处理 - 橙色)
|
||||
| 'physics' // Physics - yellow (物理 - 黄色)
|
||||
| 'audio' // Audio - pink (音频 - 粉色)
|
||||
| 'time' // Time utilities - cyan (时间工具 - 青色)
|
||||
| 'debug' // Debug utilities - gray (调试工具 - 灰色)
|
||||
| 'custom'; // Custom nodes (自定义节点)
|
||||
|
||||
/**
|
||||
* Node template definition - describes a type of node
|
||||
* 节点模板定义 - 描述一种节点类型
|
||||
*/
|
||||
export interface BlueprintNodeTemplate {
|
||||
/** Unique type identifier (唯一类型标识符) */
|
||||
type: string;
|
||||
|
||||
/** Display title (显示标题) */
|
||||
title: string;
|
||||
|
||||
/** Node category (节点类别) */
|
||||
category: BlueprintNodeCategory;
|
||||
|
||||
/** Optional subtitle (可选副标题) */
|
||||
subtitle?: string;
|
||||
|
||||
/** Icon name (图标名称) */
|
||||
icon?: string;
|
||||
|
||||
/** Description for documentation (文档描述) */
|
||||
description?: string;
|
||||
|
||||
/** Search keywords (搜索关键词) */
|
||||
keywords?: string[];
|
||||
|
||||
/** Menu path for node palette (节点面板的菜单路径) */
|
||||
menuPath?: string[];
|
||||
|
||||
/** Input pin definitions (输入引脚定义) */
|
||||
inputs: BlueprintPinDefinition[];
|
||||
|
||||
/** Output pin definitions (输出引脚定义) */
|
||||
outputs: BlueprintPinDefinition[];
|
||||
|
||||
/** Whether this node is pure (no exec pins) (是否是纯节点,无执行引脚) */
|
||||
isPure?: boolean;
|
||||
|
||||
/** Whether this node can be collapsed (是否可折叠) */
|
||||
collapsible?: boolean;
|
||||
|
||||
/** Custom header color override (自定义头部颜色) */
|
||||
headerColor?: string;
|
||||
|
||||
/** Node color for visual distinction (节点颜色用于视觉区分) */
|
||||
color?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Node instance in a blueprint graph
|
||||
* 蓝图图中的节点实例
|
||||
*/
|
||||
export interface BlueprintNode {
|
||||
/** Unique instance ID (唯一实例ID) */
|
||||
id: string;
|
||||
|
||||
/** Template type reference (模板类型引用) */
|
||||
type: string;
|
||||
|
||||
/** Position in graph (图中位置) */
|
||||
position: { x: number; y: number };
|
||||
|
||||
/** Custom data for this instance (此实例的自定义数据) */
|
||||
data: Record<string, unknown>;
|
||||
|
||||
/** Comment/note for this node (此节点的注释) */
|
||||
comment?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Connection between two pins
|
||||
* 两个引脚之间的连接
|
||||
*/
|
||||
export interface BlueprintConnection {
|
||||
/** Unique connection ID (唯一连接ID) */
|
||||
id: string;
|
||||
|
||||
/** Source node ID (源节点ID) */
|
||||
fromNodeId: string;
|
||||
|
||||
/** Source pin name (源引脚名称) */
|
||||
fromPin: string;
|
||||
|
||||
/** Target node ID (目标节点ID) */
|
||||
toNodeId: string;
|
||||
|
||||
/** Target pin name (目标引脚名称) */
|
||||
toPin: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the header color for a node category
|
||||
* 获取节点类别的头部颜色
|
||||
*/
|
||||
export function getNodeCategoryColor(category: BlueprintNodeCategory): string {
|
||||
const colors: Record<BlueprintNodeCategory, string> = {
|
||||
event: '#8b1e1e',
|
||||
flow: '#4a4a4a',
|
||||
entity: '#1e5a8b',
|
||||
component: '#1e8b8b',
|
||||
math: '#1e8b5a',
|
||||
logic: '#8b1e5a',
|
||||
variable: '#5a1e8b',
|
||||
input: '#8b5a1e',
|
||||
physics: '#8b8b1e',
|
||||
audio: '#8b1e6b',
|
||||
time: '#1e6b8b',
|
||||
debug: '#5a5a5a',
|
||||
custom: '#4a4a4a'
|
||||
};
|
||||
return colors[category] ?? colors.custom;
|
||||
}
|
||||
135
packages/blueprint/src/types/pins.ts
Normal file
135
packages/blueprint/src/types/pins.ts
Normal file
@@ -0,0 +1,135 @@
|
||||
/**
|
||||
* Blueprint Pin Types
|
||||
* 蓝图引脚类型
|
||||
*/
|
||||
|
||||
/**
|
||||
* Pin data type for blueprint nodes
|
||||
* 蓝图节点的引脚数据类型
|
||||
*/
|
||||
export type BlueprintPinType =
|
||||
| 'exec' // Execution flow (执行流)
|
||||
| 'bool' // Boolean (布尔)
|
||||
| 'int' // Integer (整数)
|
||||
| 'float' // Float (浮点数)
|
||||
| 'string' // String (字符串)
|
||||
| 'vector2' // 2D Vector (二维向量)
|
||||
| 'vector3' // 3D Vector (三维向量)
|
||||
| 'color' // RGBA Color (颜色)
|
||||
| 'entity' // Entity reference (实体引用)
|
||||
| 'component' // Component reference (组件引用)
|
||||
| 'object' // Generic object (通用对象)
|
||||
| 'array' // Array (数组)
|
||||
| 'any'; // Wildcard (通配符)
|
||||
|
||||
/**
|
||||
* Pin direction
|
||||
* 引脚方向
|
||||
*/
|
||||
export type BlueprintPinDirection = 'input' | 'output';
|
||||
|
||||
/**
|
||||
* Pin definition for node templates
|
||||
* 节点模板的引脚定义
|
||||
*
|
||||
* Note: direction is determined by whether the pin is in inputs[] or outputs[] array
|
||||
* 注意:方向由引脚在 inputs[] 还是 outputs[] 数组中决定
|
||||
*/
|
||||
export interface BlueprintPinDefinition {
|
||||
/** Unique name within node (节点内唯一名称) */
|
||||
name: string;
|
||||
|
||||
/** Pin data type (引脚数据类型) */
|
||||
type: BlueprintPinType;
|
||||
|
||||
/** Display name shown in the editor (编辑器中显示的名称) */
|
||||
displayName?: string;
|
||||
|
||||
/** Default value when not connected (未连接时的默认值) */
|
||||
defaultValue?: unknown;
|
||||
|
||||
/** Allow multiple connections (允许多个连接) */
|
||||
allowMultiple?: boolean;
|
||||
|
||||
/** Array element type if type is 'array' (数组元素类型) */
|
||||
arrayType?: BlueprintPinType;
|
||||
|
||||
/** Whether this pin is optional (是否可选) */
|
||||
optional?: boolean;
|
||||
|
||||
/** Tooltip description (提示描述) */
|
||||
tooltip?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Runtime pin with direction - used when processing pins
|
||||
* 带方向的运行时引脚 - 处理引脚时使用
|
||||
*/
|
||||
export interface BlueprintRuntimePin extends BlueprintPinDefinition {
|
||||
/** Pin direction (引脚方向) */
|
||||
direction: BlueprintPinDirection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pin instance in a node
|
||||
* 节点中的引脚实例
|
||||
*/
|
||||
export interface BlueprintPin {
|
||||
id: string;
|
||||
nodeId: string;
|
||||
definition: BlueprintPinDefinition;
|
||||
value?: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the color for a pin type
|
||||
* 获取引脚类型的颜色
|
||||
*/
|
||||
export function getPinTypeColor(type: BlueprintPinType): string {
|
||||
const colors: Record<BlueprintPinType, string> = {
|
||||
exec: '#ffffff',
|
||||
bool: '#cc0000',
|
||||
int: '#00d4aa',
|
||||
float: '#88cc00',
|
||||
string: '#ff88cc',
|
||||
vector2: '#d4aa00',
|
||||
vector3: '#ffcc00',
|
||||
color: '#ff8844',
|
||||
entity: '#0088ff',
|
||||
component: '#44aaff',
|
||||
object: '#4444aa',
|
||||
array: '#8844ff',
|
||||
any: '#888888'
|
||||
};
|
||||
return colors[type] ?? colors.any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if two pin types are compatible for connection
|
||||
* 检查两个引脚类型是否兼容连接
|
||||
*/
|
||||
export function arePinTypesCompatible(from: BlueprintPinType, to: BlueprintPinType): boolean {
|
||||
// Same type always compatible
|
||||
// 相同类型始终兼容
|
||||
if (from === to) return true;
|
||||
|
||||
// Any type is compatible with everything
|
||||
// any 类型与所有类型兼容
|
||||
if (from === 'any' || to === 'any') return true;
|
||||
|
||||
// Exec can only connect to exec
|
||||
// exec 只能连接 exec
|
||||
if (from === 'exec' || to === 'exec') return false;
|
||||
|
||||
// Numeric coercion
|
||||
// 数值类型转换
|
||||
const numericTypes: BlueprintPinType[] = ['int', 'float'];
|
||||
if (numericTypes.includes(from) && numericTypes.includes(to)) return true;
|
||||
|
||||
// Vector coercion
|
||||
// 向量类型转换
|
||||
const vectorTypes: BlueprintPinType[] = ['vector2', 'vector3', 'color'];
|
||||
if (vectorTypes.includes(from) && vectorTypes.includes(to)) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
28
packages/blueprint/tsconfig.json
Normal file
28
packages/blueprint/tsconfig.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"jsx": "react-jsx",
|
||||
"strict": true,
|
||||
"strictNullChecks": true,
|
||||
"noImplicitAny": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"composite": true,
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
106
packages/blueprint/vite.config.ts
Normal file
106
packages/blueprint/vite.config.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import { resolve } from 'path';
|
||||
import dts from 'vite-plugin-dts';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
/**
|
||||
* 自定义插件:将 CSS 转换为自执行的样式注入代码
|
||||
* Custom plugin: Convert CSS to self-executing style injection code
|
||||
*/
|
||||
function escapeUnsafeChars(str: string): string {
|
||||
const charMap: Record<string, string> = {
|
||||
'<': '\\u003C',
|
||||
'>': '\\u003E',
|
||||
'/': '\\u002F',
|
||||
'\\': '\\\\',
|
||||
'\u2028': '\\u2028',
|
||||
'\u2029': '\\u2029'
|
||||
};
|
||||
return str.replace(/[<>\\/\u2028\u2029]/g, (x) => charMap[x] || x);
|
||||
}
|
||||
|
||||
function injectCSSPlugin(): unknown {
|
||||
const cssIdMap = new Map<string, string>();
|
||||
let cssCounter = 0;
|
||||
|
||||
return {
|
||||
name: 'inject-css-plugin',
|
||||
enforce: 'post' as const,
|
||||
generateBundle(_options: unknown, bundle: Record<string, { type?: string; source?: string; code?: string }>) {
|
||||
const bundleKeys = Object.keys(bundle);
|
||||
|
||||
// 找到所有 CSS 文件
|
||||
const cssFiles = bundleKeys.filter(key => key.endsWith('.css'));
|
||||
|
||||
for (const cssFile of cssFiles) {
|
||||
const cssChunk = bundle[cssFile];
|
||||
if (!cssChunk || !cssChunk.source) continue;
|
||||
|
||||
const cssContent = cssChunk.source;
|
||||
const styleId = `esengine-blueprint-style-${cssCounter++}`;
|
||||
cssIdMap.set(cssFile, styleId);
|
||||
|
||||
// 生成样式注入代码
|
||||
const injectCode = `(function(){if(typeof document!=='undefined'){var s=document.createElement('style');s.id='${styleId}';if(!document.getElementById(s.id)){s.textContent=${escapeUnsafeChars(JSON.stringify(cssContent))};document.head.appendChild(s);}}})();`;
|
||||
|
||||
// 注入到 editor/index.js 或共享 chunk
|
||||
for (const jsKey of bundleKeys) {
|
||||
if (!jsKey.endsWith('.js')) continue;
|
||||
const jsChunk = bundle[jsKey];
|
||||
if (!jsChunk || jsChunk.type !== 'chunk' || !jsChunk.code) continue;
|
||||
|
||||
if (jsKey === 'editor/index.js' || jsKey.match(/^index-[^/]+\.js$/)) {
|
||||
jsChunk.code = injectCode + '\n' + jsChunk.code;
|
||||
}
|
||||
}
|
||||
|
||||
// 删除独立的 CSS 文件
|
||||
delete bundle[cssFile];
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
react(),
|
||||
dts({
|
||||
include: ['src'],
|
||||
outDir: 'dist',
|
||||
rollupTypes: false
|
||||
}),
|
||||
injectCSSPlugin()
|
||||
],
|
||||
esbuild: {
|
||||
jsx: 'automatic',
|
||||
},
|
||||
build: {
|
||||
lib: {
|
||||
entry: {
|
||||
index: resolve(__dirname, 'src/index.ts'),
|
||||
'editor/index': resolve(__dirname, 'src/editor/index.ts')
|
||||
},
|
||||
formats: ['es'],
|
||||
fileName: (_format, entryName) => `${entryName}.js`
|
||||
},
|
||||
rollupOptions: {
|
||||
external: [
|
||||
'@esengine/ecs-framework',
|
||||
'@esengine/editor-runtime',
|
||||
'react',
|
||||
'react/jsx-runtime',
|
||||
'lucide-react',
|
||||
'zustand',
|
||||
/^@esengine\//,
|
||||
/^@tauri-apps\//
|
||||
],
|
||||
output: {
|
||||
exports: 'named',
|
||||
preserveModules: false
|
||||
}
|
||||
},
|
||||
target: 'es2020',
|
||||
minify: false,
|
||||
sourcemap: true
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user