mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-01 19:20:10 +00:00
Merge main (as of 10-06-2025) into rh-test (#5965)
## Summary Merges latest changes from `main` as of 10-06-2025. ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-5965-Merge-main-as-of-10-06-2025-into-rh-test-2856d73d3650812cb95fd8917278a770) by [Unito](https://www.unito.io) --------- Signed-off-by: Marcel Petrick <mail@marcelpetrick.it> Co-authored-by: filtered <176114999+webfiltered@users.noreply.github.com> Co-authored-by: Christian Byrne <cbyrne@comfy.org> Co-authored-by: github-actions <github-actions@github.com> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Alexander Brown <drjkl@comfy.org> Co-authored-by: Benjamin Lu <benceruleanlu@proton.me> Co-authored-by: Terry Jia <terryjia88@gmail.com> Co-authored-by: snomiao <snomiao@gmail.com> Co-authored-by: Simula_r <18093452+simula-r@users.noreply.github.com> Co-authored-by: Jake Schroeder <jake.schroeder@isophex.com> Co-authored-by: Comfy Org PR Bot <snomiao+comfy-pr@gmail.com> Co-authored-by: AustinMroz <4284322+AustinMroz@users.noreply.github.com> Co-authored-by: GitHub Action <action@github.com> Co-authored-by: Johnpaul Chiwetelu <49923152+Myestery@users.noreply.github.com> Co-authored-by: Marcel Petrick <mail@marcelpetrick.it> Co-authored-by: Alexander Brown <DrJKL0424@gmail.com> Co-authored-by: Benjamin Lu <benjaminlu1107@gmail.com> Co-authored-by: Alexander Piskun <13381981+bigcat88@users.noreply.github.com> Co-authored-by: Rizumu Ayaka <rizumu@ayaka.moe> Co-authored-by: JakeSchroeder <jake@axiom.co> Co-authored-by: AustinMroz <austin@comfy.org> Co-authored-by: DrJKL <DrJKL@users.noreply.github.com> Co-authored-by: ComfyUI Wiki <contact@comfyui-wiki.com>
This commit is contained in:
315
src/core/graph/subgraph/SubgraphNode.vue
Normal file
315
src/core/graph/subgraph/SubgraphNode.vue
Normal file
@@ -0,0 +1,315 @@
|
||||
<script setup lang="ts">
|
||||
import { refDebounced, watchDebounced } from '@vueuse/core'
|
||||
import {
|
||||
computed,
|
||||
customRef,
|
||||
onBeforeUnmount,
|
||||
onMounted,
|
||||
ref,
|
||||
triggerRef
|
||||
} from 'vue'
|
||||
|
||||
import SearchBox from '@/components/common/SearchBox.vue'
|
||||
import SubgraphNodeWidget from '@/core/graph/subgraph/SubgraphNodeWidget.vue'
|
||||
import {
|
||||
type WidgetItem,
|
||||
demoteWidget,
|
||||
isRecommendedWidget,
|
||||
matchesPropertyItem,
|
||||
matchesWidgetItem,
|
||||
promoteWidget,
|
||||
widgetItemToProperty
|
||||
} from '@/core/graph/subgraph/proxyWidgetUtils'
|
||||
import {
|
||||
type ProxyWidgetsProperty,
|
||||
parseProxyWidgets
|
||||
} from '@/core/schemas/proxyWidget'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { SubgraphNode } from '@/lib/litegraph/src/subgraph/SubgraphNode'
|
||||
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import { DraggableList } from '@/scripts/ui/draggableList'
|
||||
import { useLitegraphService } from '@/services/litegraphService'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
|
||||
const canvasStore = useCanvasStore()
|
||||
|
||||
const draggableList = ref<DraggableList | undefined>(undefined)
|
||||
const draggableItems = ref()
|
||||
const searchQuery = ref<string>('')
|
||||
const debouncedQuery = refDebounced(searchQuery, 200)
|
||||
const proxyWidgets = customRef<ProxyWidgetsProperty>((track, trigger) => ({
|
||||
get() {
|
||||
track()
|
||||
const node = activeNode.value
|
||||
if (!node) return []
|
||||
return parseProxyWidgets(node.properties.proxyWidgets)
|
||||
},
|
||||
set(value?: ProxyWidgetsProperty) {
|
||||
trigger()
|
||||
const node = activeNode.value
|
||||
if (!value) return
|
||||
if (!node) {
|
||||
console.error('Attempted to toggle widgets with no node selected')
|
||||
return
|
||||
}
|
||||
node.properties.proxyWidgets = value
|
||||
}
|
||||
}))
|
||||
|
||||
const activeNode = computed(() => {
|
||||
const node = canvasStore.selectedItems[0]
|
||||
if (node instanceof SubgraphNode) return node
|
||||
useDialogStore().closeDialog()
|
||||
return undefined
|
||||
})
|
||||
|
||||
const activeWidgets = computed<WidgetItem[]>({
|
||||
get() {
|
||||
const node = activeNode.value
|
||||
if (!node) return []
|
||||
return proxyWidgets.value.flatMap(([id, name]: [string, string]) => {
|
||||
const wNode = node.subgraph._nodes_by_id[id]
|
||||
if (!wNode?.widgets) return []
|
||||
const w = wNode.widgets.find((w) => w.name === name)
|
||||
if (!w) return []
|
||||
return [[wNode, w]]
|
||||
})
|
||||
},
|
||||
set(value: WidgetItem[]) {
|
||||
const node = activeNode.value
|
||||
if (!node) {
|
||||
console.error('Attempted to toggle widgets with no node selected')
|
||||
return
|
||||
}
|
||||
//map back to id/name
|
||||
const widgets: ProxyWidgetsProperty = value.map(widgetItemToProperty)
|
||||
proxyWidgets.value = widgets
|
||||
}
|
||||
})
|
||||
|
||||
const interiorWidgets = computed<WidgetItem[]>(() => {
|
||||
const node = activeNode.value
|
||||
if (!node) return []
|
||||
const { updatePreviews } = useLitegraphService()
|
||||
const interiorNodes = node.subgraph.nodes
|
||||
for (const node of interiorNodes) {
|
||||
node.updateComputedDisabled()
|
||||
updatePreviews(node)
|
||||
}
|
||||
return interiorNodes
|
||||
.flatMap(nodeWidgets)
|
||||
.filter(([_, w]: WidgetItem) => !w.computedDisabled)
|
||||
})
|
||||
|
||||
const candidateWidgets = computed<WidgetItem[]>(() => {
|
||||
const node = activeNode.value
|
||||
if (!node) return []
|
||||
const widgets = proxyWidgets.value
|
||||
return interiorWidgets.value.filter(
|
||||
(widgetItem: WidgetItem) => !widgets.some(matchesPropertyItem(widgetItem))
|
||||
)
|
||||
})
|
||||
const filteredCandidates = computed<WidgetItem[]>(() => {
|
||||
const query = debouncedQuery.value.toLowerCase()
|
||||
if (!query) return candidateWidgets.value
|
||||
return candidateWidgets.value.filter(
|
||||
([n, w]: WidgetItem) =>
|
||||
n.title.toLowerCase().includes(query) ||
|
||||
w.name.toLowerCase().includes(query)
|
||||
)
|
||||
})
|
||||
|
||||
const recommendedWidgets = computed(() => {
|
||||
const node = activeNode.value
|
||||
if (!node) return [] //Not reachable
|
||||
return filteredCandidates.value.filter(isRecommendedWidget)
|
||||
})
|
||||
|
||||
const filteredActive = computed<WidgetItem[]>(() => {
|
||||
const query = debouncedQuery.value.toLowerCase()
|
||||
if (!query) return activeWidgets.value
|
||||
return activeWidgets.value.filter(
|
||||
([n, w]: WidgetItem) =>
|
||||
n.title.toLowerCase().includes(query) ||
|
||||
w.name.toLowerCase().includes(query)
|
||||
)
|
||||
})
|
||||
|
||||
function toKey(item: WidgetItem) {
|
||||
return `${item[0].id}: ${item[1].name}`
|
||||
}
|
||||
function nodeWidgets(n: LGraphNode): WidgetItem[] {
|
||||
if (!n.widgets) return []
|
||||
return n.widgets.map((w: IBaseWidget) => [n, w])
|
||||
}
|
||||
function demote([node, widget]: WidgetItem) {
|
||||
const subgraphNode = activeNode.value
|
||||
if (!subgraphNode) return []
|
||||
demoteWidget(node, widget, [subgraphNode])
|
||||
triggerRef(proxyWidgets)
|
||||
}
|
||||
function promote([node, widget]: WidgetItem) {
|
||||
const subgraphNode = activeNode.value
|
||||
if (!subgraphNode) return []
|
||||
promoteWidget(node, widget, [subgraphNode])
|
||||
triggerRef(proxyWidgets)
|
||||
}
|
||||
function showAll() {
|
||||
const node = activeNode.value
|
||||
if (!node) return //Not reachable
|
||||
const widgets = proxyWidgets.value
|
||||
const toAdd: ProxyWidgetsProperty =
|
||||
filteredCandidates.value.map(widgetItemToProperty)
|
||||
widgets.push(...toAdd)
|
||||
proxyWidgets.value = widgets
|
||||
}
|
||||
function hideAll() {
|
||||
const node = activeNode.value
|
||||
if (!node) return //Not reachable
|
||||
//Not great from a nesting perspective, but path is cold
|
||||
//and it cleans up potential error states
|
||||
proxyWidgets.value = proxyWidgets.value.filter(
|
||||
(widgetItem) => !filteredActive.value.some(matchesWidgetItem(widgetItem))
|
||||
)
|
||||
}
|
||||
function showRecommended() {
|
||||
const node = activeNode.value
|
||||
if (!node) return //Not reachable
|
||||
const widgets = proxyWidgets.value
|
||||
const toAdd: ProxyWidgetsProperty =
|
||||
recommendedWidgets.value.map(widgetItemToProperty)
|
||||
//TODO: Add sort step here
|
||||
//Input should always be before output by default
|
||||
widgets.push(...toAdd)
|
||||
proxyWidgets.value = widgets
|
||||
}
|
||||
|
||||
function setDraggableState() {
|
||||
draggableList.value?.dispose()
|
||||
if (debouncedQuery.value || !draggableItems.value?.children?.length) return
|
||||
draggableList.value = new DraggableList(
|
||||
draggableItems.value,
|
||||
'.draggable-item'
|
||||
)
|
||||
//Original implementation plays really poorly with vue,
|
||||
//It has been modified to not add/remove elements
|
||||
draggableList.value.applyNewItemsOrder = function () {
|
||||
const reorderedItems = []
|
||||
|
||||
let oldPosition = -1
|
||||
this.getAllItems().forEach((item, index) => {
|
||||
if (item === this.draggableItem) {
|
||||
oldPosition = index
|
||||
return
|
||||
}
|
||||
if (!this.isItemToggled(item)) {
|
||||
reorderedItems[index] = item
|
||||
return
|
||||
}
|
||||
const newIndex = this.isItemAbove(item) ? index + 1 : index - 1
|
||||
reorderedItems[newIndex] = item
|
||||
})
|
||||
|
||||
for (let index = 0; index < this.getAllItems().length; index++) {
|
||||
const item = reorderedItems[index]
|
||||
if (typeof item === 'undefined') {
|
||||
reorderedItems[index] = this.draggableItem
|
||||
}
|
||||
}
|
||||
const newPosition = reorderedItems.indexOf(this.draggableItem)
|
||||
const aw = activeWidgets.value
|
||||
const [w] = aw.splice(oldPosition, 1)
|
||||
aw.splice(newPosition, 0, w)
|
||||
activeWidgets.value = aw
|
||||
}
|
||||
}
|
||||
watchDebounced(
|
||||
filteredActive,
|
||||
() => {
|
||||
setDraggableState()
|
||||
},
|
||||
{ debounce: 100 }
|
||||
)
|
||||
onMounted(() => {
|
||||
setDraggableState()
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
draggableList.value?.dispose()
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<SearchBox
|
||||
v-model:model-value="searchQuery"
|
||||
class="p-2"
|
||||
:placeholder="$t('g.search') + '...'"
|
||||
/>
|
||||
<div
|
||||
v-if="filteredActive.length"
|
||||
class="pt-1 pb-4 border-b-1 border-node-component-border"
|
||||
>
|
||||
<div class="flex py-0 px-4 justify-between">
|
||||
<div class="text-slate-100 text-[9px] font-semibold uppercase">
|
||||
{{ $t('subgraphStore.shown') }}
|
||||
</div>
|
||||
<a
|
||||
class="cursor-pointer text-right text-blue-100 text-[11px] font-normal"
|
||||
@click.stop="hideAll"
|
||||
>
|
||||
{{ $t('subgraphStore.hideAll') }}</a
|
||||
>
|
||||
</div>
|
||||
<div ref="draggableItems">
|
||||
<div
|
||||
v-for="[node, widget] in filteredActive"
|
||||
:key="toKey([node, widget])"
|
||||
class="w-full draggable-item"
|
||||
style=""
|
||||
>
|
||||
<SubgraphNodeWidget
|
||||
:node-title="node.title"
|
||||
:widget-name="widget.name"
|
||||
:is-shown="true"
|
||||
:is-draggable="!debouncedQuery"
|
||||
@toggle-visibility="demote([node, widget])"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="filteredCandidates.length" class="pt-1 pb-4">
|
||||
<div class="flex py-0 px-4 justify-between">
|
||||
<div class="text-slate-100 text-[9px] font-semibold uppercase">
|
||||
{{ $t('subgraphStore.hidden') }}
|
||||
</div>
|
||||
<a
|
||||
class="cursor-pointer text-right text-blue-100 text-[11px] font-normal"
|
||||
@click.stop="showAll"
|
||||
>
|
||||
{{ $t('subgraphStore.showAll') }}</a
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
v-for="[node, widget] in filteredCandidates"
|
||||
:key="toKey([node, widget])"
|
||||
class="w-full"
|
||||
>
|
||||
<SubgraphNodeWidget
|
||||
:node-title="node.title"
|
||||
:widget-name="widget.name"
|
||||
@toggle-visibility="promote([node, widget])"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="recommendedWidgets.length"
|
||||
class="justify-center flex py-4 border-t-1 border-node-component-border"
|
||||
>
|
||||
<Button
|
||||
size="small"
|
||||
class="rounded border-none px-3 py-0.5"
|
||||
@click.stop="showRecommended"
|
||||
>
|
||||
{{ $t('subgraphStore.showRecommended') }}
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
48
src/core/graph/subgraph/SubgraphNodeWidget.vue
Normal file
48
src/core/graph/subgraph/SubgraphNodeWidget.vue
Normal file
@@ -0,0 +1,48 @@
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
const props = defineProps<{
|
||||
nodeTitle: string
|
||||
widgetName: string
|
||||
isShown?: boolean
|
||||
isDraggable?: boolean
|
||||
}>()
|
||||
defineEmits<{
|
||||
(e: 'toggleVisibility'): void
|
||||
}>()
|
||||
|
||||
function classes() {
|
||||
return cn(
|
||||
'flex py-1 pr-4 pl-0 break-all rounded items-center gap-1',
|
||||
'bg-node-component-surface',
|
||||
props.isDraggable
|
||||
? 'drag-handle cursor-grab [.is-draggable]:cursor-grabbing'
|
||||
: ''
|
||||
)
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<div :class="classes()">
|
||||
<div
|
||||
:class="
|
||||
cn(
|
||||
'size-4 pointer-events-none',
|
||||
isDraggable ? 'icon-[lucide--grip-vertical]' : ''
|
||||
)
|
||||
"
|
||||
/>
|
||||
<div class="flex-1 pointer-events-none">
|
||||
<div class="text-slate-100 text-[10px]">{{ nodeTitle }}</div>
|
||||
<div class="text-xs">{{ widgetName }}</div>
|
||||
</div>
|
||||
<Button
|
||||
size="small"
|
||||
text
|
||||
:icon="isDraggable ? 'icon-[lucide--eye]' : 'icon-[lucide--eye-off]'"
|
||||
severity="secondary"
|
||||
@click.stop="$emit('toggleVisibility')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,13 +1,18 @@
|
||||
import { useNodeImage } from '@/composables/node/useNodeImage'
|
||||
import { demoteWidget } from '@/core/graph/subgraph/proxyWidgetUtils'
|
||||
import { parseProxyWidgets } from '@/core/schemas/proxyWidget'
|
||||
import type { LGraph, LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import type {
|
||||
LGraph,
|
||||
LGraphCanvas,
|
||||
LGraphNode
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
import { SubgraphNode } from '@/lib/litegraph/src/subgraph/SubgraphNode'
|
||||
import type { ISerialisedNode } from '@/lib/litegraph/src/types/serialisation'
|
||||
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets.ts'
|
||||
import { disconnectedWidget } from '@/lib/litegraph/src/widgets/DisconnectedWidget'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import { DOMWidgetImpl } from '@/scripts/domWidget'
|
||||
import { useLitegraphService } from '@/services/litegraphService'
|
||||
import { useDomWidgetStore } from '@/stores/domWidgetStore'
|
||||
import { useNodeOutputStore } from '@/stores/imagePreviewStore'
|
||||
import { getNodeByExecutionId } from '@/utils/graphTraversalUtil'
|
||||
|
||||
/**
|
||||
@@ -43,14 +48,33 @@ function isProxyWidget(w: IBaseWidget): w is ProxyWidget {
|
||||
return (w as { _overlay?: Overlay })?._overlay?.isProxyWidget ?? false
|
||||
}
|
||||
|
||||
export function registerProxyWidgets(canvas: LGraphCanvas) {
|
||||
//NOTE: canvasStore hasn't been initialized yet
|
||||
canvas.canvas.addEventListener<'subgraph-opened'>('subgraph-opened', (e) => {
|
||||
const { subgraph, fromNode } = e.detail
|
||||
const proxyWidgets = parseProxyWidgets(fromNode.properties.proxyWidgets)
|
||||
for (const node of subgraph.nodes) {
|
||||
for (const widget of node.widgets ?? []) {
|
||||
widget.promoted = proxyWidgets.some(
|
||||
([n, w]) => node.id == n && widget.name == w
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
SubgraphNode.prototype.onConfigure = onConfigure
|
||||
}
|
||||
|
||||
const originalOnConfigure = SubgraphNode.prototype.onConfigure
|
||||
SubgraphNode.prototype.onConfigure = function (serialisedNode) {
|
||||
const onConfigure = function (
|
||||
this: LGraphNode,
|
||||
serialisedNode: ISerialisedNode
|
||||
) {
|
||||
if (!this.isSubgraphNode())
|
||||
throw new Error("Can't add proxyWidgets to non-subgraphNode")
|
||||
|
||||
const canvasStore = useCanvasStore()
|
||||
//Must give value to proxyWidgets prior to defining or it won't serialize
|
||||
this.properties.proxyWidgets ??= '[]'
|
||||
this.properties.proxyWidgets ??= []
|
||||
let proxyWidgets = this.properties.proxyWidgets
|
||||
|
||||
originalOnConfigure?.call(this, serialisedNode)
|
||||
@@ -62,13 +86,16 @@ SubgraphNode.prototype.onConfigure = function (serialisedNode) {
|
||||
set: (property: string) => {
|
||||
const parsed = parseProxyWidgets(property)
|
||||
const { deactivateWidget, setWidget } = useDomWidgetStore()
|
||||
for (const w of this.widgets.filter((w) => isProxyWidget(w))) {
|
||||
if (w instanceof DOMWidgetImpl) deactivateWidget(w.id)
|
||||
const isActiveGraph = useCanvasStore().canvas?.graph === this.graph
|
||||
if (isActiveGraph) {
|
||||
for (const w of this.widgets.filter((w) => isProxyWidget(w))) {
|
||||
if (w instanceof DOMWidgetImpl) deactivateWidget(w.id)
|
||||
}
|
||||
}
|
||||
this.widgets = this.widgets.filter((w) => !isProxyWidget(w))
|
||||
for (const [nodeId, widgetName] of parsed) {
|
||||
const w = addProxyWidget(this, `${nodeId}`, widgetName)
|
||||
if (w instanceof DOMWidgetImpl) setWidget(w)
|
||||
if (isActiveGraph && w instanceof DOMWidgetImpl) setWidget(w)
|
||||
}
|
||||
proxyWidgets = property
|
||||
canvasStore.canvas?.setDirty(true, true)
|
||||
@@ -86,19 +113,22 @@ function addProxyWidget(
|
||||
) {
|
||||
const name = `${nodeId}: ${widgetName}`
|
||||
const overlay = {
|
||||
//items specific for proxy management
|
||||
nodeId,
|
||||
widgetName,
|
||||
graph: subgraphNode.subgraph,
|
||||
name,
|
||||
label: name,
|
||||
isProxyWidget: true,
|
||||
y: 0,
|
||||
last_y: undefined,
|
||||
width: undefined,
|
||||
computedHeight: undefined,
|
||||
widgetName,
|
||||
//Items which normally exist on widgets
|
||||
afterQueued: undefined,
|
||||
computedHeight: undefined,
|
||||
isProxyWidget: true,
|
||||
last_y: undefined,
|
||||
name,
|
||||
node: subgraphNode,
|
||||
onRemove: undefined,
|
||||
node: subgraphNode
|
||||
promoted: undefined,
|
||||
serialize: false,
|
||||
width: undefined,
|
||||
y: 0
|
||||
}
|
||||
return addProxyFromOverlay(subgraphNode, overlay)
|
||||
}
|
||||
@@ -110,23 +140,20 @@ function resolveLinkedWidget(
|
||||
if (!n) return [undefined, undefined]
|
||||
return [n, n.widgets?.find((w: IBaseWidget) => w.name === widgetName)]
|
||||
}
|
||||
|
||||
function addProxyFromOverlay(subgraphNode: SubgraphNode, overlay: Overlay) {
|
||||
const { updatePreviews } = useLitegraphService()
|
||||
let [linkedNode, linkedWidget] = resolveLinkedWidget(overlay)
|
||||
let backingWidget = linkedWidget ?? disconnectedWidget
|
||||
if (overlay.widgetName == '$$canvas-image-preview')
|
||||
if (overlay.widgetName.startsWith('$$')) {
|
||||
overlay.node = new Proxy(subgraphNode, {
|
||||
get(_t, p) {
|
||||
if (p !== 'imgs') return Reflect.get(subgraphNode, p)
|
||||
if (!linkedNode) return []
|
||||
const images =
|
||||
useNodeOutputStore().getNodeOutputs(linkedNode)?.images ?? []
|
||||
if (images !== linkedNode.images) {
|
||||
linkedNode.images = images
|
||||
useNodeImage(linkedNode).showPreview()
|
||||
}
|
||||
return linkedNode.imgs
|
||||
}
|
||||
})
|
||||
}
|
||||
/**
|
||||
* A set of handlers which define widget interaction
|
||||
* Many arguments are shared between function calls
|
||||
@@ -135,7 +162,7 @@ function addProxyFromOverlay(subgraphNode: SubgraphNode, overlay: Overlay) {
|
||||
* @param {string} property - The name of the accessed value.
|
||||
* Checked for conditional logic, but never changed
|
||||
* @param {object} receiver - The object the result is set to
|
||||
* and the vlaue used as 'this' if property is a get/set method
|
||||
* and the value used as 'this' if property is a get/set method
|
||||
* @param {unknown} value - only used on set calls. The thing being assigned
|
||||
*/
|
||||
const handler = {
|
||||
@@ -155,6 +182,12 @@ function addProxyFromOverlay(subgraphNode: SubgraphNode, overlay: Overlay) {
|
||||
let redirectedReceiver = receiver
|
||||
if (property == 'value') redirectedReceiver = backingWidget
|
||||
else if (property == 'computedHeight') {
|
||||
if (overlay.widgetName.startsWith('$$') && linkedNode) {
|
||||
updatePreviews(linkedNode)
|
||||
}
|
||||
if (linkedNode && linkedWidget?.computedDisabled) {
|
||||
demoteWidget(linkedNode, linkedWidget, [subgraphNode])
|
||||
}
|
||||
//update linkage regularly, but no more than once per frame
|
||||
;[linkedNode, linkedWidget] = resolveLinkedWidget(overlay)
|
||||
backingWidget = linkedWidget ?? disconnectedWidget
|
||||
|
||||
138
src/core/graph/subgraph/proxyWidgetUtils.ts
Normal file
138
src/core/graph/subgraph/proxyWidgetUtils.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
import {
|
||||
type ProxyWidgetsProperty,
|
||||
parseProxyWidgets
|
||||
} from '@/core/schemas/proxyWidget'
|
||||
import type {
|
||||
IContextMenuValue,
|
||||
LGraphNode
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
import type { SubgraphNode } from '@/lib/litegraph/src/subgraph/SubgraphNode'
|
||||
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets.ts'
|
||||
import { useLitegraphService } from '@/services/litegraphService'
|
||||
import { useSubgraphNavigationStore } from '@/stores/subgraphNavigationStore'
|
||||
|
||||
export type WidgetItem = [LGraphNode, IBaseWidget]
|
||||
|
||||
function getProxyWidgets(node: SubgraphNode) {
|
||||
return parseProxyWidgets(node.properties.proxyWidgets)
|
||||
}
|
||||
export function promoteWidget(
|
||||
node: LGraphNode,
|
||||
widget: IBaseWidget,
|
||||
parents: SubgraphNode[]
|
||||
) {
|
||||
for (const parent of parents) {
|
||||
const proxyWidgets = [
|
||||
...getProxyWidgets(parent),
|
||||
widgetItemToProperty([node, widget])
|
||||
]
|
||||
parent.properties.proxyWidgets = proxyWidgets
|
||||
}
|
||||
widget.promoted = true
|
||||
}
|
||||
|
||||
export function demoteWidget(
|
||||
node: LGraphNode,
|
||||
widget: IBaseWidget,
|
||||
parents: SubgraphNode[]
|
||||
) {
|
||||
for (const parent of parents) {
|
||||
const proxyWidgets = getProxyWidgets(parent).filter(
|
||||
(widgetItem) => !matchesPropertyItem([node, widget])(widgetItem)
|
||||
)
|
||||
parent.properties.proxyWidgets = proxyWidgets
|
||||
}
|
||||
widget.promoted = false
|
||||
}
|
||||
|
||||
export function matchesWidgetItem([nodeId, widgetName]: [string, string]) {
|
||||
return ([n, w]: WidgetItem) => n.id == nodeId && w.name === widgetName
|
||||
}
|
||||
export function matchesPropertyItem([n, w]: WidgetItem) {
|
||||
return ([nodeId, widgetName]: [string, string]) =>
|
||||
n.id == nodeId && w.name === widgetName
|
||||
}
|
||||
export function widgetItemToProperty([n, w]: WidgetItem): [string, string] {
|
||||
return [`${n.id}`, w.name]
|
||||
}
|
||||
|
||||
function getParentNodes(): SubgraphNode[] {
|
||||
//NOTE: support for determining parents of a subgraph is limited
|
||||
//This function will require rework to properly support linked subgraphs
|
||||
//Either by including actual parents in the navigation stack,
|
||||
//or by adding a new event for parent listeners to collect from
|
||||
const { navigationStack } = useSubgraphNavigationStore()
|
||||
const subgraph = navigationStack.at(-1)
|
||||
if (!subgraph) throw new Error("Can't promote widget when not in subgraph")
|
||||
const parentGraph = navigationStack.at(-2) ?? subgraph.rootGraph
|
||||
return parentGraph.nodes.filter(
|
||||
(node): node is SubgraphNode =>
|
||||
node.type === subgraph.id && node.isSubgraphNode()
|
||||
)
|
||||
}
|
||||
|
||||
export function addWidgetPromotionOptions(
|
||||
options: (IContextMenuValue<unknown> | null)[],
|
||||
widget: IBaseWidget,
|
||||
node: LGraphNode
|
||||
) {
|
||||
const parents = getParentNodes()
|
||||
const promotableParents = parents.filter(
|
||||
(s) => !getProxyWidgets(s).some(matchesPropertyItem([node, widget]))
|
||||
)
|
||||
if (promotableParents.length > 0)
|
||||
options.unshift({
|
||||
content: `Promote Widget: ${widget.label ?? widget.name}`,
|
||||
callback: () => {
|
||||
promoteWidget(node, widget, promotableParents)
|
||||
}
|
||||
})
|
||||
else {
|
||||
options.unshift({
|
||||
content: `Un-Promote Widget: ${widget.label ?? widget.name}`,
|
||||
callback: () => {
|
||||
demoteWidget(node, widget, parents)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
const recommendedNodes = [
|
||||
'CLIPTextEncode',
|
||||
'LoadImage',
|
||||
'SaveImage',
|
||||
'PreviewImage'
|
||||
]
|
||||
const recommendedWidgetNames = ['seed']
|
||||
export function isRecommendedWidget([node, widget]: WidgetItem) {
|
||||
return (
|
||||
!widget.computedDisabled &&
|
||||
(recommendedNodes.includes(node.type) ||
|
||||
recommendedWidgetNames.includes(widget.name))
|
||||
)
|
||||
}
|
||||
|
||||
function nodeWidgets(n: LGraphNode): WidgetItem[] {
|
||||
return n.widgets?.map((w: IBaseWidget) => [n, w]) ?? []
|
||||
}
|
||||
export function promoteRecommendedWidgets(subgraphNode: SubgraphNode) {
|
||||
const { updatePreviews } = useLitegraphService()
|
||||
const interiorNodes = subgraphNode.subgraph.nodes
|
||||
for (const node of interiorNodes) {
|
||||
node.updateComputedDisabled()
|
||||
function checkWidgets() {
|
||||
updatePreviews(node)
|
||||
const widget = node.widgets?.find((w) => w.name.startsWith('$$'))
|
||||
if (!widget) return
|
||||
const pw = getProxyWidgets(subgraphNode)
|
||||
if (pw.some(matchesPropertyItem([node, widget]))) return
|
||||
promoteWidget(node, widget, [subgraphNode])
|
||||
}
|
||||
requestAnimationFrame(() => updatePreviews(node, checkWidgets))
|
||||
}
|
||||
const filteredWidgets: WidgetItem[] = interiorNodes
|
||||
.flatMap(nodeWidgets)
|
||||
.filter(isRecommendedWidget)
|
||||
const proxyWidgets: ProxyWidgetsProperty =
|
||||
filteredWidgets.map(widgetItemToProperty)
|
||||
subgraphNode.properties.proxyWidgets = proxyWidgets
|
||||
}
|
||||
26
src/core/graph/subgraph/useSubgraphNodeDialog.ts
Normal file
26
src/core/graph/subgraph/useSubgraphNodeDialog.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import SubgraphNode from '@/core/graph/subgraph/SubgraphNode.vue'
|
||||
import { type DialogComponentProps, useDialogStore } from '@/stores/dialogStore'
|
||||
|
||||
const key = 'global-subgraph-node-config'
|
||||
|
||||
export function showSubgraphNodeDialog() {
|
||||
const dialogStore = useDialogStore()
|
||||
const dialogComponentProps: DialogComponentProps = {
|
||||
modal: false,
|
||||
position: 'topright',
|
||||
pt: {
|
||||
root: {
|
||||
class: 'bg-node-component-surface mt-22'
|
||||
},
|
||||
header: {
|
||||
class: 'h-8 text-xs ml-3'
|
||||
}
|
||||
}
|
||||
}
|
||||
dialogStore.showDialog({
|
||||
title: 'Parameters',
|
||||
key,
|
||||
component: SubgraphNode,
|
||||
dialogComponentProps
|
||||
})
|
||||
}
|
||||
@@ -4,18 +4,15 @@ import { fromZodError } from 'zod-validation-error'
|
||||
import type { NodeProperty } from '@/lib/litegraph/src/LGraphNode'
|
||||
|
||||
const proxyWidgetsPropertySchema = z.array(z.tuple([z.string(), z.string()]))
|
||||
type ProxyWidgetsProperty = z.infer<typeof proxyWidgetsPropertySchema>
|
||||
export type ProxyWidgetsProperty = z.infer<typeof proxyWidgetsPropertySchema>
|
||||
|
||||
export function parseProxyWidgets(
|
||||
property: NodeProperty | undefined
|
||||
): ProxyWidgetsProperty {
|
||||
if (typeof property !== 'string') {
|
||||
throw new Error(
|
||||
'Invalid assignment for properties.proxyWidgets:\nValue must be a string'
|
||||
)
|
||||
}
|
||||
const parsed = JSON.parse(property)
|
||||
const result = proxyWidgetsPropertySchema.safeParse(parsed)
|
||||
if (typeof property === 'string') property = JSON.parse(property)
|
||||
const result = proxyWidgetsPropertySchema.safeParse(
|
||||
typeof property === 'string' ? JSON.parse(property) : property
|
||||
)
|
||||
if (result.success) return result.data
|
||||
|
||||
const error = fromZodError(result.error)
|
||||
|
||||
Reference in New Issue
Block a user