Feature/runtime cdn and plugin loader (#240)

* feat(ui): 完善 UI 布局系统和编辑器可视化工具

* refactor: 移除 ModuleRegistry,统一使用 PluginManager 插件系统

* fix: 修复 CodeQL 警告并提升测试覆盖率

* refactor: 分离运行时入口点,解决 runtime bundle 包含 React 的问题

* fix(ci): 添加 editor-core 和 editor-runtime 到 CI 依赖构建步骤

* docs: 完善 ServiceContainer 文档,新增 Symbol.for 模式和 @InjectProperty 说明

* fix(ci): 修复 type-check 失败问题

* fix(ci): 修复类型检查失败问题

* fix(ci): 修复类型检查失败问题

* fix(ci): behavior-tree 构建添加 @tauri-apps 外部依赖

* fix(ci): behavior-tree 添加 @tauri-apps/plugin-fs 类型依赖

* fix(ci): platform-web 添加缺失的 behavior-tree 依赖

* fix(lint): 移除正则表达式中不必要的转义字符
This commit is contained in:
YHH
2025-11-27 20:42:46 +08:00
committed by GitHub
parent 71869b1a58
commit 107439d70c
367 changed files with 10661 additions and 12473 deletions

View File

@@ -1,4 +1,9 @@
import { useCallback, useRef, useEffect, useState } from 'react';
/**
* FlexLayoutDockContainer - Dockable panel container based on FlexLayout
* FlexLayoutDockContainer - 基于 FlexLayout 的可停靠面板容器
*/
import { useCallback, useRef, useEffect, useState, useMemo } from 'react';
import { Layout, Model, TabNode, IJsonModel, Actions, Action, DockLocation } from 'flexlayout-react';
import 'flexlayout-react/style/light.css';
import '../styles/FlexLayoutDock.css';
@@ -6,10 +11,86 @@ import { LayoutMerger, LayoutBuilder, FlexDockPanel } from '../shared/layout';
export type { FlexDockPanel };
/**
* Panel IDs that should persist in DOM when switching tabs.
* These panels contain WebGL canvas or other stateful content that cannot be unmounted.
* 切换 tab 时需要保持 DOM 存在的面板 ID。
* 这些面板包含 WebGL canvas 或其他不能卸载的有状态内容。
*/
const PERSISTENT_PANEL_IDS = ['viewport'];
/** Tab header height in pixels | Tab 标签栏高度(像素) */
const TAB_HEADER_HEIGHT = 28;
interface PanelRect {
domRect: DOMRect;
isSelected: boolean;
isVisible?: boolean;
}
/**
* Get panel rectangle from FlexLayout model.
* 从 FlexLayout 模型获取面板矩形。
*/
function getPanelRectFromModel(model: Model, panelId: string): PanelRect | null {
const node = model.getNodeById(panelId);
if (!node || node.getType() !== 'tab') return null;
const parent = node.getParent();
if (!parent || parent.getType() !== 'tabset') return null;
const tabset = parent as any;
const selectedNode = tabset.getSelectedNode();
const isSelected = selectedNode?.getId() === panelId;
const tabsetRect = tabset.getRect();
if (!tabsetRect) return null;
return {
domRect: new DOMRect(
tabsetRect.x,
tabsetRect.y + TAB_HEADER_HEIGHT,
tabsetRect.width,
tabsetRect.height - TAB_HEADER_HEIGHT
),
isSelected
};
}
/**
* Get panel rectangle from DOM placeholder element.
* 从 DOM 占位符元素获取面板矩形。
*/
function getPanelRectFromDOM(panelId: string): PanelRect | null {
const placeholder = document.querySelector(`[data-panel-id="${panelId}"]`);
if (!placeholder) return null;
const placeholderRect = placeholder.getBoundingClientRect();
if (placeholderRect.width <= 0 || placeholderRect.height <= 0) return null;
const container = document.querySelector('.flexlayout-dock-container');
if (!container) return null;
const containerRect = container.getBoundingClientRect();
const parentTab = placeholder.closest('.flexlayout__tabset_content');
const isVisible = parentTab ? (parentTab as HTMLElement).offsetParent !== null : false;
return {
domRect: new DOMRect(
placeholderRect.x - containerRect.x,
placeholderRect.y - containerRect.y,
placeholderRect.width,
placeholderRect.height
),
isSelected: false,
isVisible
};
}
interface FlexLayoutDockContainerProps {
panels: FlexDockPanel[];
onPanelClose?: (panelId: string) => void;
activePanelId?: string;
panels: FlexDockPanel[];
onPanelClose?: (panelId: string) => void;
activePanelId?: string;
}
export function FlexLayoutDockContainer({ panels, onPanelClose, activePanelId }: FlexLayoutDockContainerProps) {
@@ -18,6 +99,17 @@ export function FlexLayoutDockContainer({ panels, onPanelClose, activePanelId }:
const previousPanelIdsRef = useRef<string>('');
const previousPanelTitlesRef = useRef<Map<string, string>>(new Map());
// Persistent panel state | 持久化面板状态
const [persistentPanelRects, setPersistentPanelRects] = useState<Map<string, DOMRect>>(new Map());
const [visiblePersistentPanels, setVisiblePersistentPanels] = useState<Set<string>>(
() => new Set(PERSISTENT_PANEL_IDS)
);
const persistentPanels = useMemo(
() => panels.filter((p) => PERSISTENT_PANEL_IDS.includes(p.id)),
[panels]
);
const createDefaultLayout = useCallback((): IJsonModel => {
return LayoutBuilder.createDefaultLayout(panels, activePanelId);
}, [panels, activePanelId]);
@@ -155,10 +247,80 @@ export function FlexLayoutDockContainer({ panels, onPanelClose, activePanelId }:
}
}, [createDefaultLayout, panels]);
/**
* Track persistent panel positions and visibility.
* Uses FlexLayout model to determine if panel tab is selected,
* falls back to DOM measurement if model data unavailable.
* 追踪持久化面板的位置和可见性。
* 使用 FlexLayout 模型判断面板 tab 是否被选中,
* 如果模型数据不可用则回退到 DOM 测量。
*/
useEffect(() => {
if (!model) return;
const updatePersistentPanelPositions = () => {
const newRects = new Map<string, DOMRect>();
const newVisible = new Set<string>();
for (const panelId of PERSISTENT_PANEL_IDS) {
// Try to get position from FlexLayout model
const rect = getPanelRectFromModel(model, panelId);
if (rect) {
newRects.set(panelId, rect.domRect);
if (rect.isSelected) {
newVisible.add(panelId);
}
continue;
}
// Fallback: measure placeholder element in DOM
const placeholderRect = getPanelRectFromDOM(panelId);
if (placeholderRect) {
newRects.set(panelId, placeholderRect.domRect);
if (placeholderRect.isVisible) {
newVisible.add(panelId);
}
}
}
setPersistentPanelRects(newRects);
setVisiblePersistentPanels(newVisible);
};
// Initial update after DOM render
requestAnimationFrame(updatePersistentPanelPositions);
// Observe layout changes
const container = document.querySelector('.flexlayout-dock-container');
if (!container) return;
const mutationObserver = new MutationObserver(() => {
requestAnimationFrame(updatePersistentPanelPositions);
});
mutationObserver.observe(container, { childList: true, subtree: true, attributes: true });
const resizeObserver = new ResizeObserver(() => {
requestAnimationFrame(updatePersistentPanelPositions);
});
resizeObserver.observe(container);
return () => {
mutationObserver.disconnect();
resizeObserver.disconnect();
};
}, [model]);
const factory = useCallback((node: TabNode) => {
const component = node.getComponent();
const panel = panels.find((p) => p.id === component);
return panel?.content || <div>Panel not found</div>;
const componentId = node.getComponent() || '';
// Persistent panels render as placeholder, actual content is in overlay
// 持久化面板渲染为占位符,实际内容在覆盖层中
if (PERSISTENT_PANEL_IDS.includes(componentId)) {
return <div className="persistent-panel-placeholder" data-panel-id={componentId} />;
}
const panel = panels.find((p) => p.id === componentId);
return panel?.content ?? <div>Panel not found</div>;
}, [panels]);
const onAction = useCallback((action: Action) => {
@@ -186,6 +348,52 @@ export function FlexLayoutDockContainer({ panels, onPanelClose, activePanelId }:
onAction={onAction}
onModelChange={onModelChange}
/>
{/* Persistent panel overlay - always mounted, visibility controlled by CSS */}
{/* 持久化面板覆盖层 - 始终挂载,通过 CSS 控制可见性 */}
{persistentPanels.map((panel) => (
<PersistentPanelContainer
key={panel.id}
panel={panel}
rect={persistentPanelRects.get(panel.id)}
isVisible={visiblePersistentPanels.has(panel.id)}
/>
))}
</div>
);
}
/**
* Container for persistent panel content.
* 持久化面板内容容器。
*/
function PersistentPanelContainer({
panel,
rect,
isVisible
}: {
panel: FlexDockPanel;
rect?: DOMRect;
isVisible: boolean;
}) {
const hasValidRect = rect && rect.width > 0 && rect.height > 0;
return (
<div
className="persistent-panel-container"
style={{
position: 'absolute',
left: hasValidRect ? rect.x : 0,
top: hasValidRect ? rect.y : 0,
width: hasValidRect ? rect.width : '100%',
height: hasValidRect ? rect.height : '100%',
visibility: isVisible ? 'visible' : 'hidden',
pointerEvents: isVisible ? 'auto' : 'none',
zIndex: isVisible ? 1 : -1,
overflow: 'hidden'
}}
>
{panel.content}
</div>
);
}