mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-01 18:19:09 +00:00
Compare commits
74 Commits
fix/load-a
...
austin/wid
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9e8a426c00 | ||
|
|
cc7c22a428 | ||
|
|
94f6778285 | ||
|
|
e91bbf030b | ||
|
|
44d57ce041 | ||
|
|
7c20f37973 | ||
|
|
e1971a2595 | ||
|
|
992e4a7f8a | ||
|
|
92f2578357 | ||
|
|
aeafee4e12 | ||
|
|
040c6e1691 | ||
|
|
f1c2d0ef6a | ||
|
|
0de4351913 | ||
|
|
3efb76b765 | ||
|
|
a32fc7a667 | ||
|
|
de399e2e6c | ||
|
|
768ffb2c9f | ||
|
|
91a6f9d762 | ||
|
|
4ba1e5e485 | ||
|
|
31a40e9769 | ||
|
|
dd58b272b8 | ||
|
|
e67bd12168 | ||
|
|
ebe92ed884 | ||
|
|
e902afc828 | ||
|
|
3d279716a3 | ||
|
|
b00855be3e | ||
|
|
915870d47c | ||
|
|
0e8679798b | ||
|
|
f63a76e41a | ||
|
|
3e677e3dbe | ||
|
|
02bf937741 | ||
|
|
784ebfe331 | ||
|
|
26a9527d23 | ||
|
|
b287bb645b | ||
|
|
ee5903877a | ||
|
|
1d7546f9bf | ||
|
|
dbb9a9acd4 | ||
|
|
dcc056754d | ||
|
|
4a7bc4aadf | ||
|
|
3187694a31 | ||
|
|
9b6611dd20 | ||
|
|
ccc8d6e441 | ||
|
|
166cdc385c | ||
|
|
7fd4fc556a | ||
|
|
ed2cc26cff | ||
|
|
073175a719 | ||
|
|
ec06e28af7 | ||
|
|
5d4c4ef63e | ||
|
|
eb85406d70 | ||
|
|
e820515db4 | ||
|
|
b9dcb813d8 | ||
|
|
1e72ed8a78 | ||
|
|
4016929070 | ||
|
|
e0eb699a93 | ||
|
|
68bb29bb17 | ||
|
|
bb25301e36 | ||
|
|
51d68654f1 | ||
|
|
ec09d9b365 | ||
|
|
2212151686 | ||
|
|
eab71667b6 | ||
|
|
b424c62a99 | ||
|
|
4150b1ccb4 | ||
|
|
985c0beb8e | ||
|
|
9b18125d16 | ||
|
|
1cca658351 | ||
|
|
e4c6514d9d | ||
|
|
e6aeaf4112 | ||
|
|
11978b6b71 | ||
|
|
703dc8fd4c | ||
|
|
ca7698dae5 | ||
|
|
b505bd9a80 | ||
|
|
d367f901a0 | ||
|
|
7bd6fa45cf | ||
|
|
5be68c03fc |
@@ -153,6 +153,7 @@
|
||||
"vue": "^3.5.13",
|
||||
"vue-i18n": "^9.14.3",
|
||||
"vue-router": "^4.4.3",
|
||||
"vuedraggable": "^4.1.0",
|
||||
"vuefire": "^3.2.1",
|
||||
"yjs": "^13.6.27",
|
||||
"zod": "^3.23.8",
|
||||
|
||||
18
pnpm-lock.yaml
generated
18
pnpm-lock.yaml
generated
@@ -155,6 +155,9 @@ importers:
|
||||
vue-router:
|
||||
specifier: ^4.4.3
|
||||
version: 4.4.3(vue@3.5.13(typescript@5.9.2))
|
||||
vuedraggable:
|
||||
specifier: ^4.1.0
|
||||
version: 4.1.0(vue@3.5.13(typescript@5.9.2))
|
||||
vuefire:
|
||||
specifier: ^3.2.1
|
||||
version: 3.2.1(consola@3.4.2)(firebase@11.6.0)(vue@3.5.13(typescript@5.9.2))
|
||||
@@ -5752,6 +5755,9 @@ packages:
|
||||
resolution: {integrity: sha512-rInDH6lCNiEyn3+hH8KVGFdbjc099j47+OSgbMrfDYX1CmXLfdKd7qi6IfcWj2wFxvSVkuI46M+wPGYfEOEj6g==}
|
||||
engines: {node: '>= 18'}
|
||||
|
||||
sortablejs@1.14.0:
|
||||
resolution: {integrity: sha512-pBXvQCs5/33fdN1/39pPL0NZF20LeRbLQ5jtnheIPN9JQAaufGjKdWduZn4U7wCtVuzKhmRkI0DFYHYRbB2H1w==}
|
||||
|
||||
source-map-js@1.2.1:
|
||||
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -6388,6 +6394,11 @@ packages:
|
||||
typescript:
|
||||
optional: true
|
||||
|
||||
vuedraggable@4.1.0:
|
||||
resolution: {integrity: sha512-FU5HCWBmsf20GpP3eudURW3WdWTKIbEIQxh9/8GE806hydR9qZqRRxRE3RjqX7PkuLuMQG/A7n3cfj9rCEchww==}
|
||||
peerDependencies:
|
||||
vue: ^3.0.1
|
||||
|
||||
vuefire@3.2.1:
|
||||
resolution: {integrity: sha512-APj/iFdEec9kO71Lsiv/7opo9xL0D43l7cjwh84rJ5WMzrmpi9z774zzN+PPhBpD6bXyueLcfg0VlOUhI9/jUA==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -12937,6 +12948,8 @@ snapshots:
|
||||
|
||||
smol-toml@1.4.2: {}
|
||||
|
||||
sortablejs@1.14.0: {}
|
||||
|
||||
source-map-js@1.2.1: {}
|
||||
|
||||
source-map-support@0.5.19:
|
||||
@@ -13624,6 +13637,11 @@ snapshots:
|
||||
optionalDependencies:
|
||||
typescript: 5.9.2
|
||||
|
||||
vuedraggable@4.1.0(vue@3.5.13(typescript@5.9.2)):
|
||||
dependencies:
|
||||
sortablejs: 1.14.0
|
||||
vue: 3.5.13(typescript@5.9.2)
|
||||
|
||||
vuefire@3.2.1(consola@3.4.2)(firebase@11.6.0)(vue@3.5.13(typescript@5.9.2)):
|
||||
dependencies:
|
||||
vue: 3.5.13(typescript@5.9.2)
|
||||
|
||||
@@ -22,7 +22,8 @@
|
||||
<ColorPickerButton v-if="showColorPicker" />
|
||||
<FrameNodes v-if="showFrameNodes" />
|
||||
<ConvertToSubgraphButton v-if="showConvertToSubgraph" />
|
||||
<PublishSubgraphButton v-if="showPublishSubgraph" />
|
||||
<ConfigureSubgraph v-if="showSubgraphButtons" />
|
||||
<PublishSubgraphButton v-if="showSubgraphButtons" />
|
||||
<MaskEditorButton v-if="showMaskEditor" />
|
||||
<VerticalDivider
|
||||
v-if="showAnyPrimaryActions && showAnyControlActions"
|
||||
@@ -50,6 +51,7 @@ import { computed, ref } from 'vue'
|
||||
|
||||
import BypassButton from '@/components/graph/selectionToolbox/BypassButton.vue'
|
||||
import ColorPickerButton from '@/components/graph/selectionToolbox/ColorPickerButton.vue'
|
||||
import ConfigureSubgraph from '@/components/graph/selectionToolbox/ConfigureSubgraph.vue'
|
||||
import ConvertToSubgraphButton from '@/components/graph/selectionToolbox/ConvertToSubgraphButton.vue'
|
||||
import DeleteButton from '@/components/graph/selectionToolbox/DeleteButton.vue'
|
||||
import ExecuteButton from '@/components/graph/selectionToolbox/ExecuteButton.vue'
|
||||
@@ -112,7 +114,7 @@ const showInfoButton = computed(() => !!nodeDef.value)
|
||||
const showColorPicker = computed(() => hasAnySelection.value)
|
||||
const showConvertToSubgraph = computed(() => hasAnySelection.value)
|
||||
const showFrameNodes = computed(() => hasMultipleSelection.value)
|
||||
const showPublishSubgraph = computed(() => isSingleSubgraph.value)
|
||||
const showSubgraphButtons = computed(() => isSingleSubgraph.value)
|
||||
|
||||
const showBypass = computed(
|
||||
() =>
|
||||
@@ -130,7 +132,7 @@ const showAnyPrimaryActions = computed(
|
||||
showColorPicker.value ||
|
||||
showConvertToSubgraph.value ||
|
||||
showFrameNodes.value ||
|
||||
showPublishSubgraph.value
|
||||
showSubgraphButtons.value
|
||||
)
|
||||
|
||||
const showAnyControlActions = computed(() => showBypass.value)
|
||||
|
||||
24
src/components/graph/selectionToolbox/ConfigureSubgraph.vue
Normal file
24
src/components/graph/selectionToolbox/ConfigureSubgraph.vue
Normal file
@@ -0,0 +1,24 @@
|
||||
<template>
|
||||
<Button
|
||||
v-tooltip.top="{
|
||||
value: t('Edit Subgraph Widgets'),
|
||||
showDelay: 1000
|
||||
}"
|
||||
severity="secondary"
|
||||
text
|
||||
@click="showSubgraphNodeDialog"
|
||||
>
|
||||
<template #icon>
|
||||
<i-lucide:settings2 />
|
||||
</template>
|
||||
</Button>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { showSubgraphNodeDialog } from '@/core/graph/subgraph/useSubgraphNodeDialog'
|
||||
|
||||
const { t } = useI18n()
|
||||
</script>
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { useImagePreviewWidget } from '@/renderer/extensions/vueNodes/widgets/composables/useImagePreviewWidget'
|
||||
|
||||
const CANVAS_IMAGE_PREVIEW_WIDGET = '$$canvas-image-preview'
|
||||
export const CANVAS_IMAGE_PREVIEW_WIDGET = '$$canvas-image-preview'
|
||||
|
||||
/**
|
||||
* Composable for handling canvas image previews in nodes
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
DEFAULT_DARK_COLOR_PALETTE,
|
||||
DEFAULT_LIGHT_COLOR_PALETTE
|
||||
} from '@/constants/coreColorPalettes'
|
||||
import { promoteRecommendedWidgets } from '@/core/graph/subgraph/proxyWidgetUtils'
|
||||
import { t } from '@/i18n'
|
||||
import {
|
||||
LGraphEventMode,
|
||||
@@ -867,6 +868,7 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
|
||||
const { node } = res
|
||||
canvas.select(node)
|
||||
promoteRecommendedWidgets(node)
|
||||
canvasStore.updateSelectedItems()
|
||||
}
|
||||
},
|
||||
|
||||
288
src/core/graph/subgraph/SubgraphNode.vue
Normal file
288
src/core/graph/subgraph/SubgraphNode.vue
Normal file
@@ -0,0 +1,288 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import draggable from 'vuedraggable'
|
||||
|
||||
import SearchBox from '@/components/common/SearchBox.vue'
|
||||
import SubgraphNodeWidget from '@/core/graph/subgraph/SubgraphNodeWidget.vue'
|
||||
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 { useDialogStore } from '@/stores/dialogStore'
|
||||
|
||||
type WidgetItem = [LGraphNode, IBaseWidget]
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const canvasStore = useCanvasStore()
|
||||
|
||||
const searchQuery = ref<string>('')
|
||||
|
||||
const triggerUpdate = ref(0)
|
||||
|
||||
function toKey(item: WidgetItem) {
|
||||
return `${item[0].id}: ${item[1].name}`
|
||||
}
|
||||
|
||||
const activeNode = computed(() => {
|
||||
const node = canvasStore.selectedItems[0]
|
||||
if (node instanceof SubgraphNode) return node
|
||||
useDialogStore().closeDialog()
|
||||
return undefined
|
||||
})
|
||||
|
||||
const activeWidgets = computed<WidgetItem[]>({
|
||||
get() {
|
||||
if (triggerUpdate.value < 0) console.log('unreachable')
|
||||
const node = activeNode.value
|
||||
if (!node) return []
|
||||
const pw = parseProxyWidgets(node.properties.proxyWidgets)
|
||||
return pw.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)
|
||||
throw new Error('Attempted to toggle widgets with no node selected')
|
||||
//map back to id/name
|
||||
const pw: ProxyWidgetsProperty = value.map(([node, widget]) => [
|
||||
`${node.id}`,
|
||||
widget.name
|
||||
])
|
||||
node.properties.proxyWidgets = JSON.stringify(pw)
|
||||
//force trigger an update
|
||||
triggerUpdate.value++
|
||||
}
|
||||
})
|
||||
function toggleVisibility(
|
||||
nodeId: string,
|
||||
widgetName: string,
|
||||
isShown: boolean
|
||||
) {
|
||||
const node = activeNode.value
|
||||
if (!node)
|
||||
throw new Error('Attempted to toggle widgets with no node selected')
|
||||
if (!isShown) {
|
||||
const proxyWidgets = parseProxyWidgets(node.properties.proxyWidgets)
|
||||
proxyWidgets.push([nodeId, widgetName])
|
||||
node.properties.proxyWidgets = JSON.stringify(proxyWidgets)
|
||||
} else {
|
||||
let pw = parseProxyWidgets(node.properties.proxyWidgets)
|
||||
pw = pw.filter(
|
||||
(p: [string, string]) => p[1] !== widgetName || p[0] !== nodeId
|
||||
)
|
||||
node.properties.proxyWidgets = JSON.stringify(pw)
|
||||
}
|
||||
triggerUpdate.value++
|
||||
}
|
||||
|
||||
function nodeWidgets(n: LGraphNode): WidgetItem[] {
|
||||
if (!n.widgets) return []
|
||||
return n.widgets.map((w: IBaseWidget) => [n, w])
|
||||
}
|
||||
|
||||
const candidateWidgets = computed<WidgetItem[]>(() => {
|
||||
const node = activeNode.value
|
||||
if (!node) return []
|
||||
if (triggerUpdate.value < 0) console.log('unreachable')
|
||||
const pw = parseProxyWidgets(node.properties.proxyWidgets)
|
||||
const interiorNodes = node.subgraph.nodes
|
||||
//node.widgets ??= []
|
||||
const allWidgets: WidgetItem[] = interiorNodes.flatMap(nodeWidgets)
|
||||
const filteredWidgets = allWidgets
|
||||
//widget has connected link. Should not be displayed
|
||||
.filter(([_, w]: WidgetItem) => !w.computedDisabled)
|
||||
.filter(
|
||||
([n, w]: WidgetItem) =>
|
||||
!pw.some(([pn, pw]: [string, string]) => n.id == pn && w.name == pw)
|
||||
)
|
||||
return filteredWidgets
|
||||
})
|
||||
const filteredCandidates = computed<WidgetItem[]>(() => {
|
||||
const query = searchQuery.value.toLowerCase()
|
||||
if (!query) return candidateWidgets.value
|
||||
return candidateWidgets.value.filter(
|
||||
([n, w]: WidgetItem) =>
|
||||
n.title.toLowerCase().includes(query) ||
|
||||
w.name.toLowerCase().includes(query)
|
||||
)
|
||||
})
|
||||
function showAll() {
|
||||
const node = activeNode.value
|
||||
if (!node) return //Not reachable
|
||||
const pw = parseProxyWidgets(node.properties.proxyWidgets)
|
||||
const toAdd: ProxyWidgetsProperty = filteredCandidates.value.map(
|
||||
([n, w]: WidgetItem) => [`${n.id}`, w.name]
|
||||
)
|
||||
pw.push(...toAdd)
|
||||
node.properties.proxyWidgets = JSON.stringify(pw)
|
||||
triggerUpdate.value++
|
||||
}
|
||||
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
|
||||
const toKeep: ProxyWidgetsProperty = parseProxyWidgets(
|
||||
node.properties.proxyWidgets
|
||||
).filter(
|
||||
([nodeId, widgetName]) =>
|
||||
!filteredActive.value.some(
|
||||
([n, w]: WidgetItem) => n.id == nodeId && w.name === widgetName
|
||||
)
|
||||
)
|
||||
node.properties.proxyWidgets = JSON.stringify(toKeep)
|
||||
triggerUpdate.value++
|
||||
}
|
||||
const recommendedNodes = [
|
||||
'CLIPTextEncode',
|
||||
'LoadImage',
|
||||
'SaveImage',
|
||||
'PreviewImage'
|
||||
]
|
||||
const recommendedWidgetNames = ['seed']
|
||||
const recommendedWidgets = computed(() => {
|
||||
const node = activeNode.value
|
||||
if (!node) return [] //Not reachable
|
||||
return filteredCandidates.value.filter(
|
||||
([node, widget]: WidgetItem) =>
|
||||
recommendedNodes.includes(node.type) ||
|
||||
recommendedWidgetNames.includes(widget.name)
|
||||
)
|
||||
})
|
||||
function showRecommended() {
|
||||
const node = activeNode.value
|
||||
if (!node) return //Not reachable
|
||||
const pw = parseProxyWidgets(node.properties.proxyWidgets)
|
||||
const toAdd: ProxyWidgetsProperty = recommendedWidgets.value.map(
|
||||
([n, w]: WidgetItem) => [`${n.id}`, w.name]
|
||||
)
|
||||
//TODO: Add sort step here
|
||||
//Input should always be before output by default
|
||||
pw.push(...toAdd)
|
||||
node.properties.proxyWidgets = JSON.stringify(pw)
|
||||
triggerUpdate.value++
|
||||
}
|
||||
|
||||
const filteredActive = computed<WidgetItem[]>(() => {
|
||||
const query = searchQuery.value.toLowerCase()
|
||||
if (!query) return activeWidgets.value
|
||||
return activeWidgets.value.filter(
|
||||
([n, w]: WidgetItem) =>
|
||||
n.title.toLowerCase().includes(query) ||
|
||||
w.name.toLowerCase().includes(query)
|
||||
)
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<SearchBox
|
||||
v-model:model-value="searchQuery"
|
||||
class="model-lib-search-box p-2 2xl:p-4"
|
||||
:placeholder="$t('g.search') + '...'"
|
||||
/>
|
||||
<div v-if="filteredActive.length" class="widgets-section">
|
||||
<div class="widgets-section-header">
|
||||
<div>{{ t('subgraphStore.shown') }}</div>
|
||||
<a @click.stop="hideAll"> {{ t('subgraphStore.hideAll') }}</a>
|
||||
</div>
|
||||
<div v-if="searchQuery" class="w-full">
|
||||
<div
|
||||
v-for="element in filteredActive"
|
||||
:key="toKey(element)"
|
||||
class="w-full"
|
||||
>
|
||||
<SubgraphNodeWidget
|
||||
:node-id="`${element[0].id}`"
|
||||
:node-title="element[0].title"
|
||||
:widget-name="element[1].name"
|
||||
:toggle-visibility="toggleVisibility"
|
||||
:is-shown="true"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<draggable
|
||||
v-else
|
||||
v-model="activeWidgets"
|
||||
group="enabledWidgets"
|
||||
class="w-full cursor-grab"
|
||||
chosen-class="cursor-grabbing"
|
||||
drag-class="cursor-grabbing"
|
||||
:animation="100"
|
||||
item-key="id"
|
||||
>
|
||||
<template #item="{ element }">
|
||||
<SubgraphNodeWidget
|
||||
:node-id="`${element[0].id}`"
|
||||
:node-title="element[0].title"
|
||||
:widget-name="element[1].name"
|
||||
:is-shown="true"
|
||||
:toggle-visibility="toggleVisibility"
|
||||
:is-draggable="true"
|
||||
/>
|
||||
</template>
|
||||
</draggable>
|
||||
</div>
|
||||
<div v-if="filteredCandidates.length" class="widgets-section">
|
||||
<div class="widgets-section-header">
|
||||
<div>{{ t('subgraphStore.hidden') }}</div>
|
||||
<a @click.stop="showAll"> {{ t('subgraphStore.showAll') }}</a>
|
||||
</div>
|
||||
<div
|
||||
v-for="element in filteredCandidates"
|
||||
:key="toKey(element)"
|
||||
class="w-full"
|
||||
>
|
||||
<SubgraphNodeWidget
|
||||
:node-id="`${element[0].id}`"
|
||||
:node-title="element[0].title"
|
||||
:widget-name="element[1].name"
|
||||
:toggle-visibility="toggleVisibility"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="recommendedWidgets.length" class="justify-center flex py-4">
|
||||
<Button size="small" @click.stop="showRecommended">
|
||||
{{ t('subgraphStore.showRecommended') }}
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
<style scoped>
|
||||
.widgets-section-header {
|
||||
display: flex;
|
||||
padding: 0 16px;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.widgets-section-header div {
|
||||
color: var(--color-slate-100, #9c9eab);
|
||||
/* body-text-badge */
|
||||
font-family: Inter;
|
||||
font-size: 9px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
a {
|
||||
cursor: pointer;
|
||||
color: var(--color-blue-100, #0b8ce9);
|
||||
text-align: right;
|
||||
|
||||
/* body-text-caption */
|
||||
font-family: Inter;
|
||||
font-size: 11px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.widgets-section {
|
||||
padding: 4px 0 16px 0;
|
||||
border-bottom: 1px solid var(--color-node-divider, #2e3037);
|
||||
}
|
||||
</style>
|
||||
65
src/core/graph/subgraph/SubgraphNodeWidget.vue
Normal file
65
src/core/graph/subgraph/SubgraphNodeWidget.vue
Normal file
@@ -0,0 +1,65 @@
|
||||
<script setup lang="ts">
|
||||
import Button from 'primevue/button'
|
||||
|
||||
const props = defineProps<{
|
||||
nodeId: string
|
||||
nodeTitle: string
|
||||
widgetName: string
|
||||
isShown?: boolean
|
||||
isDraggable?: boolean
|
||||
toggleVisibility: (
|
||||
nodeId: string,
|
||||
widgetName: string,
|
||||
isShown: boolean
|
||||
) => void
|
||||
}>()
|
||||
|
||||
function onClick() {
|
||||
props.toggleVisibility(props.nodeId, props.widgetName, props.isShown ?? false)
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<div class="widget-item items-center gap-1">
|
||||
<div class="size-4">
|
||||
<i-lucide:grip-vertical v-if="isDraggable" />
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<div class="widget-node">{{ nodeTitle }}</div>
|
||||
<div class="widget-name">{{ widgetName }}</div>
|
||||
</div>
|
||||
<Button
|
||||
size="small"
|
||||
class="shrink-0"
|
||||
text
|
||||
severity="secondary"
|
||||
@click.stop="onClick"
|
||||
>
|
||||
<i-lucide:eye v-if="isShown" />
|
||||
<i-lucide:eye-off v-else />
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.widget-item {
|
||||
display: flex;
|
||||
padding: 4px 16px 4px 0;
|
||||
word-break: break-all;
|
||||
border-radius: 4px;
|
||||
background: var(--p-dialog-background, #202020);
|
||||
}
|
||||
.widget-node {
|
||||
color: var(--color-slate-100, #9c9eab);
|
||||
|
||||
/* heading-text-nav */
|
||||
font-family: Inter;
|
||||
font-size: 10px;
|
||||
}
|
||||
.widget-name {
|
||||
color: var(--color-text-primary, #fff);
|
||||
|
||||
/* body-text-small */
|
||||
font-family: Inter;
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,13 +1,18 @@
|
||||
import { useNodeImage } from '@/composables/node/useNodeImage'
|
||||
import { CANVAS_IMAGE_PREVIEW_WIDGET } from '@/composables/node/useNodeCanvasImagePreview'
|
||||
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,8 +48,25 @@ 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 pw = parseProxyWidgets(fromNode.properties.proxyWidgets)
|
||||
for (const node of subgraph.nodes) {
|
||||
for (const widget of node.widgets ?? []) {
|
||||
widget.promoted = pw.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")
|
||||
|
||||
@@ -62,13 +84,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)
|
||||
@@ -89,16 +114,18 @@ function addProxyWidget(
|
||||
nodeId,
|
||||
widgetName,
|
||||
graph: subgraphNode.subgraph,
|
||||
name,
|
||||
label: name,
|
||||
isProxyWidget: true,
|
||||
y: 0,
|
||||
last_y: undefined,
|
||||
width: undefined,
|
||||
computedHeight: undefined,
|
||||
afterQueued: undefined,
|
||||
computedHeight: undefined,
|
||||
isProxyWidget: true,
|
||||
label: name,
|
||||
last_y: undefined,
|
||||
name,
|
||||
node: subgraphNode,
|
||||
onRemove: undefined,
|
||||
node: subgraphNode
|
||||
promoted: undefined,
|
||||
serialize: false,
|
||||
width: undefined,
|
||||
y: 0
|
||||
}
|
||||
return addProxyFromOverlay(subgraphNode, overlay)
|
||||
}
|
||||
@@ -110,23 +137,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 == CANVAS_IMAGE_PREVIEW_WIDGET) {
|
||||
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
|
||||
@@ -155,7 +179,9 @@ function addProxyFromOverlay(subgraphNode: SubgraphNode, overlay: Overlay) {
|
||||
let redirectedReceiver = receiver
|
||||
if (property == 'value') redirectedReceiver = backingWidget
|
||||
else if (property == 'computedHeight') {
|
||||
//update linkage regularly, but no more than once per frame
|
||||
if (overlay.widgetName == CANVAS_IMAGE_PREVIEW_WIDGET && linkedNode)
|
||||
updatePreviews(linkedNode)
|
||||
//update linkage regularly, but no more than once per frame
|
||||
;[linkedNode, linkedWidget] = resolveLinkedWidget(overlay)
|
||||
backingWidget = linkedWidget ?? disconnectedWidget
|
||||
}
|
||||
|
||||
126
src/core/graph/subgraph/proxyWidgetUtils.ts
Normal file
126
src/core/graph/subgraph/proxyWidgetUtils.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
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 { useSubgraphNavigationStore } from '@/stores/subgraphNavigationStore'
|
||||
|
||||
function pushWidgets(node: SubgraphNode, ...widgets: [string, string][]) {
|
||||
const pw = getProxyWidgets(node)
|
||||
pw.push(...widgets)
|
||||
node.properties.proxyWidgets = JSON.stringify(pw)
|
||||
}
|
||||
function getProxyWidgets(node: SubgraphNode) {
|
||||
return parseProxyWidgets(node.properties.proxyWidgets)
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables display of a widget on the parent subgraphNode
|
||||
* @param {IBaseWidget} widget - The widget to be promoted
|
||||
* @param {LGraphNode} node - the node which owns the widget
|
||||
*/
|
||||
function promoteWidget(
|
||||
widget: IBaseWidget,
|
||||
node: LGraphNode,
|
||||
parents: SubgraphNode[]
|
||||
) {
|
||||
for (const parent of parents) pushWidgets(parent, [`${node.id}`, widget.name])
|
||||
widget.promoted = true
|
||||
}
|
||||
|
||||
function demoteWidget(
|
||||
widget: IBaseWidget,
|
||||
node: LGraphNode,
|
||||
parents: SubgraphNode[]
|
||||
) {
|
||||
for (const parent of parents) {
|
||||
const pw = getProxyWidgets(parent).filter(
|
||||
([id, name]) => node.id != id || widget.name !== name
|
||||
)
|
||||
parent.properties.proxyWidgets = JSON.stringify(pw)
|
||||
}
|
||||
widget.promoted = false
|
||||
}
|
||||
|
||||
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 validNodes = []
|
||||
const parentGraph = navigationStack.at(-2) ?? subgraph.rootGraph
|
||||
for (const onode of parentGraph.nodes) {
|
||||
if (onode.type === subgraph.id && onode.isSubgraphNode()) {
|
||||
validNodes.push(onode)
|
||||
}
|
||||
}
|
||||
return validNodes
|
||||
}
|
||||
|
||||
export function addWidgetPromotionOptions(
|
||||
options: (IContextMenuValue<unknown> | null)[],
|
||||
widget: IBaseWidget,
|
||||
node: LGraphNode
|
||||
) {
|
||||
const parents = getParentNodes()
|
||||
const promotableParents = parents.filter(
|
||||
(s) =>
|
||||
!getProxyWidgets(s).some(
|
||||
([id, name]) => node.id == id && widget.name === name
|
||||
)
|
||||
)
|
||||
if (promotableParents.length > 0)
|
||||
options.unshift({
|
||||
content: `Promote Widget: ${widget.label ?? widget.name}`,
|
||||
callback: () => {
|
||||
promoteWidget(widget, node, promotableParents)
|
||||
}
|
||||
})
|
||||
else {
|
||||
options.unshift({
|
||||
content: `Un-Promote Widget: ${widget.label ?? widget.name}`,
|
||||
callback: () => {
|
||||
demoteWidget(widget, node, parents)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
//FIXME: This currently has ugly duplication with the sidebar pane
|
||||
//Refactor all the computed widget logic into a separate file (composable?)
|
||||
type WidgetItem = [LGraphNode, IBaseWidget]
|
||||
const recommendedNodes = [
|
||||
'CLIPTextEncode',
|
||||
'LoadImage',
|
||||
'SaveImage',
|
||||
'PreviewImage'
|
||||
]
|
||||
const recommendedWidgetNames = ['seed']
|
||||
function nodeWidgets(n: LGraphNode): WidgetItem[] {
|
||||
if (!n.widgets) return []
|
||||
return n.widgets.map((w: IBaseWidget) => [n, w])
|
||||
}
|
||||
export function promoteRecommendedWidgets(subgraphNode: SubgraphNode) {
|
||||
const interiorNodes = subgraphNode.subgraph.nodes
|
||||
const filteredWidgets: WidgetItem[] = interiorNodes
|
||||
.flatMap(nodeWidgets)
|
||||
//widget has connected link. Should not be eligible for promotion
|
||||
.filter(([_, w]: WidgetItem) => !w.computedDisabled)
|
||||
.filter(
|
||||
([node, widget]: WidgetItem) =>
|
||||
recommendedNodes.includes(node.type) ||
|
||||
recommendedWidgetNames.includes(widget.name)
|
||||
)
|
||||
const pw: ProxyWidgetsProperty = filteredWidgets.map(([n, w]: WidgetItem) => [
|
||||
`${n.id}`,
|
||||
w.name
|
||||
])
|
||||
subgraphNode.properties.proxyWidgets = JSON.stringify(pw)
|
||||
}
|
||||
22
src/core/graph/subgraph/useSubgraphNodeDialog.ts
Normal file
22
src/core/graph/subgraph/useSubgraphNodeDialog.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { type DialogComponentProps, useDialogStore } from '@/stores/dialogStore'
|
||||
|
||||
const key = 'global-subgraph-node-config'
|
||||
|
||||
export function showSubgraphNodeDialog() {
|
||||
const dialogStore = useDialogStore()
|
||||
const dialogComponentProps: DialogComponentProps = {
|
||||
headless: true,
|
||||
modal: false,
|
||||
closable: false,
|
||||
position: 'right'
|
||||
}
|
||||
//FIXME: the vuedraggable import has unknown sideffects that break tests.
|
||||
void import('@/core/graph/subgraph/SubgraphNode.vue').then((SubgraphNode) => {
|
||||
dialogStore.showDialog({
|
||||
title: 'Parameters',
|
||||
key,
|
||||
component: SubgraphNode.default,
|
||||
dialogComponentProps
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -4,7 +4,7 @@ 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
|
||||
|
||||
@@ -1866,13 +1866,13 @@ export class LGraphCanvas
|
||||
this.#dirty()
|
||||
}
|
||||
|
||||
openSubgraph(subgraph: Subgraph): void {
|
||||
openSubgraph(subgraph: Subgraph, fromNode: SubgraphNode): void {
|
||||
const { graph } = this
|
||||
if (!graph) throw new NullGraphError()
|
||||
|
||||
const options = {
|
||||
bubbles: true,
|
||||
detail: { subgraph, closingGraph: graph },
|
||||
detail: { subgraph, closingGraph: graph, fromNode },
|
||||
cancelable: true
|
||||
}
|
||||
const mayContinue = this.canvas.dispatchEvent(
|
||||
@@ -2793,7 +2793,7 @@ export class LGraphCanvas
|
||||
if (pos[1] < 0 && !inCollapse) {
|
||||
node.onNodeTitleDblClick?.(e, pos, this)
|
||||
} else if (node instanceof SubgraphNode) {
|
||||
this.openSubgraph(node.subgraph)
|
||||
this.openSubgraph(node.subgraph, node)
|
||||
}
|
||||
|
||||
node.onDblClick?.(e, pos, this)
|
||||
|
||||
@@ -70,6 +70,7 @@ export class LiteGraphGlobal {
|
||||
|
||||
WIDGET_BGCOLOR = '#222'
|
||||
WIDGET_OUTLINE_COLOR = '#666'
|
||||
WIDGET_PROMOTED_OUTLINE_COLOR = '#BF00FF'
|
||||
WIDGET_ADVANCED_OUTLINE_COLOR = 'rgba(56, 139, 253, 0.8)'
|
||||
WIDGET_TEXT_COLOR = '#DDD'
|
||||
WIDGET_SECONDARY_TEXT_COLOR = '#999'
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { LGraphGroup } from '@/lib/litegraph/src/LGraphGroup'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
|
||||
import type { ConnectingLink } from '@/lib/litegraph/src/interfaces'
|
||||
import type { Subgraph } from '@/lib/litegraph/src/subgraph/Subgraph'
|
||||
import type { SubgraphNode } from '@/lib/litegraph/src/subgraph/SubgraphNode'
|
||||
import type { CanvasPointerEvent } from '@/lib/litegraph/src/types/events'
|
||||
|
||||
export interface LGraphCanvasEventMap {
|
||||
@@ -14,6 +15,11 @@ export interface LGraphCanvasEventMap {
|
||||
/** The old active graph, or `null` if there was no active graph. */
|
||||
oldGraph: LGraph | Subgraph | null | undefined
|
||||
}
|
||||
'subgraph-opened': {
|
||||
subgraph: Subgraph
|
||||
closingGraph: LGraph
|
||||
fromNode: SubgraphNode
|
||||
}
|
||||
|
||||
'litegraph:canvas':
|
||||
| { subType: 'before-change' | 'after-change' }
|
||||
|
||||
@@ -168,7 +168,7 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
|
||||
canvas: LGraphCanvas
|
||||
): void {
|
||||
if (button.name === 'enter_subgraph') {
|
||||
canvas.openSubgraph(this.subgraph)
|
||||
canvas.openSubgraph(this.subgraph, this)
|
||||
} else {
|
||||
super.onTitleButtonClick(button, canvas)
|
||||
}
|
||||
|
||||
@@ -304,6 +304,12 @@ export interface IBaseWidget<
|
||||
|
||||
hidden?: boolean
|
||||
advanced?: boolean
|
||||
/**
|
||||
* Set if the node is displayed on the parent subgraphNode
|
||||
* Promoted widgets have a green border
|
||||
* @readonly [Computed] This property is computed on graph change
|
||||
*/
|
||||
promoted?: boolean
|
||||
|
||||
tooltip?: string
|
||||
|
||||
|
||||
@@ -74,6 +74,7 @@ export abstract class BaseWidget<TWidget extends IBaseWidget = IBaseWidget>
|
||||
computedDisabled?: boolean
|
||||
hidden?: boolean
|
||||
advanced?: boolean
|
||||
promoted?: boolean
|
||||
tooltip?: string
|
||||
element?: HTMLElement
|
||||
callback?(
|
||||
@@ -146,6 +147,7 @@ export abstract class BaseWidget<TWidget extends IBaseWidget = IBaseWidget>
|
||||
}
|
||||
|
||||
get outline_color() {
|
||||
if (this.promoted) return LiteGraph.WIDGET_PROMOTED_OUTLINE_COLOR
|
||||
return this.advanced
|
||||
? LiteGraph.WIDGET_ADVANCED_OUTLINE_COLOR
|
||||
: LiteGraph.WIDGET_OUTLINE_COLOR
|
||||
|
||||
@@ -90,6 +90,7 @@
|
||||
"searchModels": "Search Models",
|
||||
"searchKeybindings": "Search Keybindings",
|
||||
"searchExtensions": "Search Extensions",
|
||||
"search": "Search",
|
||||
"noResultsFound": "No Results Found",
|
||||
"searchFailedMessage": "We couldn't find any settings matching your search. Try adjusting your search terms.",
|
||||
"noTasksFound": "No Tasks Found",
|
||||
@@ -1048,7 +1049,11 @@
|
||||
"publish": "Publish Subgraph",
|
||||
"publishSuccess": "Saved to Nodes Library",
|
||||
"publishSuccessMessage": "You can find your subgraph blueprint in the nodes library under \"Subgraph Blueprints\"",
|
||||
"loadFailure": "Failed to load subgraph blueprints"
|
||||
"loadFailure": "Failed to load subgraph blueprints",
|
||||
"shown": "Shown on node",
|
||||
"showAll": "Show all",
|
||||
"hidden": "Hidden / nested parameters",
|
||||
"hideAll": "Hide all"
|
||||
},
|
||||
"electronFileDownload": {
|
||||
"inProgress": "In Progress",
|
||||
|
||||
@@ -3,7 +3,11 @@ import { defineStore } from 'pinia'
|
||||
import { type Raw, computed, markRaw, ref, shallowRef, watch } from 'vue'
|
||||
|
||||
import { t } from '@/i18n'
|
||||
import type { LGraph, Subgraph } from '@/lib/litegraph/src/litegraph'
|
||||
import type {
|
||||
LGraph,
|
||||
LGraphNode,
|
||||
Subgraph
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
import type { ComfyWorkflowJSON } from '@/platform/workflow/validation/schemas/workflowSchema'
|
||||
import type { NodeId } from '@/platform/workflow/validation/schemas/workflowSchema'
|
||||
import { useWorkflowThumbnail } from '@/renderer/core/thumbnail/useWorkflowThumbnail'
|
||||
@@ -184,6 +188,7 @@ interface WorkflowStore {
|
||||
updateActiveGraph: () => void
|
||||
executionIdToCurrentId: (id: string) => any
|
||||
nodeIdToNodeLocatorId: (nodeId: NodeId, subgraph?: Subgraph) => NodeLocatorId
|
||||
nodeToNodeLocatorId: (node: LGraphNode) => NodeLocatorId
|
||||
nodeExecutionIdToNodeLocatorId: (
|
||||
nodeExecutionId: NodeExecutionId | string
|
||||
) => NodeLocatorId | null
|
||||
@@ -581,6 +586,17 @@ export const useWorkflowStore = defineStore('workflow', () => {
|
||||
|
||||
return createNodeLocatorId(targetSubgraph.id, nodeId)
|
||||
}
|
||||
/**
|
||||
* Convert a node to a NodeLocatorId
|
||||
* Does not assume the node resides in the active graph
|
||||
* @param The actual node instance
|
||||
* @returns The NodeLocatorId (for root graph nodes, returns the node ID as-is)
|
||||
*/
|
||||
const nodeToNodeLocatorId = (node: LGraphNode): NodeLocatorId => {
|
||||
if (isSubgraph(node.graph))
|
||||
return createNodeLocatorId(node.graph.id, node.id)
|
||||
return String(node.id)
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an execution ID to a NodeLocatorId
|
||||
@@ -723,6 +739,7 @@ export const useWorkflowStore = defineStore('workflow', () => {
|
||||
updateActiveGraph,
|
||||
executionIdToCurrentId,
|
||||
nodeIdToNodeLocatorId,
|
||||
nodeToNodeLocatorId,
|
||||
nodeExecutionIdToNodeLocatorId,
|
||||
nodeLocatorIdToNodeId,
|
||||
nodeLocatorIdToNodeExecutionId
|
||||
|
||||
@@ -326,7 +326,7 @@ const handleEnterSubgraph = () => {
|
||||
return
|
||||
}
|
||||
|
||||
canvas.openSubgraph(litegraphNode.subgraph)
|
||||
canvas.openSubgraph(litegraphNode.subgraph, litegraphNode)
|
||||
}
|
||||
|
||||
const nodeOutputs = useNodeOutputStore()
|
||||
|
||||
@@ -19,7 +19,8 @@ import { is_all_same_aspect_ratio } from '@/utils/imageUtil'
|
||||
const renderPreview = (
|
||||
ctx: CanvasRenderingContext2D,
|
||||
node: LGraphNode,
|
||||
shiftY: number
|
||||
shiftY: number,
|
||||
computedHeight: number | undefined
|
||||
) => {
|
||||
const canvas = useCanvasStore().getCanvas()
|
||||
const mouse = canvas.graph_mouse
|
||||
@@ -46,7 +47,7 @@ const renderPreview = (
|
||||
const allowImageSizeDraw = settingStore.get('Comfy.Node.AllowImageSizeDraw')
|
||||
const IMAGE_TEXT_SIZE_TEXT_HEIGHT = allowImageSizeDraw ? 15 : 0
|
||||
const dw = node.size[0]
|
||||
const dh = node.size[1] - shiftY - IMAGE_TEXT_SIZE_TEXT_HEIGHT
|
||||
const dh = computedHeight ? computedHeight - IMAGE_TEXT_SIZE_TEXT_HEIGHT : 0
|
||||
|
||||
if (imageIndex == null) {
|
||||
// No image selected; draw thumbnails of all
|
||||
@@ -260,7 +261,7 @@ class ImagePreviewWidget extends BaseWidget {
|
||||
}
|
||||
|
||||
override drawWidget(ctx: CanvasRenderingContext2D): void {
|
||||
renderPreview(ctx, this.node, this.y)
|
||||
renderPreview(ctx, this.node, this.y, this.computedHeight)
|
||||
}
|
||||
|
||||
override onPointerDown(pointer: CanvasPointer, node: LGraphNode): boolean {
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { ToastMessageOptions } from 'primevue/toast'
|
||||
import { reactive } from 'vue'
|
||||
|
||||
import { useCanvasPositionConversion } from '@/composables/element/useCanvasPositionConversion'
|
||||
import { registerProxyWidgets } from '@/core/graph/subgraph/proxyWidget'
|
||||
import { st, t } from '@/i18n'
|
||||
import {
|
||||
LGraph,
|
||||
@@ -868,6 +869,7 @@ export class ComfyApp {
|
||||
}
|
||||
}
|
||||
)
|
||||
registerProxyWidgets(this.canvas)
|
||||
|
||||
this.graph.start()
|
||||
|
||||
|
||||
@@ -160,16 +160,24 @@ abstract class BaseDOMWidgetImpl<V extends object | string>
|
||||
widget_height: number,
|
||||
lowQuality?: boolean
|
||||
): void {
|
||||
if (this.options.hideOnZoom && lowQuality && this.isVisible()) {
|
||||
if (
|
||||
(this.promoted || (this.options.hideOnZoom && lowQuality)) &&
|
||||
this.isVisible()
|
||||
) {
|
||||
// Draw a placeholder rectangle
|
||||
const originalFillStyle = ctx.fillStyle
|
||||
ctx.beginPath()
|
||||
ctx.fillStyle = LiteGraph.WIDGET_BGCOLOR
|
||||
let margin = this.margin
|
||||
if (this.promoted) {
|
||||
ctx.fillStyle = LiteGraph.WIDGET_PROMOTED_OUTLINE_COLOR
|
||||
margin -= 1
|
||||
}
|
||||
ctx.rect(
|
||||
this.margin,
|
||||
y + this.margin,
|
||||
widget_width - this.margin * 2,
|
||||
(this.computedHeight ?? widget_height) - 2 * this.margin
|
||||
margin,
|
||||
y + margin,
|
||||
widget_width - margin * 2,
|
||||
(this.computedHeight ?? widget_height) - 2 * margin
|
||||
)
|
||||
ctx.fill()
|
||||
ctx.fillStyle = originalFillStyle
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import '@/core/graph/subgraph/proxyWidget'
|
||||
import { t } from '@/i18n'
|
||||
import { type LGraphNode, isComboWidget } from '@/lib/litegraph/src/litegraph'
|
||||
import type {
|
||||
|
||||
@@ -4,6 +4,8 @@ import { useSelectedLiteGraphItems } from '@/composables/canvas/useSelectedLiteG
|
||||
import { useNodeAnimatedImage } from '@/composables/node/useNodeAnimatedImage'
|
||||
import { useNodeCanvasImagePreview } from '@/composables/node/useNodeCanvasImagePreview'
|
||||
import { useNodeImage, useNodeVideo } from '@/composables/node/useNodeImage'
|
||||
import { addWidgetPromotionOptions } from '@/core/graph/subgraph/proxyWidgetUtils'
|
||||
import { showSubgraphNodeDialog } from '@/core/graph/subgraph/useSubgraphNodeDialog'
|
||||
import { st, t } from '@/i18n'
|
||||
import {
|
||||
type IContextMenuValue,
|
||||
@@ -741,7 +743,7 @@ export const useLitegraphService = () => {
|
||||
]
|
||||
}
|
||||
|
||||
node.prototype.getExtraMenuOptions = function (_, options) {
|
||||
node.prototype.getExtraMenuOptions = function (canvas, options) {
|
||||
if (this.imgs) {
|
||||
// If this node has images then we add an open in new tab item
|
||||
let img
|
||||
@@ -788,7 +790,7 @@ export const useLitegraphService = () => {
|
||||
content: 'Bypass',
|
||||
callback: () => {
|
||||
toggleSelectedNodesMode(LGraphEventMode.BYPASS)
|
||||
app.canvas.setDirty(true, true)
|
||||
canvas.setDirty(true, true)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -824,18 +826,88 @@ export const useLitegraphService = () => {
|
||||
}
|
||||
}
|
||||
if (this instanceof SubgraphNode) {
|
||||
options.unshift({
|
||||
content: 'Unpack Subgraph',
|
||||
callback: () => {
|
||||
useNodeOutputStore().revokeSubgraphPreviews(this)
|
||||
this.graph.unpackSubgraph(this)
|
||||
options.unshift(
|
||||
{
|
||||
content: 'Edit Subgraph Widgets',
|
||||
callback: () => {
|
||||
showSubgraphNodeDialog()
|
||||
}
|
||||
},
|
||||
{
|
||||
content: 'Unpack Subgraph',
|
||||
callback: () => {
|
||||
useNodeOutputStore().revokeSubgraphPreviews(this)
|
||||
this.graph.unpackSubgraph(this)
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
if (this.graph && !this.graph.isRootGraph) {
|
||||
const [x, y] = canvas.canvas_mouse
|
||||
const overWidget = this.getWidgetOnPos(x, y, true)
|
||||
if (overWidget) {
|
||||
addWidgetPromotionOptions(options, overWidget, this)
|
||||
}
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
}
|
||||
function updatePreviews(node: LGraphNode) {
|
||||
try {
|
||||
unsafeUpdatePreviews.call(node)
|
||||
} catch (error) {
|
||||
console.error('Error drawing node background', error)
|
||||
}
|
||||
}
|
||||
function unsafeUpdatePreviews(this: LGraphNode) {
|
||||
if (this.flags.collapsed) return
|
||||
|
||||
const nodeOutputStore = useNodeOutputStore()
|
||||
const { showAnimatedPreview, removeAnimatedPreview } =
|
||||
useNodeAnimatedImage()
|
||||
const { showCanvasImagePreview, removeCanvasImagePreview } =
|
||||
useNodeCanvasImagePreview()
|
||||
|
||||
const output = nodeOutputStore.getNodeOutputs(this)
|
||||
const preview = nodeOutputStore.getNodePreviews(this)
|
||||
|
||||
const isNewOutput = output && this.images !== output.images
|
||||
const isNewPreview = preview && this.preview !== preview
|
||||
|
||||
if (isNewPreview) this.preview = preview
|
||||
if (isNewOutput) this.images = output.images
|
||||
|
||||
if (isNewOutput || isNewPreview) {
|
||||
this.animatedImages = output?.animated?.find(Boolean)
|
||||
|
||||
const isAnimatedWebp =
|
||||
this.animatedImages &&
|
||||
output?.images?.some((img) => img.filename?.includes('webp'))
|
||||
const isAnimatedPng =
|
||||
this.animatedImages &&
|
||||
output?.images?.some((img) => img.filename?.includes('png'))
|
||||
const isVideo =
|
||||
(this.animatedImages && !isAnimatedWebp && !isAnimatedPng) ||
|
||||
isVideoNode(this)
|
||||
if (isVideo) {
|
||||
useNodeVideo(this).showPreview()
|
||||
} else {
|
||||
useNodeImage(this).showPreview()
|
||||
}
|
||||
}
|
||||
|
||||
// Nothing to do
|
||||
if (!this.imgs?.length) return
|
||||
|
||||
if (this.animatedImages) {
|
||||
removeCanvasImagePreview(this)
|
||||
showAnimatedPreview(this)
|
||||
} else {
|
||||
removeAnimatedPreview(this)
|
||||
showCanvasImagePreview(this)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds Custom drawing logic for nodes
|
||||
@@ -851,62 +923,8 @@ export const useLitegraphService = () => {
|
||||
'node.setSizeForImage is deprecated. Now it has no effect. Please remove the call to it.'
|
||||
)
|
||||
}
|
||||
|
||||
function unsafeDrawBackground(this: LGraphNode) {
|
||||
if (this.flags.collapsed) return
|
||||
|
||||
const nodeOutputStore = useNodeOutputStore()
|
||||
const { showAnimatedPreview, removeAnimatedPreview } =
|
||||
useNodeAnimatedImage()
|
||||
const { showCanvasImagePreview, removeCanvasImagePreview } =
|
||||
useNodeCanvasImagePreview()
|
||||
|
||||
const output = nodeOutputStore.getNodeOutputs(this)
|
||||
const preview = nodeOutputStore.getNodePreviews(this)
|
||||
|
||||
const isNewOutput = output && this.images !== output.images
|
||||
const isNewPreview = preview && this.preview !== preview
|
||||
|
||||
if (isNewPreview) this.preview = preview
|
||||
if (isNewOutput) this.images = output.images
|
||||
|
||||
if (isNewOutput || isNewPreview) {
|
||||
this.animatedImages = output?.animated?.find(Boolean)
|
||||
|
||||
const isAnimatedWebp =
|
||||
this.animatedImages &&
|
||||
output?.images?.some((img) => img.filename?.includes('webp'))
|
||||
const isAnimatedPng =
|
||||
this.animatedImages &&
|
||||
output?.images?.some((img) => img.filename?.includes('png'))
|
||||
const isVideo =
|
||||
(this.animatedImages && !isAnimatedWebp && !isAnimatedPng) ||
|
||||
isVideoNode(this)
|
||||
if (isVideo) {
|
||||
useNodeVideo(this).showPreview()
|
||||
} else {
|
||||
useNodeImage(this).showPreview()
|
||||
}
|
||||
}
|
||||
|
||||
// Nothing to do
|
||||
if (!this.imgs?.length) return
|
||||
|
||||
if (this.animatedImages) {
|
||||
removeCanvasImagePreview(this)
|
||||
showAnimatedPreview(this)
|
||||
} else {
|
||||
removeAnimatedPreview(this)
|
||||
showCanvasImagePreview(this)
|
||||
}
|
||||
}
|
||||
|
||||
node.prototype.onDrawBackground = function () {
|
||||
try {
|
||||
unsafeDrawBackground.call(this)
|
||||
} catch (error) {
|
||||
console.error('Error drawing node background', error)
|
||||
}
|
||||
updatePreviews(this)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1036,6 +1054,7 @@ export const useLitegraphService = () => {
|
||||
getCanvasCenter,
|
||||
goToNode,
|
||||
resetView,
|
||||
fitView
|
||||
fitView,
|
||||
updatePreviews
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import type { LGraphNode, SubgraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { Subgraph } from '@/lib/litegraph/src/litegraph'
|
||||
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
|
||||
import type {
|
||||
ExecutedWsMessage,
|
||||
@@ -38,7 +37,7 @@ interface SetOutputOptions {
|
||||
}
|
||||
|
||||
export const useNodeOutputStore = defineStore('nodeOutput', () => {
|
||||
const { nodeIdToNodeLocatorId } = useWorkflowStore()
|
||||
const { nodeIdToNodeLocatorId, nodeToNodeLocatorId } = useWorkflowStore()
|
||||
const { executionIdToNodeLocatorId } = useExecutionStore()
|
||||
const scheduledRevoke: Record<NodeLocatorId, { stop: () => void }> = {}
|
||||
|
||||
@@ -63,11 +62,11 @@ export const useNodeOutputStore = defineStore('nodeOutput', () => {
|
||||
function getNodeOutputs(
|
||||
node: LGraphNode
|
||||
): ExecutedWsMessage['output'] | undefined {
|
||||
return app.nodeOutputs[nodeIdToNodeLocatorId(node.id)]
|
||||
return app.nodeOutputs[nodeToNodeLocatorId(node)]
|
||||
}
|
||||
|
||||
function getNodePreviews(node: LGraphNode): string[] | undefined {
|
||||
return app.nodePreviewImages[nodeIdToNodeLocatorId(node.id)]
|
||||
return app.nodePreviewImages[nodeToNodeLocatorId(node)]
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -161,10 +160,7 @@ export const useNodeOutputStore = defineStore('nodeOutput', () => {
|
||||
) {
|
||||
if (!filenames || !node) return
|
||||
|
||||
const locatorId =
|
||||
node.graph instanceof Subgraph
|
||||
? nodeIdToNodeLocatorId(node.id, node.graph ?? undefined)
|
||||
: `${node.id}`
|
||||
const locatorId = nodeToNodeLocatorId(node)
|
||||
if (!locatorId) return
|
||||
if (typeof filenames === 'string') {
|
||||
setOutputsByLocatorId(
|
||||
|
||||
@@ -134,6 +134,7 @@ LiteGraphGlobal {
|
||||
"WIDGET_BGCOLOR": "#222",
|
||||
"WIDGET_DISABLED_TEXT_COLOR": "#666",
|
||||
"WIDGET_OUTLINE_COLOR": "#666",
|
||||
"WIDGET_PROMOTED_OUTLINE_COLOR": "#BF00FF",
|
||||
"WIDGET_SECONDARY_TEXT_COLOR": "#999",
|
||||
"WIDGET_TEXT_COLOR": "#DDD",
|
||||
"allow_multi_output_for_events": true,
|
||||
|
||||
@@ -1,21 +1,30 @@
|
||||
import { describe, expect, test, vi } from 'vitest'
|
||||
|
||||
import '@/core/graph/subgraph/proxyWidget'
|
||||
//import { ComponentWidgetImpl, DOMWidgetImpl } from '@/scripts/domWidget'
|
||||
|
||||
import { LGraphNode, type SubgraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { registerProxyWidgets } from '@/core/graph/subgraph/proxyWidget'
|
||||
import {
|
||||
type LGraphCanvas,
|
||||
LGraphNode,
|
||||
type SubgraphNode
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
|
||||
import {
|
||||
createTestSubgraph,
|
||||
createTestSubgraphNode
|
||||
} from '../litegraph/subgraph/fixtures/subgraphHelpers'
|
||||
|
||||
registerProxyWidgets({
|
||||
canvas: { addEventListener() {} }
|
||||
} as unknown as LGraphCanvas)
|
||||
|
||||
vi.mock('@/renderer/core/canvas/canvasStore', () => ({
|
||||
useCanvasStore: () => ({})
|
||||
}))
|
||||
vi.mock('@/stores/domWidgetStore', () => ({
|
||||
useDomWidgetStore: () => ({ widgetStates: new Map() })
|
||||
}))
|
||||
vi.mock('@/services/litegraphService', () => ({
|
||||
useLitegraphService: () => ({ updatePreviews: () => ({}) })
|
||||
}))
|
||||
|
||||
function setupSubgraph(
|
||||
innerNodeCount: number = 0
|
||||
|
||||
Reference in New Issue
Block a user