feat(blueprint, node-editor): 重构蓝图装饰器系统,添加 Shadow DOM 支持 (#430)
**blueprint** - 移除 Reflect.getMetadata 依赖,装饰器要求显式指定类型 - 新增 ECS 节点:Entity、Component、Flow 控制节点 - 新增组件自动注册系统 (BlueprintExpose, BlueprintProperty, BlueprintMethod) - 删除未实现的事件节点占位文件 **node-editor** - 新增 injectNodeEditorStyles() 函数支持 Shadow DOM 样式注入 - 导出 nodeEditorCssText 用于手动样式注入
This commit is contained in:
@@ -9,7 +9,8 @@
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js"
|
||||
"import": "./dist/index.js",
|
||||
"require": "./dist/index.cjs"
|
||||
},
|
||||
"./styles": {
|
||||
"import": "./dist/styles/index.css"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useRef, useCallback, useState, useMemo } from 'react';
|
||||
import React, { useRef, useCallback, useState, useMemo, useEffect } from 'react';
|
||||
import { Graph } from '../../domain/models/Graph';
|
||||
import { GraphNode, NodeTemplate } from '../../domain/models/GraphNode';
|
||||
import { Connection } from '../../domain/models/Connection';
|
||||
@@ -127,6 +127,18 @@ export const NodeEditor: React.FC<NodeEditorProps> = ({
|
||||
const [connectionDrag, setConnectionDrag] = useState<ConnectionDragState | null>(null);
|
||||
const [hoveredPin, setHoveredPin] = useState<Pin | null>(null);
|
||||
|
||||
// Force re-render after mount to ensure connections are drawn correctly
|
||||
// 挂载后强制重渲染以确保连接线正确绘制
|
||||
const [, forceUpdate] = useState(0);
|
||||
useEffect(() => {
|
||||
// Use requestAnimationFrame to wait for DOM to be fully rendered
|
||||
// 使用 requestAnimationFrame 等待 DOM 完全渲染
|
||||
const rafId = requestAnimationFrame(() => {
|
||||
forceUpdate(n => n + 1);
|
||||
});
|
||||
return () => cancelAnimationFrame(rafId);
|
||||
}, [graph.id]);
|
||||
|
||||
/**
|
||||
* Converts screen coordinates to canvas coordinates
|
||||
* 将屏幕坐标转换为画布坐标
|
||||
|
||||
@@ -10,6 +10,9 @@
|
||||
// Import styles (导入样式)
|
||||
import './styles/index.css';
|
||||
|
||||
// CSS utilities for Shadow DOM (Shadow DOM 的 CSS 工具)
|
||||
export { nodeEditorCssText, injectNodeEditorStyles } from './styles/cssText';
|
||||
|
||||
// Domain models (领域模型)
|
||||
export {
|
||||
// Models
|
||||
|
||||
55
packages/devtools/node-editor/src/styles/cssText.ts
Normal file
55
packages/devtools/node-editor/src/styles/cssText.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* @zh 节点编辑器 CSS 样式文本
|
||||
* @en Node Editor CSS style text
|
||||
*
|
||||
* @zh 此文件在构建时由 vite 插件自动生成
|
||||
* @en This file is auto-generated by vite plugin during build
|
||||
*/
|
||||
|
||||
// Placeholder - will be replaced by vite plugin during build
|
||||
export const nodeEditorCssText = '__NODE_EDITOR_CSS_PLACEHOLDER__';
|
||||
|
||||
/**
|
||||
* @zh 将 CSS 注入到指定的根节点(支持 Shadow DOM)
|
||||
* @en Inject CSS into specified root node (supports Shadow DOM)
|
||||
*
|
||||
* @param root - @zh 目标根节点(Document 或 ShadowRoot)@en Target root node (Document or ShadowRoot)
|
||||
* @param styleId - @zh 样式标签的 ID @en ID for the style tag
|
||||
* @returns @zh 创建的 style 元素 @en The created style element
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Inject into Shadow DOM
|
||||
* const shadowRoot = element.attachShadow({ mode: 'open' });
|
||||
* injectNodeEditorStyles(shadowRoot);
|
||||
*
|
||||
* // Inject into document (with custom ID)
|
||||
* injectNodeEditorStyles(document, 'my-editor-styles');
|
||||
* ```
|
||||
*/
|
||||
export function injectNodeEditorStyles(
|
||||
root: Document | ShadowRoot | DocumentFragment,
|
||||
styleId: string = 'esengine-node-editor-styles'
|
||||
): HTMLStyleElement | null {
|
||||
// Check if already injected
|
||||
const existingStyle = (root as any).getElementById?.(styleId) ||
|
||||
(root as any).querySelector?.(`#${styleId}`);
|
||||
if (existingStyle) {
|
||||
return existingStyle as HTMLStyleElement;
|
||||
}
|
||||
|
||||
// Create and inject style element
|
||||
const style = document.createElement('style');
|
||||
style.id = styleId;
|
||||
style.textContent = nodeEditorCssText;
|
||||
|
||||
if ('head' in root) {
|
||||
// Document
|
||||
(root as Document).head.appendChild(style);
|
||||
} else {
|
||||
// ShadowRoot or DocumentFragment
|
||||
root.appendChild(style);
|
||||
}
|
||||
|
||||
return style;
|
||||
}
|
||||
@@ -4,12 +4,14 @@ import dts from 'vite-plugin-dts';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
/**
|
||||
* Custom plugin: Convert CSS to self-executing style injection code
|
||||
* 自定义插件:将 CSS 转换为自执行的样式注入代码
|
||||
* Custom plugin: Handle CSS for node editor
|
||||
* 自定义插件:处理节点编辑器的 CSS
|
||||
*
|
||||
* This plugin does two things:
|
||||
* 1. Auto-injects CSS into document.head for normal usage
|
||||
* 2. Replaces placeholder in cssText.ts with actual CSS for Shadow DOM usage
|
||||
*/
|
||||
function injectCSSPlugin(): any {
|
||||
let cssCounter = 0;
|
||||
|
||||
return {
|
||||
name: 'inject-css-plugin',
|
||||
enforce: 'post' as const,
|
||||
@@ -23,19 +25,28 @@ function injectCSSPlugin(): any {
|
||||
const cssChunk = bundle[cssFile];
|
||||
if (!cssChunk || !cssChunk.source) continue;
|
||||
|
||||
const cssContent = cssChunk.source;
|
||||
const styleId = `esengine-node-editor-style-${cssCounter++}`;
|
||||
const cssContent = cssChunk.source as string;
|
||||
const styleId = 'esengine-node-editor-styles';
|
||||
|
||||
// Generate style injection code (生成样式注入代码)
|
||||
const injectCode = `(function(){if(typeof document!=='undefined'){var s=document.createElement('style');s.id='${styleId}';if(!document.getElementById(s.id)){s.textContent=${JSON.stringify(cssContent)};document.head.appendChild(s);}}})();`;
|
||||
|
||||
// Inject into index.js (注入到 index.js)
|
||||
// Process all JS bundles (处理所有 JS 包)
|
||||
for (const jsKey of bundleKeys) {
|
||||
if (!jsKey.endsWith('.js')) continue;
|
||||
if (!jsKey.endsWith('.js') && !jsKey.endsWith('.cjs')) continue;
|
||||
const jsChunk = bundle[jsKey];
|
||||
if (!jsChunk || jsChunk.type !== 'chunk' || !jsChunk.code) continue;
|
||||
|
||||
if (jsKey === 'index.js') {
|
||||
// Replace CSS placeholder with actual CSS content
|
||||
// 将 CSS 占位符替换为实际的 CSS 内容
|
||||
// Match both single and double quotes (ESM uses single, CJS uses double)
|
||||
jsChunk.code = jsChunk.code.replace(
|
||||
/['"]__NODE_EDITOR_CSS_PLACEHOLDER__['"]/g,
|
||||
JSON.stringify(cssContent)
|
||||
);
|
||||
|
||||
// Auto-inject CSS for index bundles (为 index 包自动注入 CSS)
|
||||
if (jsKey === 'index.js' || jsKey === 'index.cjs') {
|
||||
jsChunk.code = injectCode + '\n' + jsChunk.code;
|
||||
}
|
||||
}
|
||||
@@ -65,8 +76,11 @@ export default defineConfig({
|
||||
entry: {
|
||||
index: resolve(__dirname, 'src/index.ts')
|
||||
},
|
||||
formats: ['es'],
|
||||
fileName: (format, entryName) => `${entryName}.js`
|
||||
formats: ['es', 'cjs'],
|
||||
fileName: (format, entryName) => {
|
||||
if (format === 'cjs') return `${entryName}.cjs`;
|
||||
return `${entryName}.js`;
|
||||
}
|
||||
},
|
||||
rollupOptions: {
|
||||
external: [
|
||||
|
||||
Reference in New Issue
Block a user