Complete context menu migration for Load3D, Preview3D, and SaveGLB (#6454)

This pull request refactors how export menu options are added to
3D-related nodes, updating the API to use a new `getNodeMenuItems` hook
and simplifying the menu item creation logic. The changes improve
consistency and maintainability across extensions handling 3D nodes, and
clarify the expected return types for menu item hooks.

**Refactoring and API updates for node menu items:**

* Replaced usage of the legacy `createExportMenuOptions` function with
the new `createExportMenuItems` function in `load3d.ts`, `saveMesh.ts`,
and related imports, aligning all 3D node extensions to the new API.
[[1]](diffhunk://#diff-a5c612d9322ca4cbbeda097d13e2fa1ef017d4c3076d23fc228afee5a79a56e3L6-R12)
[[2]](diffhunk://#diff-7dede72060130d77ce5191fc86d115bd9b93311cb0438400730d8e20b2aa8e43L4-R7)
* Introduced and implemented the `getNodeMenuItems` hook in the
extension registration for `Load3D`, `Preview3D`, and `SaveGLB` nodes,
ensuring export menu items are only shown for the appropriate node
types.
[[1]](diffhunk://#diff-a5c612d9322ca4cbbeda097d13e2fa1ef017d4c3076d23fc228afee5a79a56e3R293-R302)
[[2]](diffhunk://#diff-a5c612d9322ca4cbbeda097d13e2fa1ef017d4c3076d23fc228afee5a79a56e3R521-R530)
[[3]](diffhunk://#diff-7dede72060130d77ce5191fc86d115bd9b93311cb0438400730d8e20b2aa8e43R48-R57)
* Removed assignment of `getExtraMenuOptions` to nodes, fully migrating
to the new menu item hook approach for context menus.
[[1]](diffhunk://#diff-a5c612d9322ca4cbbeda097d13e2fa1ef017d4c3076d23fc228afee5a79a56e3L301-L302)
[[2]](diffhunk://#diff-a5c612d9322ca4cbbeda097d13e2fa1ef017d4c3076d23fc228afee5a79a56e3L548-L549)
[[3]](diffhunk://#diff-7dede72060130d77ce5191fc86d115bd9b93311cb0438400730d8e20b2aa8e43L64-L67)

**Menu item creation logic simplification:**

* Refactored `createExportMenuOptions` to `createExportMenuItems` in
`exportMenuHelper.ts`, changing it to directly return an array of menu
items (with separators) instead of a function, simplifying its usage and
reducing boilerplate.
[[1]](diffhunk://#diff-42404da1a87a52d304371a13d5f021bdad837765b94d86d186abb2d99a8cb707L14-R22)
[[2]](diffhunk://#diff-42404da1a87a52d304371a13d5f021bdad837765b94d86d186abb2d99a8cb707L59-R58)

**Type and documentation improvements:**

* Updated the `ComfyExtension` interface in `comfy.ts` to clarify that
menu item hooks (`getCanvasMenuItems`, `getNodeMenuItems`) now return
arrays that may include `null` values as separators, improving type
safety and documentation.

See before and after below
<img width="1459" height="897" alt="Screenshot 2025-10-30 at 07 08 04"
src="https://github.com/user-attachments/assets/ec4464c9-f733-4b4c-87c4-bb5060ccaa68"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6454-Complete-context-menu-migration-for-Load3D-Preview3D-and-SaveGLB-29c6d73d3650819995b4c4dc1582cd86)
by [Unito](https://www.unito.io)
This commit is contained in:
Johnpaul Chiwetelu
2025-10-30 07:26:05 +01:00
committed by GitHub
parent 9e309308ed
commit c642ed5703
4 changed files with 51 additions and 28 deletions

View File

@@ -3,11 +3,13 @@ import { nextTick } from 'vue'
import Load3D from '@/components/load3d/Load3D.vue'
import Load3DAnimation from '@/components/load3d/Load3DAnimation.vue'
import Load3DViewerContent from '@/components/load3d/Load3dViewerContent.vue'
import { createExportMenuOptions } from '@/extensions/core/load3d/exportMenuHelper'
import { createExportMenuItems } from '@/extensions/core/load3d/exportMenuHelper'
import Load3DConfiguration from '@/extensions/core/load3d/Load3DConfiguration'
import Load3dAnimation from '@/extensions/core/load3d/Load3dAnimation'
import Load3dUtils from '@/extensions/core/load3d/Load3dUtils'
import { t } from '@/i18n'
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
import type { IContextMenuValue } from '@/lib/litegraph/src/interfaces'
import type { IStringWidget } from '@/lib/litegraph/src/types/widgets'
import { useToastStore } from '@/platform/updates/common/toastStore'
import { type CustomInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
@@ -288,6 +290,16 @@ useExtensionService().registerExtension({
}
},
getNodeMenuItems(node: LGraphNode): (IContextMenuValue | null)[] {
// Only show menu items for Load3D nodes
if (node.constructor.comfyClass !== 'Load3D') return []
const load3d = useLoad3dService().getLoad3d(node)
if (!load3d) return []
return createExportMenuItems(load3d)
},
async nodeCreated(node) {
if (node.constructor.comfyClass !== 'Load3D') return
@@ -298,8 +310,6 @@ useExtensionService().registerExtension({
await nextTick()
useLoad3dService().waitForLoad3d(node, (load3d) => {
node.getExtraMenuOptions = createExportMenuOptions(load3d)
let cameraState = node.properties['Camera Info']
const config = new Load3DConfiguration(load3d)
@@ -508,6 +518,16 @@ useExtensionService().registerExtension({
}
},
getNodeMenuItems(node: LGraphNode): (IContextMenuValue | null)[] {
// Only show menu items for Preview3D nodes
if (node.constructor.comfyClass !== 'Preview3D') return []
const load3d = useLoad3dService().getLoad3d(node)
if (!load3d) return []
return createExportMenuItems(load3d)
},
getCustomWidgets() {
return {
PREVIEW_3D(node) {
@@ -545,8 +565,6 @@ useExtensionService().registerExtension({
const onExecuted = node.onExecuted
useLoad3dService().waitForLoad3d(node, (load3d) => {
node.getExtraMenuOptions = createExportMenuOptions(load3d)
const config = new Load3DConfiguration(load3d)
const modelWidget = node.widgets?.find((w) => w.name === 'model_file')

View File

@@ -1,5 +1,4 @@
import { t } from '@/i18n'
import type { LGraphCanvas } from '@/lib/litegraph/src/LGraphCanvas'
import type { IContextMenuValue } from '@/lib/litegraph/src/interfaces'
import { useToastStore } from '@/platform/updates/common/toastStore'
import Load3d from '@/extensions/core/load3d/Load3d'
@@ -11,17 +10,16 @@ const EXPORT_FORMATS = [
{ label: 'STL', value: 'stl' }
] as const
export function createExportMenuOptions(
/**
* Creates export menu items for a 3D node using the new extension API.
* Returns an array of context menu items including a separator and export submenu.
*/
export function createExportMenuItems(
load3d: Load3d
): (
canvas: LGraphCanvas,
options: (IContextMenuValue | null)[]
) => (IContextMenuValue | null)[] {
return function (
_canvas: LGraphCanvas,
options: (IContextMenuValue | null)[]
): (IContextMenuValue | null)[] {
options.push(null, {
): (IContextMenuValue | null)[] {
return [
null, // Separator
{
content: 'Save',
has_submenu: true,
callback: (_value, _options, event, prev_menu) => {
@@ -56,7 +54,6 @@ export function createExportMenuOptions(
parentMenu: prev_menu
})
}
})
return options
}
}
]
}

View File

@@ -1,8 +1,10 @@
import { nextTick } from 'vue'
import Load3D from '@/components/load3d/Load3D.vue'
import { createExportMenuOptions } from '@/extensions/core/load3d/exportMenuHelper'
import { createExportMenuItems } from '@/extensions/core/load3d/exportMenuHelper'
import Load3DConfiguration from '@/extensions/core/load3d/Load3DConfiguration'
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
import type { IContextMenuValue } from '@/lib/litegraph/src/interfaces'
import { type CustomInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
import { ComponentWidgetImpl, addWidget } from '@/scripts/domWidget'
import { useExtensionService } from '@/services/extensionService'
@@ -43,6 +45,16 @@ useExtensionService().registerExtension({
}
},
getNodeMenuItems(node: LGraphNode): (IContextMenuValue | null)[] {
// Only show menu items for SaveGLB nodes
if (node.constructor.comfyClass !== 'SaveGLB') return []
const load3d = useLoad3dService().getLoad3d(node)
if (!load3d) return []
return createExportMenuItems(load3d)
},
async nodeCreated(node) {
if (node.constructor.comfyClass !== 'SaveGLB') return
@@ -61,10 +73,6 @@ useExtensionService().registerExtension({
const load3d = useLoad3dService().getLoad3d(node)
if (load3d) {
node.getExtraMenuOptions = createExportMenuOptions(load3d)
}
const modelWidget = node.widgets?.find((w) => w.name === 'image')
if (load3d && modelWidget) {

View File

@@ -140,16 +140,16 @@ export interface ComfyExtension {
/**
* Allows the extension to add context menu items to canvas right-click menus
* @param canvas The canvas instance
* @returns An array of context menu items to add
* @returns An array of context menu items to add (null values represent separators)
*/
getCanvasMenuItems?(canvas: LGraphCanvas): IContextMenuValue[]
getCanvasMenuItems?(canvas: LGraphCanvas): (IContextMenuValue | null)[]
/**
* Allows the extension to add context menu items to node right-click menus
* @param node The node being right-clicked
* @returns An array of context menu items to add
* @returns An array of context menu items to add (null values represent separators)
*/
getNodeMenuItems?(node: LGraphNode): IContextMenuValue[]
getNodeMenuItems?(node: LGraphNode): (IContextMenuValue | null)[]
/**
* Allows the extension to add additional handling to the node before it is registered with **LGraph**