fix(editor): 修复右键菜单和粒子编辑器问题 (#286)

- 修复右键菜单被状态栏遮挡的问题
- 修复右键菜单边界检测,考虑标题栏和状态栏高度
- 调整右键菜单结构:新建文件夹 → 资源类型 → 工具操作
- 修复 Particle 插件默认未启用的问题(defaultEnabled 的新插件不被旧配置禁用)
- 修复 SizeOverLifetime 模块在预览中无效果的问题
- 移除 MaterialEditorModule 中的重复模板注册
This commit is contained in:
YHH
2025-12-06 11:56:25 +08:00
committed by GitHub
parent 397f79caa5
commit 3cbfa1e4cb
6 changed files with 153 additions and 53 deletions

View File

@@ -164,6 +164,34 @@ export function ContentBrowser({
template: FileCreationTemplate; template: FileCreationTemplate;
} | null>(null); } | null>(null);
// 文件创建模板列表(需要状态跟踪以便插件安装后刷新)
// File creation templates list (need state tracking to refresh after plugin installation)
const [fileCreationTemplates, setFileCreationTemplates] = useState<FileCreationTemplate[]>([]);
// 初始化和监听插件安装事件以更新模板列表
// Initialize and listen for plugin installation events to update template list
useEffect(() => {
const updateTemplates = () => {
if (fileActionRegistry) {
const templates = fileActionRegistry.getCreationTemplates();
setFileCreationTemplates([...templates]);
}
};
// 初始加载
updateTemplates();
// 监听插件安装/卸载事件
if (messageHub) {
const unsubInstall = messageHub.subscribe('plugin:installed', updateTemplates);
const unsubUninstall = messageHub.subscribe('plugin:uninstalled', updateTemplates);
return () => {
unsubInstall();
unsubUninstall();
};
}
}, [fileActionRegistry, messageHub]);
const t = { const t = {
en: { en: {
favorites: 'Favorites', favorites: 'Favorites',
@@ -844,7 +872,6 @@ export class ${className} {
const items: ContextMenuItem[] = []; const items: ContextMenuItem[] = [];
if (!asset) { if (!asset) {
// Background context menu
items.push({ items.push({
label: t.newFolder, label: t.newFolder,
icon: <FolderClosed size={16} />, icon: <FolderClosed size={16} />,
@@ -861,29 +888,55 @@ export class ${className} {
} }
}); });
if (fileActionRegistry) { if (fileCreationTemplates.length > 0) {
const templates = fileActionRegistry.getCreationTemplates(); items.push({ label: '', separator: true, onClick: () => {} });
if (templates.length > 0) {
items.push({ label: '', separator: true, onClick: () => {} }); for (const template of fileCreationTemplates) {
for (const template of templates) { const localizedLabel = getTemplateLabel(template.label);
const localizedLabel = getTemplateLabel(template.label); items.push({
items.push({ label: localizedLabel,
label: `${t.newPrefix} ${localizedLabel}`, icon: getIconComponent(template.icon, 16),
icon: getIconComponent(template.icon, 16), onClick: () => {
onClick: () => { setContextMenu(null);
setContextMenu(null); if (currentPath) {
if (currentPath) { setCreateFileDialog({
setCreateFileDialog({ parentPath: currentPath,
parentPath: currentPath, template
template });
});
}
} }
}); }
} });
} }
} }
items.push({ label: '', separator: true, onClick: () => {} });
items.push({
label: locale === 'zh' ? '在资源管理器中显示' : 'Show in Explorer',
icon: <ExternalLink size={16} />,
onClick: async () => {
if (currentPath) {
try {
await TauriAPI.showInFolder(currentPath);
} catch (error) {
console.error('Failed to show in folder:', error);
}
}
setContextMenu(null);
}
});
items.push({
label: locale === 'zh' ? '刷新' : 'Refresh',
icon: <RefreshCw size={16} />,
onClick: async () => {
if (currentPath) {
await loadAssets(currentPath);
}
setContextMenu(null);
}
});
return items; return items;
} }
@@ -1093,7 +1146,7 @@ export class ${className} {
}); });
return items; return items;
}, [currentPath, fileActionRegistry, handleAssetDoubleClick, loadAssets, locale, t.newFolder, setRenameDialog, setDeleteConfirmDialog, setContextMenu, setCreateFileDialog]); }, [currentPath, fileCreationTemplates, handleAssetDoubleClick, loadAssets, locale, t.newFolder, t.newPrefix, setRenameDialog, setDeleteConfirmDialog, setContextMenu, setCreateFileDialog]);
// Render folder tree node // Render folder tree node
const renderFolderNode = useCallback((node: FolderNode, depth: number = 0) => { const renderFolderNode = useCallback((node: FolderNode, depth: number = 0) => {

View File

@@ -129,27 +129,43 @@ export function ContextMenu({ items, position, onClose }: ContextMenuProps) {
const [submenuRect, setSubmenuRect] = useState<DOMRect | null>(null); const [submenuRect, setSubmenuRect] = useState<DOMRect | null>(null);
useEffect(() => { useEffect(() => {
if (menuRef.current) { const adjustPosition = () => {
const menu = menuRef.current; if (menuRef.current) {
const rect = menu.getBoundingClientRect(); const menu = menuRef.current;
const viewportWidth = window.innerWidth; const rect = menu.getBoundingClientRect();
const viewportHeight = window.innerHeight; const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
let x = position.x; const STATUS_BAR_HEIGHT = 28;
let y = position.y; const TITLE_BAR_HEIGHT = 32;
if (x + rect.width > viewportWidth) { let x = position.x;
x = Math.max(0, viewportWidth - rect.width - 10); let y = position.y;
}
if (y + rect.height > viewportHeight) { if (x + rect.width > viewportWidth - 10) {
y = Math.max(0, viewportHeight - rect.height - 10); x = Math.max(10, viewportWidth - rect.width - 10);
} }
if (y + rect.height > viewportHeight - STATUS_BAR_HEIGHT - 10) {
y = Math.max(TITLE_BAR_HEIGHT + 10, viewportHeight - STATUS_BAR_HEIGHT - rect.height - 10);
}
if (x < 10) {
x = 10;
}
if (y < TITLE_BAR_HEIGHT + 10) {
y = TITLE_BAR_HEIGHT + 10;
}
if (x !== position.x || y !== position.y) {
setAdjustedPosition({ x, y }); setAdjustedPosition({ x, y });
} }
} };
adjustPosition();
const rafId = requestAnimationFrame(adjustPosition);
return () => cancelAnimationFrame(rafId);
}, [position]); }, [position]);
useEffect(() => { useEffect(() => {

View File

@@ -6,7 +6,9 @@
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
padding: 4px 0; padding: 4px 0;
min-width: 200px; min-width: 200px;
z-index: var(--z-index-popover); max-height: calc(100vh - 80px);
overflow-y: auto;
z-index: 10001;
font-size: 13px; font-size: 13px;
} }

View File

@@ -1076,8 +1076,15 @@ export class PluginManager implements IService {
continue; // 核心插件始终启用 continue; // 核心插件始终启用
} }
const shouldBeEnabled = enabledPlugins.includes(id); const inConfig = enabledPlugins.includes(id);
const wasEnabled = plugin.enabled; const wasEnabled = plugin.enabled;
const isDefaultEnabled = plugin.plugin.manifest.defaultEnabled;
// 如果插件在配置中明确列出,按配置来
// 如果插件不在配置中但 defaultEnabled=true保持启用新插件不应被旧配置禁用
// If plugin is explicitly in config, follow config
// If plugin is not in config but defaultEnabled=true, keep enabled (new plugins should not be disabled by old config)
const shouldBeEnabled = inConfig || (isDefaultEnabled && !enabledPlugins.some(p => p === id));
if (shouldBeEnabled && !wasEnabled) { if (shouldBeEnabled && !wasEnabled) {
toEnable.push(id); toEnable.push(id);

View File

@@ -62,14 +62,14 @@ export class MaterialEditorModule implements IEditorModuleLoader {
private inspectorProvider?: MaterialAssetInspectorProvider; private inspectorProvider?: MaterialAssetInspectorProvider;
async install(services: ServiceContainer): Promise<void> { async install(services: ServiceContainer): Promise<void> {
// Register file creation templates // 注意:文件创建模板由 PluginManager.activatePluginEditor() 自动注册
// 不要在这里手动注册,否则会重复
// NOTE: File creation templates are auto-registered by PluginManager.activatePluginEditor()
// Do not manually register here to avoid duplicates
// Register asset creation mapping for .mat files
const fileActionRegistry = services.resolve(FileActionRegistry); const fileActionRegistry = services.resolve(FileActionRegistry);
if (fileActionRegistry) { if (fileActionRegistry) {
for (const template of this.getFileCreationTemplates()) {
fileActionRegistry.registerCreationTemplate(template);
}
// Register asset creation mapping for .mat files
fileActionRegistry.registerAssetCreationMapping({ fileActionRegistry.registerAssetCreationMapping({
extension: '.mat', extension: '.mat',
createMessage: 'material:create', createMessage: 'material:create',

View File

@@ -140,6 +140,28 @@ function evaluateScaleCurve(t: number, curveType: string): number {
} }
} }
/** 评估缩放关键帧 | Evaluate scale keys */
function evaluateScaleKeys(keys: ScaleKey[], normalizedAge: number): number {
if (keys.length === 0) return 1;
if (keys.length === 1) return keys[0].scale;
// 找到当前时间所在的两个关键帧
let startKey = keys[0];
let endKey = keys[keys.length - 1];
for (let i = 0; i < keys.length - 1; i++) {
if (normalizedAge >= keys[i].time && normalizedAge <= keys[i + 1].time) {
startKey = keys[i];
endKey = keys[i + 1];
break;
}
}
const range = endKey.time - startKey.time;
const t = range > 0 ? (normalizedAge - startKey.time) / range : 0;
return lerp(startKey.scale, endKey.scale, t);
}
/** 评估颜色渐变 | Evaluate color gradient */ /** 评估颜色渐变 | Evaluate color gradient */
function evaluateColorGradient(gradient: ColorKey[], normalizedAge: number): ColorKey { function evaluateColorGradient(gradient: ColorKey[], normalizedAge: number): ColorKey {
if (gradient.length === 0) { if (gradient.length === 0) {
@@ -235,11 +257,10 @@ function useParticlePreview(
]; ];
// 缩放曲线 | Scale curve // 缩放曲线 | Scale curve
const scaleCurveType: string = sizeModule?.enabled && sizeModule.params?.curveType const sizeEnabled = sizeModule?.enabled ?? false;
? sizeModule.params.curveType as string const scaleKeys: ScaleKey[] = sizeModule?.params?.keys
: 'linear'; ? sizeModule.params.keys as ScaleKey[]
const scaleStartMultiplier: number = (sizeModule?.params?.startMultiplier as number) ?? 1; : [{ time: 0, scale: 1 }, { time: 1, scale: data.endScale }];
const scaleEndMultiplier: number = (sizeModule?.params?.endMultiplier as number) ?? data.endScale;
// 噪声参数 | Noise parameters // 噪声参数 | Noise parameters
const noiseEnabled = noiseModule?.enabled ?? false; const noiseEnabled = noiseModule?.enabled ?? false;
@@ -532,10 +553,11 @@ function useParticlePreview(
p.alpha = p.startAlpha * color.a; p.alpha = p.startAlpha * color.a;
// 缩放曲线 | Scale curve // 缩放曲线 | Scale curve
const scaleT = evaluateScaleCurve(normalizedAge, scaleCurveType); if (sizeEnabled) {
const scaleMult = lerp(scaleStartMultiplier, scaleEndMultiplier, scaleT); const scaleMult = evaluateScaleKeys(scaleKeys, normalizedAge);
p.scaleX = p.startScaleX * scaleMult; p.scaleX = p.startScaleX * scaleMult;
p.scaleY = p.startScaleY * scaleMult; p.scaleY = p.startScaleY * scaleMult;
}
// 噪声模块 | Noise module // 噪声模块 | Noise module
if (noiseEnabled) { if (noiseEnabled) {