Compare commits

...

74 Commits

Author SHA1 Message Date
Austin Mroz
9e8a426c00 Update test for previews fix 2025-09-28 00:19:46 -05:00
Austin Mroz
cc7c22a428 Fix preview initialization 2025-09-28 00:09:19 -05:00
Austin Mroz
94f6778285 Move SubgraphNode components 2025-09-27 16:19:03 -05:00
Austin Mroz
e91bbf030b Fix test cases
Turns out the vuedraggable import has side effects that break other
test cases. This is awful and requires further investigation, but for
now, the import is performed at execution time
2025-09-27 15:34:39 -05:00
Austin Mroz
44d57ce041 Move widget configuration out of sidebar 2025-09-27 12:23:45 -05:00
Austin Mroz
7c20f37973 Finish partial migrations from merge 2025-09-26 14:36:31 -05:00
Austin Mroz
e1971a2595 Merge working branch with main 2025-09-26 13:19:40 -05:00
Austin Mroz
992e4a7f8a Display recommended widgets on conversion.
This code will need refactoring later.
2025-09-26 09:37:06 -05:00
Austin Mroz
92f2578357 Prevent doubled promotion, add demotion option 2025-09-26 08:48:27 -05:00
Austin Mroz
aeafee4e12 Update promoted widget outline color 2025-09-26 08:47:08 -05:00
Austin Mroz
040c6e1691 Disable serialization of proxyWidgets
Also sorts the overlay property values
2025-09-25 14:02:53 -05:00
Austin Mroz
f1c2d0ef6a Add promoted border to DOMWidgets 2025-09-25 11:00:19 -05:00
Austin Mroz
0de4351913 Only update domWidgetStore if graph is active 2025-09-25 10:51:04 -05:00
Austin Mroz
3efb76b765 Add logic to compute widget.prompted
NOTE: Currently fails to track state with nested subgraphs
The event for "Leaving a graph" does not indicate the direction
2025-09-25 10:42:32 -05:00
Austin Mroz
a32fc7a667 Add display code to indicate widget is promoted
Actually calculating that this should be displayed will come later
2025-09-24 15:53:30 -05:00
Austin Mroz
de399e2e6c Add right click widget -> promote inside subgraph 2025-09-24 14:49:14 -05:00
Austin Mroz
768ffb2c9f For "Show Recommended" keep existing promotions 2025-09-24 10:59:14 -05:00
Austin Mroz
91a6f9d762 Fix hiding of non-proxied widgets 2025-09-23 13:40:24 -05:00
Austin Mroz
4ba1e5e485 Remove test for reading proxyWidgets 2025-09-22 16:41:46 -05:00
Austin Mroz
31a40e9769 Move from wrapping an interal method to onConfigure 2025-09-22 15:44:48 -05:00
Austin Mroz
dd58b272b8 Conditionally display show recommended
Also use a slightly nicer button as placeholder
2025-09-22 14:45:05 -05:00
Austin Mroz
e67bd12168 Reimplement disconnect/reconnect logic, add test 2025-09-22 13:32:58 -05:00
Austin Mroz
ebe92ed884 Add widget positioning test 2025-09-22 12:44:57 -05:00
Austin Mroz
e902afc828 Update comments 2025-09-22 09:43:49 -05:00
Austin Mroz
3d279716a3 Split handler into individual functions 2025-09-21 16:24:48 -05:00
Austin Mroz
b00855be3e Use computedHeight for preview display 2025-09-19 18:08:11 -05:00
Austin Mroz
915870d47c Fix accidental style breakage 2025-09-19 15:07:40 -05:00
Austin Mroz
0e8679798b Guard node proxying by widgetName so both function 2025-09-19 14:53:46 -05:00
Austin Mroz
f63a76e41a Repurpose existing graph traversal util 2025-09-19 10:52:21 -05:00
Austin Mroz
3e677e3dbe Add show recommended button, preview work
Adds the framework for a system to automate display of a curated list of
recommended widgets to the node.

As part of this, a return to display of "image previews" was made.
This code is causing lots of problems. Much of the logic is dependent
upon the actual node going through the draw loop. As nodes in the
subgraph don't receive redraws, there's lots of issues with managing the
initial display and ensuring that an initial draw occurs.

This commit includes support for updating previews, but is more brittle
than I would like.
2025-09-19 09:51:04 -05:00
Austin Mroz
02bf937741 Clean up widget item styling 2025-09-16 18:11:24 -05:00
Austin Mroz
784ebfe331 Styling cleanup, fix node size on append 2025-09-16 17:16:36 -05:00
Austin Mroz
26a9527d23 Actually test that widget names aren't equal 2025-09-16 14:41:59 -05:00
Austin Mroz
b287bb645b Fix unused exports to satisfy knip 2025-09-16 14:23:50 -05:00
Austin Mroz
ee5903877a More tests with better organization 2025-09-16 14:18:03 -05:00
Austin Mroz
1d7546f9bf Add basic tests 2025-09-16 12:34:47 -05:00
Austin Mroz
dbb9a9acd4 Move proxyWidgets out of extensions dir 2025-09-16 12:25:00 -05:00
Austin Mroz
dcc056754d Remove extension code and setTimeout 2025-09-16 11:13:01 -05:00
Austin Mroz
4a7bc4aadf Fix typo in last_y breaking mouse events
Further prune hot loop, and remove cast
2025-09-16 01:19:47 -05:00
Austin Mroz
3187694a31 Simplify proxy handler code 2025-09-15 17:57:47 -05:00
Austin Mroz
9b6611dd20 Move schema to dedicated file, remove cast 2025-09-15 16:35:25 -05:00
Austin Mroz
ccc8d6e441 serialize to string at proxyWidgets boundry
While node.properties function with anything serializeable, the format
for proxyWidgets is not a valid option for type. After great
consideration, all access to and from this value goes through a JSON
serialization and parsing always includes a zod validation step.

This is sturdier to outside misuse, has even lower risk of custom node
breakage, and means that there's now proper type checking at the
boundries of interaction.

Performance was a major concern against this, but the path is quite
cold. I estimate the value of optimization here to be 3-4 orders of
magnitude less important than anything occuring during the draw loop
(like access to proxyWidget elements)
2025-09-15 15:44:15 -05:00
Austin Mroz
166cdc385c Fix type checking on ProxyWidget implementation 2025-09-13 13:05:27 -05:00
Austin Mroz
7fd4fc556a Swap label order: cleaner and more consistent 2025-09-13 10:36:47 -05:00
Austin Mroz
ed2cc26cff Implement a DisconnectedWidget, refactor to use 2025-09-13 10:33:31 -05:00
Austin Mroz
073175a719 Fix typechecking and linting
Note: the proxywidget code is still marked as ignored. This will require
further careful review in the future.
2025-09-12 13:19:47 -05:00
Austin Mroz
ec06e28af7 Pull proxyWidget code out of SubgraphNode
This was needed from an organizational standpoint. For now, it requires
an ugly setTimeout to prevent proxyWidgets from being clobbered during
initialization, but this will be cleaned up later.

This also allows for the proxy widget code to have type checks ignored.
I fully intend to find a functional solution here, but this provides a
migration path where typechecking can be enabled for the rest of the PR
first

Also cleans up type checking on graph change in scripts/app.ts
2025-09-12 09:42:08 -05:00
Austin Mroz
5d4c4ef63e Restrict show/hide all to search query 2025-09-12 08:06:36 -05:00
Austin Mroz
eb85406d70 linting 2025-09-11 17:36:39 -05:00
Austin Mroz
e820515db4 On widget change, also set back canvas dirty 2025-09-11 17:24:12 -05:00
Austin Mroz
b9dcb813d8 Implement show/hide all. dw state in prop handler 2025-09-11 17:03:48 -05:00
Austin Mroz
1e72ed8a78 Cursor fixes, show/hide all framework 2025-09-11 15:10:48 -05:00
Austin Mroz
4016929070 Merge main into austin/widgets-v2 2025-09-11 13:04:34 -05:00
Austin Mroz
e0eb699a93 Add searchbar 2025-09-11 12:02:34 -05:00
Austin Mroz
68bb29bb17 Add vuedraggable as dependency 2025-09-10 16:11:48 -05:00
Austin Mroz
bb25301e36 Update widget state after visibility toggle 2025-09-10 15:37:31 -05:00
Austin Mroz
51d68654f1 Spacing and filtering cleanup 2025-09-10 14:19:40 -05:00
Austin Mroz
ec09d9b365 Working drag and drop 2025-09-10 12:32:02 -05:00
Austin Mroz
2212151686 Fix removal of DOM Widgets
Needs further review for what the actual purpose of the active state in
the widget store is if it's not being used for determining visibility
2025-09-05 16:43:34 -05:00
Austin Mroz
eab71667b6 Better computedDisabled handling
computedDisabled is inherited, but propogates
2025-09-05 11:21:51 -05:00
Austin Mroz
b424c62a99 wip cleanup, filtering, setdirty 2025-09-05 10:11:51 -05:00
Austin Mroz
4150b1ccb4 Minor formatting fix and log removal 2025-09-04 15:02:50 -05:00
Austin Mroz
985c0beb8e Immediately display DOMWidgets when added 2025-09-04 13:31:49 -05:00
Austin Mroz
9b18125d16 Basic widget toggling 2025-09-04 13:13:18 -05:00
Austin Mroz
1cca658351 Functional listing of widgets using TreeExplorer 2025-09-04 12:05:58 -05:00
Austin Mroz
e4c6514d9d Minimal subgraph widget ui framework
Displays all widgets poorly and allows re ordering
2025-09-01 18:08:06 -07:00
Austin Mroz
e6aeaf4112 Add persistence to proxy widgets 2025-09-01 15:44:39 -07:00
Austin Mroz
11978b6b71 Mostly functional DOMWidgets 2025-09-01 10:12:12 -07:00
Austin Mroz
703dc8fd4c Remove private variables, simplify logic
Turns out, ComboWidget was the only one using private variables.
Everything else is simple
2025-08-30 07:44:36 -07:00
Austin Mroz
ca7698dae5 what even is a reciever 2025-08-29 14:40:41 -07:00
Austin Mroz
b505bd9a80 More private suffering 2025-08-29 10:09:49 -07:00
Austin Mroz
d367f901a0 wip 2025-08-29 10:08:54 -07:00
Austin Mroz
7bd6fa45cf further exp 2025-08-28 11:54:42 -07:00
Austin Mroz
5be68c03fc Wip mirror widget test 2025-08-27 21:15:12 -07:00
29 changed files with 766 additions and 120 deletions

View File

@@ -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
View File

@@ -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)

View File

@@ -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)

View 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>

View File

@@ -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

View File

@@ -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()
}
},

View 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>

View 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>

View File

@@ -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
}

View 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)
}

View 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
})
})
}

View File

@@ -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

View File

@@ -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)

View File

@@ -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'

View File

@@ -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' }

View File

@@ -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)
}

View File

@@ -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

View File

@@ -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

View File

@@ -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",

View File

@@ -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

View File

@@ -326,7 +326,7 @@ const handleEnterSubgraph = () => {
return
}
canvas.openSubgraph(litegraphNode.subgraph)
canvas.openSubgraph(litegraphNode.subgraph, litegraphNode)
}
const nodeOutputs = useNodeOutputStore()

View File

@@ -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 {

View File

@@ -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()

View File

@@ -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

View File

@@ -1,4 +1,3 @@
import '@/core/graph/subgraph/proxyWidget'
import { t } from '@/i18n'
import { type LGraphNode, isComboWidget } from '@/lib/litegraph/src/litegraph'
import type {

View File

@@ -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
}
}

View File

@@ -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(

View File

@@ -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,

View File

@@ -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