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;
} | 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 = {
en: {
favorites: 'Favorites',
@@ -844,7 +872,6 @@ export class ${className} {
const items: ContextMenuItem[] = [];
if (!asset) {
// Background context menu
items.push({
label: t.newFolder,
icon: <FolderClosed size={16} />,
@@ -861,14 +888,13 @@ export class ${className} {
}
});
if (fileActionRegistry) {
const templates = fileActionRegistry.getCreationTemplates();
if (templates.length > 0) {
if (fileCreationTemplates.length > 0) {
items.push({ label: '', separator: true, onClick: () => {} });
for (const template of templates) {
for (const template of fileCreationTemplates) {
const localizedLabel = getTemplateLabel(template.label);
items.push({
label: `${t.newPrefix} ${localizedLabel}`,
label: localizedLabel,
icon: getIconComponent(template.icon, 16),
onClick: () => {
setContextMenu(null);
@@ -882,7 +908,34 @@ export class ${className} {
});
}
}
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;
}
@@ -1093,7 +1146,7 @@ export class ${className} {
});
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
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);
useEffect(() => {
const adjustPosition = () => {
if (menuRef.current) {
const menu = menuRef.current;
const rect = menu.getBoundingClientRect();
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
const STATUS_BAR_HEIGHT = 28;
const TITLE_BAR_HEIGHT = 32;
let x = position.x;
let y = position.y;
if (x + rect.width > viewportWidth) {
x = Math.max(0, viewportWidth - rect.width - 10);
if (x + rect.width > viewportWidth - 10) {
x = Math.max(10, viewportWidth - rect.width - 10);
}
if (y + rect.height > viewportHeight) {
y = Math.max(0, viewportHeight - rect.height - 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 });
}
}
};
adjustPosition();
const rafId = requestAnimationFrame(adjustPosition);
return () => cancelAnimationFrame(rafId);
}, [position]);
useEffect(() => {

View File

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

View File

@@ -1076,8 +1076,15 @@ export class PluginManager implements IService {
continue; // 核心插件始终启用
}
const shouldBeEnabled = enabledPlugins.includes(id);
const inConfig = enabledPlugins.includes(id);
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) {
toEnable.push(id);

View File

@@ -62,14 +62,14 @@ export class MaterialEditorModule implements IEditorModuleLoader {
private inspectorProvider?: MaterialAssetInspectorProvider;
async install(services: ServiceContainer): Promise<void> {
// Register file creation templates
const fileActionRegistry = services.resolve(FileActionRegistry);
if (fileActionRegistry) {
for (const template of this.getFileCreationTemplates()) {
fileActionRegistry.registerCreationTemplate(template);
}
// 注意:文件创建模板由 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);
if (fileActionRegistry) {
fileActionRegistry.registerAssetCreationMapping({
extension: '.mat',
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 */
function evaluateColorGradient(gradient: ColorKey[], normalizedAge: number): ColorKey {
if (gradient.length === 0) {
@@ -235,11 +257,10 @@ function useParticlePreview(
];
// 缩放曲线 | Scale curve
const scaleCurveType: string = sizeModule?.enabled && sizeModule.params?.curveType
? sizeModule.params.curveType as string
: 'linear';
const scaleStartMultiplier: number = (sizeModule?.params?.startMultiplier as number) ?? 1;
const scaleEndMultiplier: number = (sizeModule?.params?.endMultiplier as number) ?? data.endScale;
const sizeEnabled = sizeModule?.enabled ?? false;
const scaleKeys: ScaleKey[] = sizeModule?.params?.keys
? sizeModule.params.keys as ScaleKey[]
: [{ time: 0, scale: 1 }, { time: 1, scale: data.endScale }];
// 噪声参数 | Noise parameters
const noiseEnabled = noiseModule?.enabled ?? false;
@@ -532,10 +553,11 @@ function useParticlePreview(
p.alpha = p.startAlpha * color.a;
// 缩放曲线 | Scale curve
const scaleT = evaluateScaleCurve(normalizedAge, scaleCurveType);
const scaleMult = lerp(scaleStartMultiplier, scaleEndMultiplier, scaleT);
if (sizeEnabled) {
const scaleMult = evaluateScaleKeys(scaleKeys, normalizedAge);
p.scaleX = p.startScaleX * scaleMult;
p.scaleY = p.startScaleY * scaleMult;
}
// 噪声模块 | Noise module
if (noiseEnabled) {