Feature/editor optimization (#251)
* refactor: 编辑器/运行时架构拆分与构建系统升级 * feat(core): 层级系统重构与UI变换矩阵修复 * refactor: 移除 ecs-components 聚合包并修复跨包组件查找问题 * fix(physics): 修复跨包组件类引用问题 * feat: 统一运行时架构与浏览器运行支持 * feat(asset): 实现浏览器运行时资产加载系统 * fix: 修复文档、CodeQL安全问题和CI类型检查错误 * fix: 修复文档、CodeQL安全问题和CI类型检查错误 * fix: 修复文档、CodeQL安全问题、CI类型检查和测试错误 * test: 补齐核心模块测试用例,修复CI构建配置 * fix: 修复测试用例中的类型错误和断言问题 * fix: 修复 turbo build:npm 任务的依赖顺序问题 * fix: 修复 CI 构建错误并优化构建性能
This commit is contained in:
@@ -1,85 +1,52 @@
|
||||
{
|
||||
"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"
|
||||
"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"
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"clean": "rimraf dist tsconfig.tsbuildinfo",
|
||||
"build": "tsup",
|
||||
"build:watch": "tsup --watch",
|
||||
"type-check": "tsc --noEmit"
|
||||
},
|
||||
"./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
|
||||
"keywords": [
|
||||
"ecs",
|
||||
"blueprint",
|
||||
"visual-scripting",
|
||||
"game-engine"
|
||||
],
|
||||
"author": "yhh",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@esengine/ecs-framework": "workspace:*",
|
||||
"@esengine/build-config": "workspace:*",
|
||||
"@types/node": "^20.19.17",
|
||||
"rimraf": "^5.0.0",
|
||||
"tsup": "^8.0.0",
|
||||
"typescript": "^5.8.3"
|
||||
},
|
||||
"@esengine/node-editor": {
|
||||
"optional": true
|
||||
"dependencies": {
|
||||
"tslib": "^2.8.1"
|
||||
},
|
||||
"lucide-react": {
|
||||
"optional": true
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://registry.npmjs.org/"
|
||||
},
|
||||
"react": {
|
||||
"optional": true
|
||||
},
|
||||
"zustand": {
|
||||
"optional": true
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/esengine/ecs-framework.git",
|
||||
"directory": "packages/blueprint"
|
||||
}
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,180 +0,0 @@
|
||||
/**
|
||||
* 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()
|
||||
};
|
||||
@@ -1,383 +0,0 @@
|
||||
/**
|
||||
* 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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,48 +0,0 @@
|
||||
/**
|
||||
* 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>
|
||||
);
|
||||
};
|
||||
@@ -1,7 +0,0 @@
|
||||
/**
|
||||
* Blueprint Editor Components
|
||||
* 蓝图编辑器组件
|
||||
*/
|
||||
|
||||
export * from './BlueprintCanvas';
|
||||
export * from './BlueprintEditorPanel';
|
||||
@@ -1,8 +0,0 @@
|
||||
/**
|
||||
* Blueprint Editor Module
|
||||
* 蓝图编辑器模块
|
||||
*/
|
||||
|
||||
export * from './components';
|
||||
export * from './stores';
|
||||
export * from './BlueprintPlugin';
|
||||
@@ -1,256 +0,0 @@
|
||||
/**
|
||||
* 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 });
|
||||
}
|
||||
}));
|
||||
@@ -1,6 +0,0 @@
|
||||
/**
|
||||
* Blueprint Editor Stores
|
||||
* 蓝图编辑器状态存储
|
||||
*/
|
||||
|
||||
export * from './blueprintEditorStore';
|
||||
23
packages/blueprint/tsconfig.build.json
Normal file
23
packages/blueprint/tsconfig.build.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "ES2020",
|
||||
"moduleResolution": "bundler",
|
||||
"lib": ["ES2020", "DOM"],
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"jsx": "react-jsx",
|
||||
"resolveJsonModule": true,
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
7
packages/blueprint/tsup.config.ts
Normal file
7
packages/blueprint/tsup.config.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { defineConfig } from 'tsup';
|
||||
import { runtimeOnlyPreset } from '../build-config/src/presets/plugin-tsup';
|
||||
|
||||
export default defineConfig({
|
||||
...runtimeOnlyPreset(),
|
||||
tsconfig: 'tsconfig.build.json'
|
||||
});
|
||||
@@ -1,106 +0,0 @@
|
||||
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