Skip to content

Commit 5f42521

Browse files
authored
Merge branch 'main' into feat/vue-nodes-try-it-now-banner
2 parents 95bf09f + fac8bd6 commit 5f42521

File tree

20 files changed

+604
-159
lines changed

20 files changed

+604
-159
lines changed

src/components/common/EditableText.vue

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
@keyup.enter="blurInputElement"
2222
@keyup.escape="cancelEditing"
2323
@click.stop
24+
@pointerdown.stop.capture
25+
@pointermove.stop.capture
2426
/>
2527
</div>
2628
</template>

src/components/sidebar/tabs/QueueSidebarTab.vue

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ import { useI18n } from 'vue-i18n'
104104
105105
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
106106
import VirtualGrid from '@/components/common/VirtualGrid.vue'
107+
import { isCloud } from '@/platform/distribution/types'
107108
import { useSettingStore } from '@/platform/settings/settingStore'
108109
import type { ComfyNode } from '@/platform/workflow/validation/schemas/workflowSchema'
109110
import { api } from '@/scripts/api'
@@ -206,7 +207,9 @@ const menuItems = computed<MenuItem[]>(() => {
206207
label: t('g.loadWorkflow'),
207208
icon: 'pi pi-file-export',
208209
command: () => menuTargetTask.value?.loadWorkflow(app),
209-
disabled: !menuTargetTask.value?.workflow
210+
disabled: isCloud
211+
? !menuTargetTask.value?.isHistory
212+
: !menuTargetTask.value?.workflow
210213
},
211214
{
212215
label: t('g.goToNode'),

src/core/graph/subgraph/proxyWidget.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,11 @@ function resolveLinkedWidget(
158158
const { graph, nodeId, widgetName } = overlay
159159
const n = getNodeByExecutionId(graph, nodeId)
160160
if (!n) return [undefined, undefined]
161-
return [n, n.widgets?.find((w: IBaseWidget) => w.name === widgetName)]
161+
const widget = n.widgets?.find((w: IBaseWidget) => w.name === widgetName)
162+
//Slightly hacky. Force recursive resolution of nested widgets
163+
if (widget instanceof disconnectedWidget.constructor && isProxyWidget(widget))
164+
widget.computedHeight = 20
165+
return [n, widget]
162166
}
163167

164168
function newProxyFromOverlay(subgraphNode: SubgraphNode, overlay: Overlay) {

src/extensions/core/load3d.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { nextTick } from 'vue'
33
import Load3D from '@/components/load3d/Load3D.vue'
44
import Load3DAnimation from '@/components/load3d/Load3DAnimation.vue'
55
import Load3DViewerContent from '@/components/load3d/Load3dViewerContent.vue'
6+
import { createExportMenuOptions } from '@/extensions/core/load3d/exportMenuHelper'
67
import Load3DConfiguration from '@/extensions/core/load3d/Load3DConfiguration'
78
import Load3dAnimation from '@/extensions/core/load3d/Load3dAnimation'
89
import Load3dUtils from '@/extensions/core/load3d/Load3dUtils'
@@ -297,6 +298,8 @@ useExtensionService().registerExtension({
297298
await nextTick()
298299

299300
useLoad3dService().waitForLoad3d(node, (load3d) => {
301+
node.getExtraMenuOptions = createExportMenuOptions(load3d)
302+
300303
let cameraState = node.properties['Camera Info']
301304

302305
const config = new Load3DConfiguration(load3d)
@@ -542,6 +545,8 @@ useExtensionService().registerExtension({
542545
const onExecuted = node.onExecuted
543546

544547
useLoad3dService().waitForLoad3d(node, (load3d) => {
548+
node.getExtraMenuOptions = createExportMenuOptions(load3d)
549+
545550
const config = new Load3DConfiguration(load3d)
546551

547552
const modelWidget = node.widgets?.find((w) => w.name === 'model_file')

src/extensions/core/load3d/Load3d.ts

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as THREE from 'three'
22

3-
import { LGraphNode } from '@/lib/litegraph/src/litegraph'
3+
import { LGraphNode, LiteGraph } from '@/lib/litegraph/src/litegraph'
44
import { type CustomInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
55

66
import { CameraManager } from './CameraManager'
@@ -22,6 +22,7 @@ import {
2222
type MaterialMode,
2323
type UpDirection
2424
} from './interfaces'
25+
import { app } from '@/scripts/app'
2526

2627
class Load3d {
2728
renderer: THREE.WebGLRenderer
@@ -51,6 +52,13 @@ class Load3d {
5152
targetAspectRatio: number = 1
5253
isViewerMode: boolean = false
5354

55+
// Context menu tracking
56+
private rightMouseDownX: number = 0
57+
private rightMouseDownY: number = 0
58+
private rightMouseMoved: boolean = false
59+
private readonly dragThreshold: number = 5
60+
private contextMenuAbortController: AbortController | null = null
61+
5462
constructor(
5563
container: Element | HTMLElement,
5664
options: Load3DOptions = {
@@ -164,6 +172,8 @@ class Load3d {
164172
this.STATUS_MOUSE_ON_SCENE = false
165173
this.STATUS_MOUSE_ON_VIEWER = false
166174

175+
this.initContextMenu()
176+
167177
this.handleResize()
168178
this.startAnimation()
169179

@@ -172,6 +182,65 @@ class Load3d {
172182
}, 100)
173183
}
174184

185+
/**
186+
* Initialize context menu on the Three.js canvas
187+
* Detects right-click vs right-drag to show menu only on click
188+
*/
189+
private initContextMenu(): void {
190+
const canvas = this.renderer.domElement
191+
192+
this.contextMenuAbortController = new AbortController()
193+
const { signal } = this.contextMenuAbortController
194+
195+
const mousedownHandler = (e: MouseEvent) => {
196+
if (e.button === 2) {
197+
this.rightMouseDownX = e.clientX
198+
this.rightMouseDownY = e.clientY
199+
this.rightMouseMoved = false
200+
}
201+
}
202+
203+
const mousemoveHandler = (e: MouseEvent) => {
204+
if (e.buttons === 2) {
205+
const dx = Math.abs(e.clientX - this.rightMouseDownX)
206+
const dy = Math.abs(e.clientY - this.rightMouseDownY)
207+
208+
if (dx > this.dragThreshold || dy > this.dragThreshold) {
209+
this.rightMouseMoved = true
210+
}
211+
}
212+
}
213+
214+
const contextmenuHandler = (e: MouseEvent) => {
215+
const wasDragging = this.rightMouseMoved
216+
217+
this.rightMouseMoved = false
218+
219+
if (wasDragging) {
220+
return
221+
}
222+
223+
e.preventDefault()
224+
e.stopPropagation()
225+
226+
this.showNodeContextMenu(e)
227+
}
228+
229+
canvas.addEventListener('mousedown', mousedownHandler, { signal })
230+
canvas.addEventListener('mousemove', mousemoveHandler, { signal })
231+
canvas.addEventListener('contextmenu', contextmenuHandler, { signal })
232+
}
233+
234+
private showNodeContextMenu(event: MouseEvent): void {
235+
const menuOptions = app.canvas.getNodeMenuOptions(this.node)
236+
237+
new LiteGraph.ContextMenu(menuOptions, {
238+
event,
239+
title: this.node.type,
240+
extra: this.node
241+
})
242+
}
243+
175244
getEventManager(): EventManager {
176245
return this.eventManager
177246
}
@@ -621,6 +690,11 @@ class Load3d {
621690
}
622691

623692
public remove(): void {
693+
if (this.contextMenuAbortController) {
694+
this.contextMenuAbortController.abort()
695+
this.contextMenuAbortController = null
696+
}
697+
624698
this.renderer.forceContextLoss()
625699
const canvas = this.renderer.domElement
626700
const event = new Event('webglcontextlost', {
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { t } from '@/i18n'
2+
import type { LGraphCanvas } from '@/lib/litegraph/src/LGraphCanvas'
3+
import type { IContextMenuValue } from '@/lib/litegraph/src/interfaces'
4+
import { useToastStore } from '@/platform/updates/common/toastStore'
5+
import Load3d from '@/extensions/core/load3d/Load3d'
6+
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
7+
8+
const EXPORT_FORMATS = [
9+
{ label: 'GLB', value: 'glb' },
10+
{ label: 'OBJ', value: 'obj' },
11+
{ label: 'STL', value: 'stl' }
12+
] as const
13+
14+
export function createExportMenuOptions(
15+
load3d: Load3d
16+
): (
17+
canvas: LGraphCanvas,
18+
options: (IContextMenuValue | null)[]
19+
) => (IContextMenuValue | null)[] {
20+
return function (
21+
_canvas: LGraphCanvas,
22+
options: (IContextMenuValue | null)[]
23+
): (IContextMenuValue | null)[] {
24+
options.push(null, {
25+
content: 'Save',
26+
has_submenu: true,
27+
callback: (_value, _options, event, prev_menu) => {
28+
const submenuOptions: IContextMenuValue[] = EXPORT_FORMATS.map(
29+
(format) => ({
30+
content: format.label,
31+
callback: () => {
32+
void (async () => {
33+
try {
34+
await load3d.exportModel(format.value)
35+
useToastStore().add({
36+
severity: 'success',
37+
summary: t('toastMessages.exportSuccess', {
38+
format: format.label
39+
})
40+
})
41+
} catch (error) {
42+
console.error('Export failed:', error)
43+
useToastStore().addAlert(
44+
t('toastMessages.failedToExportModel', {
45+
format: format.label
46+
})
47+
)
48+
}
49+
})()
50+
}
51+
})
52+
)
53+
54+
new LiteGraph.ContextMenu(submenuOptions, {
55+
event,
56+
parentMenu: prev_menu
57+
})
58+
}
59+
})
60+
return options
61+
}
62+
}

src/extensions/core/saveMesh.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { nextTick } from 'vue'
22

33
import Load3D from '@/components/load3d/Load3D.vue'
4+
import { createExportMenuOptions } from '@/extensions/core/load3d/exportMenuHelper'
45
import Load3DConfiguration from '@/extensions/core/load3d/Load3DConfiguration'
56
import { type CustomInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
67
import { ComponentWidgetImpl, addWidget } from '@/scripts/domWidget'
@@ -60,6 +61,10 @@ useExtensionService().registerExtension({
6061

6162
const load3d = useLoad3dService().getLoad3d(node)
6263

64+
if (load3d) {
65+
node.getExtraMenuOptions = createExportMenuOptions(load3d)
66+
}
67+
6368
const modelWidget = node.widgets?.find((w) => w.name === 'image')
6469

6570
if (load3d && modelWidget) {

0 commit comments

Comments
 (0)