fix(editor): 修复右键菜单和粒子编辑器问题 (#286)
- 修复右键菜单被状态栏遮挡的问题 - 修复右键菜单边界检测,考虑标题栏和状态栏高度 - 调整右键菜单结构:新建文件夹 → 资源类型 → 工具操作 - 修复 Particle 插件默认未启用的问题(defaultEnabled 的新插件不被旧配置禁用) - 修复 SizeOverLifetime 模块在预览中无效果的问题 - 移除 MaterialEditorModule 中的重复模板注册
This commit is contained in:
@@ -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) => {
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user