refactor: reorganize package structure and decouple framework packages (#338)

* refactor: reorganize package structure and decouple framework packages

## Package Structure Reorganization
- Reorganized 55 packages into categorized subdirectories:
  - packages/framework/ - Generic framework (Laya/Cocos compatible)
  - packages/engine/ - ESEngine core modules
  - packages/rendering/ - Rendering modules (WASM dependent)
  - packages/physics/ - Physics modules
  - packages/streaming/ - World streaming
  - packages/network-ext/ - Network extensions
  - packages/editor/ - Editor framework and plugins
  - packages/rust/ - Rust WASM engine
  - packages/tools/ - Build tools and SDK

## Framework Package Decoupling
- Decoupled behavior-tree and blueprint packages from ESEngine dependencies
- Created abstracted interfaces (IBTAssetManager, IBehaviorTreeAssetContent)
- ESEngine-specific code moved to esengine/ subpath exports
- Framework packages now usable with Cocos/Laya without ESEngine

## CI Configuration
- Updated CI to only type-check and lint framework packages
- Added type-check:framework and lint:framework scripts

## Breaking Changes
- Package import paths changed due to directory reorganization
- ESEngine integrations now use subpath imports (e.g., '@esengine/behavior-tree/esengine')

* fix: update es-engine file path after directory reorganization

* docs: update README to focus on framework over engine

* ci: only build framework packages, remove Rust/WASM dependencies

* fix: remove esengine subpath from behavior-tree and blueprint builds

ESEngine integration code will only be available in full engine builds.
Framework packages are now purely engine-agnostic.

* fix: move network-protocols to framework, build both in CI

* fix: update workflow paths from packages/core to packages/framework/core

* fix: exclude esengine folder from type-check in behavior-tree and blueprint

* fix: update network tsconfig references to new paths

* fix: add test:ci:framework to only test framework packages in CI

* fix: only build core and math npm packages in CI

* fix: exclude test files from CodeQL and fix string escaping security issue
This commit is contained in:
YHH
2025-12-26 14:50:35 +08:00
committed by GitHub
parent a84ff902e4
commit 155411e743
1936 changed files with 4147 additions and 11578 deletions

View File

@@ -0,0 +1,15 @@
{
"id": "blueprint-editor",
"name": "@esengine/blueprint-editor",
"displayName": "Blueprint Editor",
"description": "Visual scripting editor | 可视化脚本编辑器",
"version": "1.0.0",
"category": "Editor",
"icon": "Workflow",
"isEditorPlugin": true,
"runtimeModule": "@esengine/blueprint",
"exports": {
"inspectors": ["BlueprintComponentInspector"],
"panels": ["BlueprintEditorPanel"]
}
}

View File

@@ -0,0 +1,53 @@
{
"name": "@esengine/blueprint-editor",
"version": "1.0.0",
"description": "Editor support for @esengine/blueprint - visual scripting editor",
"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"
}
},
"files": [
"dist"
],
"scripts": {
"build": "tsup",
"build:watch": "tsup --watch",
"type-check": "tsc --noEmit",
"clean": "rimraf dist"
},
"dependencies": {
"@esengine/blueprint": "workspace:*"
},
"peerDependencies": {
"@esengine/editor-core": "workspace:*"
},
"devDependencies": {
"@esengine/ecs-framework": "workspace:*",
"@esengine/engine-core": "workspace:*",
"@esengine/editor-core": "workspace:*",
"@esengine/node-editor": "workspace:*",
"@esengine/build-config": "workspace:*",
"lucide-react": "^0.545.0",
"react": "^18.3.1",
"zustand": "^5.0.8",
"@types/react": "^18.3.12",
"rimraf": "^5.0.5",
"tsup": "^8.0.0",
"typescript": "^5.3.3"
},
"keywords": [
"ecs",
"blueprint",
"editor",
"visual-scripting"
],
"author": "",
"license": "MIT",
"private": true
}

View File

@@ -0,0 +1,130 @@
/**
* Blueprint Editor Plugin
* 蓝图编辑器插件
*/
import { Core, type ServiceContainer } from '@esengine/ecs-framework';
import type { ModuleManifest } from '@esengine/engine-core';
import type { IEditorPlugin, IEditorModuleLoader, PanelDescriptor, FileActionHandler, FileCreationTemplate } from '@esengine/editor-core';
import { MessageHub, PanelPosition } from '@esengine/editor-core';
// Re-export from @esengine/blueprint for runtime module
import { NodeRegistry, BlueprintVM, createBlueprintSystem } from '@esengine/blueprint';
// Store for pending file path
import { useBlueprintEditorStore } from './stores/blueprintEditorStore';
// Direct import of panel component (not dynamic import)
import { BlueprintEditorPanel } from './components/BlueprintEditorPanel';
/**
* Blueprint Editor Module Implementation
* 蓝图编辑器模块实现
*/
class BlueprintEditorModuleImpl implements IEditorModuleLoader {
async install(_services: ServiceContainer): Promise<void> {
// Editor module installation
}
async uninstall(): Promise<void> {
// Cleanup
}
getPanels(): PanelDescriptor[] {
return [
{
id: 'blueprint-editor',
title: 'Blueprint Editor',
position: PanelPosition.Center,
icon: 'Workflow',
closable: true,
resizable: true,
order: 50,
component: BlueprintEditorPanel,
isDynamic: true
}
];
}
getFileActionHandlers(): FileActionHandler[] {
return [
{
// 扩展名不带点号,与 FileActionRegistry.getFileExtension() 保持一致
// Extensions without dot prefix, consistent with FileActionRegistry.getFileExtension()
extensions: ['blueprint', 'bp'],
onDoubleClick: (filePath: string) => {
// 设置待加载的文件路径到 store
// Set pending file path to store
useBlueprintEditorStore.getState().setPendingFilePath(filePath);
// 通过 MessageHub 打开蓝图编辑器面板
// Open blueprint editor panel via MessageHub
const messageHub = Core.services.resolve(MessageHub);
if (messageHub) {
messageHub.publish('dynamic-panel:open', {
panelId: 'blueprint-editor',
title: `Blueprint - ${filePath.split(/[\\/]/).pop()}`
});
}
}
}
];
}
getFileCreationTemplates(): FileCreationTemplate[] {
return [
{
id: 'blueprint',
label: 'Blueprint',
// 扩展名不带点号FileTree 会自动添加点号
// Extension without dot, FileTree will add the dot automatically
extension: 'blueprint',
icon: 'Workflow',
getContent: (fileName: string) => {
const name = fileName.replace(/\.blueprint$/i, '') || 'NewBlueprint';
return JSON.stringify({
version: '1.0.0',
name,
nodes: [],
connections: [],
variables: []
}, null, 2);
}
}
];
}
}
const manifest: ModuleManifest = {
id: '@esengine/blueprint',
name: '@esengine/blueprint',
displayName: 'Blueprint',
version: '1.0.0',
description: 'Visual scripting system for ECS Framework',
category: 'Other',
isCore: false,
defaultEnabled: false,
isEngineModule: true,
canContainContent: true,
dependencies: ['engine-core'],
exports: {
components: ['BlueprintComponent'],
systems: ['BlueprintSystem'],
other: ['NodeRegistry', 'BlueprintVM']
}
};
/**
* Complete Blueprint plugin with both runtime and editor modules
* 完整的蓝图插件,包含运行时和编辑器模块
*/
export const BlueprintPlugin: IEditorPlugin = {
manifest,
editorModule: new BlueprintEditorModuleImpl()
};
// Also export the editor module instance for direct use
export const BlueprintEditorModule = new BlueprintEditorModuleImpl();
// Re-export useful items
export { NodeRegistry, BlueprintVM, createBlueprintSystem };

View File

@@ -0,0 +1,384 @@
/**
* 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 '@esengine/blueprint';
import type { BlueprintNode, BlueprintConnection, BlueprintNodeTemplate, BlueprintPinDefinition } from '@esengine/blueprint';
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);
}
}
// 安全访问 metadata.name兼容旧格式文件
const blueprintName = blueprint.metadata?.name || (blueprint as any).name || 'Blueprint';
return new Graph('blueprint', blueprintName, 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}
/>
</>
);
};

View File

@@ -0,0 +1,90 @@
/**
* Blueprint Editor Panel - Main panel for blueprint editing
* 蓝图编辑器面板 - 蓝图编辑的主面板
*/
import React, { useEffect } from 'react';
import { Core } from '@esengine/ecs-framework';
import { IFileSystemService, type IFileSystem } from '@esengine/editor-core';
import { BlueprintCanvas } from './BlueprintCanvas';
import { useBlueprintEditorStore } from '../stores/blueprintEditorStore';
import type { BlueprintAsset } from '@esengine/blueprint';
// Import blueprint package to register nodes
// 导入蓝图包以注册节点
import '@esengine/blueprint';
/**
* 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,
pendingFilePath,
createNewBlueprint,
loadBlueprint,
setPendingFilePath
} = useBlueprintEditorStore();
// Load blueprint from pending file path
// 从待加载的文件路径加载蓝图
useEffect(() => {
if (!pendingFilePath) return;
const loadBlueprintFile = async () => {
try {
const fileSystem = Core.services.tryResolve(IFileSystemService) as IFileSystem | null;
if (!fileSystem) {
console.error('[BlueprintEditorPanel] FileSystem service not available');
setPendingFilePath(null);
createNewBlueprint('New Blueprint');
return;
}
const content = await fileSystem.readFile(pendingFilePath);
const asset = JSON.parse(content) as BlueprintAsset;
loadBlueprint(asset, pendingFilePath);
setPendingFilePath(null);
console.log('[BlueprintEditorPanel] Loaded blueprint from file:', pendingFilePath);
} catch (error) {
console.error('[BlueprintEditorPanel] Failed to load blueprint file:', error);
setPendingFilePath(null);
// 加载失败时创建新蓝图
createNewBlueprint('New Blueprint');
}
};
loadBlueprintFile();
}, [pendingFilePath, loadBlueprint, setPendingFilePath, createNewBlueprint]);
// Create a default blueprint if none exists and no pending file
// 如果不存在蓝图且没有待加载文件,则创建默认蓝图
useEffect(() => {
if (!blueprint && !pendingFilePath) {
createNewBlueprint('New Blueprint');
}
}, [blueprint, pendingFilePath, createNewBlueprint]);
return (
<div style={panelStyles}>
<BlueprintCanvas />
</div>
);
};

View File

@@ -0,0 +1,7 @@
/**
* Blueprint Editor Components
* 蓝图编辑器组件
*/
export * from './BlueprintCanvas';
export * from './BlueprintEditorPanel';

View File

@@ -0,0 +1,8 @@
/**
* Blueprint Editor Module
* 蓝图编辑器模块
*/
export * from './components';
export * from './stores';
export * from './BlueprintPlugin';

View File

@@ -0,0 +1,275 @@
/**
* Blueprint Editor Store - State management for blueprint editor
* 蓝图编辑器状态管理
*/
import { create } from 'zustand';
import { createEmptyBlueprint } from '@esengine/blueprint';
import type { BlueprintAsset, BlueprintNode, BlueprintConnection } from '@esengine/blueprint';
/**
* 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;
/** Pending file path to load when panel opens (面板打开时待加载的文件路径) */
pendingFilePath: string | null;
/** 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;
/** Set pending file path (设置待加载的文件路径) */
setPendingFilePath: (path: string | null) => void;
}
/**
* Generate unique ID for nodes and connections
* 为节点和连接生成唯一ID
*/
function generateId(): string {
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}
/**
* 更新 metadata 的修改时间
* Update metadata modification time
*/
function getUpdatedMetadata(blueprint: BlueprintAsset): BlueprintAsset['metadata'] {
return { ...blueprint.metadata, modifiedAt: Date.now() };
}
/**
* Blueprint editor store
* 蓝图编辑器状态存储
*/
export const useBlueprintEditorStore = create<BlueprintEditorState>((set, get) => ({
blueprint: null,
selectedNodeIds: [],
draggingNodeId: null,
panOffset: { x: 0, y: 0 },
zoom: 1,
isDirty: false,
pendingFilePath: null,
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: getUpdatedMetadata(blueprint)
},
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: getUpdatedMetadata(blueprint)
},
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: getUpdatedMetadata(blueprint)
},
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: getUpdatedMetadata(blueprint)
},
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: getUpdatedMetadata(blueprint)
},
isDirty: true
});
},
removeConnection: (connectionId: string) => {
const { blueprint } = get();
if (!blueprint) return;
set({
blueprint: {
...blueprint,
connections: blueprint.connections.filter(c => c.id !== connectionId),
metadata: getUpdatedMetadata(blueprint)
},
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 });
},
setPendingFilePath: (path: string | null) => {
set({ pendingFilePath: path });
}
}));

View File

@@ -0,0 +1,6 @@
/**
* Blueprint Editor Stores
* 蓝图编辑器状态存储
*/
export * from './blueprintEditorStore';

View 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"]
}

View File

@@ -0,0 +1,11 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src",
"declaration": true,
"jsx": "react-jsx"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}

View File

@@ -0,0 +1,7 @@
import { defineConfig } from 'tsup';
import { editorOnlyPreset } from '../../../tools/build-config/src/presets/plugin-tsup';
export default defineConfig({
...editorOnlyPreset(),
tsconfig: 'tsconfig.build.json'
});