mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-01-26 19:09:52 +00:00
add save option for 3d node on context menu (#6319)
## Summary add save option for 3d node on context menu ## Changes allow to export model from context menu, supported in preview3d/load3d/saveMesh https://github.com/user-attachments/assets/1f0f1a93-9cdb-477f-8bd3-e298c7e3892b ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-6319-add-save-option-for-3d-node-on-context-menu-2996d73d365081f7853cd2ae9c69fe4d) by [Unito](https://www.unito.io)
This commit is contained in:
@@ -3,6 +3,7 @@ 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 Load3DConfiguration from '@/extensions/core/load3d/Load3DConfiguration'
|
||||
import Load3dAnimation from '@/extensions/core/load3d/Load3dAnimation'
|
||||
import Load3dUtils from '@/extensions/core/load3d/Load3dUtils'
|
||||
@@ -297,6 +298,8 @@ useExtensionService().registerExtension({
|
||||
await nextTick()
|
||||
|
||||
useLoad3dService().waitForLoad3d(node, (load3d) => {
|
||||
node.getExtraMenuOptions = createExportMenuOptions(load3d)
|
||||
|
||||
let cameraState = node.properties['Camera Info']
|
||||
|
||||
const config = new Load3DConfiguration(load3d)
|
||||
@@ -542,6 +545,8 @@ 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')
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as THREE from 'three'
|
||||
|
||||
import { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { LGraphNode, LiteGraph } from '@/lib/litegraph/src/litegraph'
|
||||
import { type CustomInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
|
||||
import { CameraManager } from './CameraManager'
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
type MaterialMode,
|
||||
type UpDirection
|
||||
} from './interfaces'
|
||||
import { app } from '@/scripts/app'
|
||||
|
||||
class Load3d {
|
||||
renderer: THREE.WebGLRenderer
|
||||
@@ -51,6 +52,13 @@ class Load3d {
|
||||
targetAspectRatio: number = 1
|
||||
isViewerMode: boolean = false
|
||||
|
||||
// Context menu tracking
|
||||
private rightMouseDownX: number = 0
|
||||
private rightMouseDownY: number = 0
|
||||
private rightMouseMoved: boolean = false
|
||||
private readonly dragThreshold: number = 5
|
||||
private contextMenuAbortController: AbortController | null = null
|
||||
|
||||
constructor(
|
||||
container: Element | HTMLElement,
|
||||
options: Load3DOptions = {
|
||||
@@ -164,6 +172,8 @@ class Load3d {
|
||||
this.STATUS_MOUSE_ON_SCENE = false
|
||||
this.STATUS_MOUSE_ON_VIEWER = false
|
||||
|
||||
this.initContextMenu()
|
||||
|
||||
this.handleResize()
|
||||
this.startAnimation()
|
||||
|
||||
@@ -172,6 +182,65 @@ class Load3d {
|
||||
}, 100)
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize context menu on the Three.js canvas
|
||||
* Detects right-click vs right-drag to show menu only on click
|
||||
*/
|
||||
private initContextMenu(): void {
|
||||
const canvas = this.renderer.domElement
|
||||
|
||||
this.contextMenuAbortController = new AbortController()
|
||||
const { signal } = this.contextMenuAbortController
|
||||
|
||||
const mousedownHandler = (e: MouseEvent) => {
|
||||
if (e.button === 2) {
|
||||
this.rightMouseDownX = e.clientX
|
||||
this.rightMouseDownY = e.clientY
|
||||
this.rightMouseMoved = false
|
||||
}
|
||||
}
|
||||
|
||||
const mousemoveHandler = (e: MouseEvent) => {
|
||||
if (e.buttons === 2) {
|
||||
const dx = Math.abs(e.clientX - this.rightMouseDownX)
|
||||
const dy = Math.abs(e.clientY - this.rightMouseDownY)
|
||||
|
||||
if (dx > this.dragThreshold || dy > this.dragThreshold) {
|
||||
this.rightMouseMoved = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const contextmenuHandler = (e: MouseEvent) => {
|
||||
const wasDragging = this.rightMouseMoved
|
||||
|
||||
this.rightMouseMoved = false
|
||||
|
||||
if (wasDragging) {
|
||||
return
|
||||
}
|
||||
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
this.showNodeContextMenu(e)
|
||||
}
|
||||
|
||||
canvas.addEventListener('mousedown', mousedownHandler, { signal })
|
||||
canvas.addEventListener('mousemove', mousemoveHandler, { signal })
|
||||
canvas.addEventListener('contextmenu', contextmenuHandler, { signal })
|
||||
}
|
||||
|
||||
private showNodeContextMenu(event: MouseEvent): void {
|
||||
const menuOptions = app.canvas.getNodeMenuOptions(this.node)
|
||||
|
||||
new LiteGraph.ContextMenu(menuOptions, {
|
||||
event,
|
||||
title: this.node.type,
|
||||
extra: this.node
|
||||
})
|
||||
}
|
||||
|
||||
getEventManager(): EventManager {
|
||||
return this.eventManager
|
||||
}
|
||||
@@ -621,6 +690,11 @@ class Load3d {
|
||||
}
|
||||
|
||||
public remove(): void {
|
||||
if (this.contextMenuAbortController) {
|
||||
this.contextMenuAbortController.abort()
|
||||
this.contextMenuAbortController = null
|
||||
}
|
||||
|
||||
this.renderer.forceContextLoss()
|
||||
const canvas = this.renderer.domElement
|
||||
const event = new Event('webglcontextlost', {
|
||||
|
||||
62
src/extensions/core/load3d/exportMenuHelper.ts
Normal file
62
src/extensions/core/load3d/exportMenuHelper.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
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'
|
||||
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
|
||||
|
||||
const EXPORT_FORMATS = [
|
||||
{ label: 'GLB', value: 'glb' },
|
||||
{ label: 'OBJ', value: 'obj' },
|
||||
{ label: 'STL', value: 'stl' }
|
||||
] as const
|
||||
|
||||
export function createExportMenuOptions(
|
||||
load3d: Load3d
|
||||
): (
|
||||
canvas: LGraphCanvas,
|
||||
options: (IContextMenuValue | null)[]
|
||||
) => (IContextMenuValue | null)[] {
|
||||
return function (
|
||||
_canvas: LGraphCanvas,
|
||||
options: (IContextMenuValue | null)[]
|
||||
): (IContextMenuValue | null)[] {
|
||||
options.push(null, {
|
||||
content: 'Save',
|
||||
has_submenu: true,
|
||||
callback: (_value, _options, event, prev_menu) => {
|
||||
const submenuOptions: IContextMenuValue[] = EXPORT_FORMATS.map(
|
||||
(format) => ({
|
||||
content: format.label,
|
||||
callback: () => {
|
||||
void (async () => {
|
||||
try {
|
||||
await load3d.exportModel(format.value)
|
||||
useToastStore().add({
|
||||
severity: 'success',
|
||||
summary: t('toastMessages.exportSuccess', {
|
||||
format: format.label
|
||||
})
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Export failed:', error)
|
||||
useToastStore().addAlert(
|
||||
t('toastMessages.failedToExportModel', {
|
||||
format: format.label
|
||||
})
|
||||
)
|
||||
}
|
||||
})()
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
new LiteGraph.ContextMenu(submenuOptions, {
|
||||
event,
|
||||
parentMenu: prev_menu
|
||||
})
|
||||
}
|
||||
})
|
||||
return options
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { nextTick } from 'vue'
|
||||
|
||||
import Load3D from '@/components/load3d/Load3D.vue'
|
||||
import { createExportMenuOptions } from '@/extensions/core/load3d/exportMenuHelper'
|
||||
import Load3DConfiguration from '@/extensions/core/load3d/Load3DConfiguration'
|
||||
import { type CustomInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import { ComponentWidgetImpl, addWidget } from '@/scripts/domWidget'
|
||||
@@ -60,6 +61,10 @@ 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) {
|
||||
|
||||
@@ -1472,6 +1472,7 @@
|
||||
"failedToApplyTexture": "Failed to apply texture",
|
||||
"no3dSceneToExport": "No 3D scene to export",
|
||||
"failedToExportModel": "Failed to export model as {format}",
|
||||
"exportSuccess": "Successfully exported model as {format}",
|
||||
"fileLoadError": "Unable to find workflow in {fileName}",
|
||||
"dropFileError": "Unable to process dropped item: {error}",
|
||||
"interrupted": "Execution has been interrupted",
|
||||
|
||||
Reference in New Issue
Block a user