mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-24 14:45:36 +00:00
refactor(subgraph): remove promotion store use
Amp-Thread-ID: https://ampcode.com/threads/T-019e05c3-bed1-706a-a7a7-27733a6ab1e4 Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
@@ -3,10 +3,10 @@ import { computed, inject, provide, ref, shallowRef, watchEffect } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import { isWidgetPromotedOnSubgraphNode } from '@/core/graph/subgraph/promotionUtils'
|
||||
import { isPromotedWidgetView } from '@/core/graph/subgraph/promotedWidgetTypes'
|
||||
import type { LGraphGroup, LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { SubgraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { usePromotionStore } from '@/stores/promotionStore'
|
||||
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import { useExecutionErrorStore } from '@/stores/executionErrorStore'
|
||||
@@ -70,8 +70,6 @@ const { t } = useI18n()
|
||||
|
||||
const getNodeParentGroup = inject(GetNodeParentGroupKey, null)
|
||||
|
||||
const promotionStore = usePromotionStore()
|
||||
|
||||
function isWidgetShownOnParents(
|
||||
widgetNode: LGraphNode,
|
||||
widget: IBaseWidget
|
||||
@@ -83,13 +81,13 @@ function isWidgetShownOnParents(
|
||||
? widget.sourceNodeId
|
||||
: String(widgetNode.id)
|
||||
|
||||
return promotionStore.isPromoted(parent.rootGraph.id, parent.id, {
|
||||
return isWidgetPromotedOnSubgraphNode(parent, {
|
||||
sourceNodeId: interiorNodeId,
|
||||
sourceWidgetName: widget.sourceWidgetName,
|
||||
disambiguatingSourceNodeId: widget.disambiguatingSourceNodeId
|
||||
})
|
||||
}
|
||||
return promotionStore.isPromoted(parent.rootGraph.id, parent.id, {
|
||||
return isWidgetPromotedOnSubgraphNode(parent, {
|
||||
sourceNodeId: String(widgetNode.id),
|
||||
sourceWidgetName: widget.name
|
||||
})
|
||||
|
||||
@@ -14,13 +14,16 @@ import {
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { isPromotedWidgetView } from '@/core/graph/subgraph/promotedWidgetTypes'
|
||||
import { getWidgetName } from '@/core/graph/subgraph/promotionUtils'
|
||||
import {
|
||||
getWidgetName,
|
||||
isWidgetPromotedOnSubgraphNode
|
||||
} from '@/core/graph/subgraph/promotionUtils'
|
||||
import type { SubgraphNode } from '@/lib/litegraph/src/subgraph/SubgraphNode'
|
||||
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import FormSearchInput from '@/renderer/extensions/vueNodes/widgets/components/form/FormSearchInput.vue'
|
||||
import CollapseToggleButton from '@/components/rightSidePanel/layout/CollapseToggleButton.vue'
|
||||
import { DraggableList } from '@/scripts/ui/draggableList'
|
||||
import { usePromotionStore } from '@/stores/promotionStore'
|
||||
import { useRightSidePanelStore } from '@/stores/workspace/rightSidePanelStore'
|
||||
|
||||
import { searchWidgets } from '../shared'
|
||||
@@ -33,7 +36,6 @@ const { node } = defineProps<{
|
||||
|
||||
const { t } = useI18n()
|
||||
const canvasStore = useCanvasStore()
|
||||
const promotionStore = usePromotionStore()
|
||||
const rightSidePanelStore = useRightSidePanelStore()
|
||||
const { focusedSection, searchQuery } = storeToRefs(rightSidePanelStore)
|
||||
|
||||
@@ -55,9 +57,32 @@ const draggableList = ref<DraggableList | undefined>(undefined)
|
||||
const sectionWidgetsRef = useTemplateRef('sectionWidgetsRef')
|
||||
const advancedInputsSectionRef = useTemplateRef('advancedInputsSectionRef')
|
||||
|
||||
const promotionEntries = computed(() =>
|
||||
promotionStore.getPromotions(node.rootGraph.id, node.id)
|
||||
)
|
||||
function isSamePromotedWidget(a: IBaseWidget, b: IBaseWidget): boolean {
|
||||
return (
|
||||
isPromotedWidgetView(a) &&
|
||||
isPromotedWidgetView(b) &&
|
||||
a.sourceNodeId === b.sourceNodeId &&
|
||||
a.sourceWidgetName === b.sourceWidgetName &&
|
||||
a.disambiguatingSourceNodeId === b.disambiguatingSourceNodeId
|
||||
)
|
||||
}
|
||||
|
||||
function getPromotedWidgets(): IBaseWidget[] {
|
||||
const inputWidgets = node.inputs
|
||||
.map((input) => input._widget)
|
||||
.filter((widget): widget is IBaseWidget =>
|
||||
Boolean(widget && isPromotedWidgetView(widget))
|
||||
)
|
||||
const extraWidgets = (node.widgets ?? []).filter(
|
||||
(widget) =>
|
||||
isPromotedWidgetView(widget) &&
|
||||
!inputWidgets.some((inputWidget) =>
|
||||
isSamePromotedWidget(inputWidget, widget)
|
||||
)
|
||||
)
|
||||
|
||||
return [...inputWidgets, ...extraWidgets]
|
||||
}
|
||||
|
||||
watch(
|
||||
focusedSection,
|
||||
@@ -81,37 +106,7 @@ watch(
|
||||
)
|
||||
|
||||
const widgetsList = computed((): NodeWidgetsList => {
|
||||
const entries = promotionEntries.value
|
||||
const { widgets = [] } = node
|
||||
|
||||
const result: NodeWidgetsList = []
|
||||
for (const {
|
||||
sourceNodeId: entryNodeId,
|
||||
sourceWidgetName,
|
||||
disambiguatingSourceNodeId
|
||||
} of entries) {
|
||||
const widget = widgets.find((w) => {
|
||||
if (isPromotedWidgetView(w)) {
|
||||
if (
|
||||
String(w.sourceNodeId) !== entryNodeId ||
|
||||
w.sourceWidgetName !== sourceWidgetName
|
||||
)
|
||||
return false
|
||||
|
||||
if (!disambiguatingSourceNodeId) return true
|
||||
|
||||
return (
|
||||
(w.disambiguatingSourceNodeId ?? w.sourceNodeId) ===
|
||||
disambiguatingSourceNodeId
|
||||
)
|
||||
}
|
||||
return w.name === sourceWidgetName
|
||||
})
|
||||
if (widget) {
|
||||
result.push({ node, widget })
|
||||
}
|
||||
}
|
||||
return result
|
||||
return getPromotedWidgets().map((widget) => ({ node, widget }))
|
||||
})
|
||||
|
||||
const advancedInputsWidgets = computed((): NodeWidgetsList => {
|
||||
@@ -126,7 +121,7 @@ const advancedInputsWidgets = computed((): NodeWidgetsList => {
|
||||
|
||||
return allInteriorWidgets.filter(
|
||||
({ node: interiorNode, widget }) =>
|
||||
!promotionStore.isPromoted(node.rootGraph.id, node.id, {
|
||||
!isWidgetPromotedOnSubgraphNode(node, {
|
||||
sourceNodeId: String(interiorNode.id),
|
||||
sourceWidgetName: getWidgetName(widget),
|
||||
disambiguatingSourceNodeId: isPromotedWidgetView(widget)
|
||||
@@ -190,12 +185,19 @@ function setDraggableState() {
|
||||
this.draggableItem as HTMLElement
|
||||
)
|
||||
|
||||
promotionStore.movePromotion(
|
||||
node.rootGraph.id,
|
||||
node.id,
|
||||
oldPosition,
|
||||
newPosition
|
||||
const subgraphInputs = node.subgraph.inputs
|
||||
const hostInputs = node.inputs
|
||||
|
||||
if (
|
||||
oldPosition >= subgraphInputs.length ||
|
||||
newPosition >= subgraphInputs.length
|
||||
)
|
||||
return
|
||||
|
||||
const [input] = subgraphInputs.splice(oldPosition, 1)
|
||||
if (input) subgraphInputs.splice(newPosition, 0, input)
|
||||
const [nodeInput] = hostInputs.splice(oldPosition, 1)
|
||||
if (nodeInput) hostInputs.splice(newPosition, 0, nodeInput)
|
||||
canvasStore.canvas?.setDirty(true, true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import { useI18n } from 'vue-i18n'
|
||||
|
||||
import MoreButton from '@/components/button/MoreButton.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
import type { PromotedWidgetSource } from '@/core/graph/subgraph/promotedWidgetTypes'
|
||||
import { isPromotedWidgetView } from '@/core/graph/subgraph/promotedWidgetTypes'
|
||||
import {
|
||||
demoteWidget,
|
||||
@@ -17,7 +16,6 @@ import type { SubgraphNode } from '@/lib/litegraph/src/subgraph/SubgraphNode'
|
||||
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import { useNodeDefStore } from '@/stores/nodeDefStore'
|
||||
import { usePromotionStore } from '@/stores/promotionStore'
|
||||
import { useFavoritedWidgetsStore } from '@/stores/workspace/favoritedWidgetsStore'
|
||||
import { getWidgetDefaultValue, promptWidgetLabel } from '@/utils/widgetUtil'
|
||||
import type { WidgetValue } from '@/utils/widgetUtil'
|
||||
@@ -43,13 +41,17 @@ const label = defineModel<string>('label', { required: true })
|
||||
const canvasStore = useCanvasStore()
|
||||
const favoritedWidgetsStore = useFavoritedWidgetsStore()
|
||||
const nodeDefStore = useNodeDefStore()
|
||||
const promotionStore = usePromotionStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
const hasParents = computed(() => parents?.length > 0)
|
||||
const isLinked = computed(() => {
|
||||
if (!node.isSubgraphNode() || !isPromotedWidgetView(widget)) return false
|
||||
return isLinkedPromotion(node, widget.sourceNodeId, widget.sourceWidgetName)
|
||||
return isLinkedPromotion(
|
||||
node,
|
||||
widget.sourceNodeId,
|
||||
widget.sourceWidgetName,
|
||||
widget.disambiguatingSourceNodeId
|
||||
)
|
||||
})
|
||||
const canToggleVisibility = computed(() => hasParents.value && !isLinked.value)
|
||||
const favoriteNode = computed(() =>
|
||||
@@ -82,16 +84,19 @@ function handleHideInput() {
|
||||
|
||||
if (isPromotedWidgetView(widget)) {
|
||||
for (const parent of parents) {
|
||||
const source: PromotedWidgetSource = {
|
||||
sourceNodeId:
|
||||
String(node.id) === String(parent.id)
|
||||
? widget.sourceNodeId
|
||||
: String(node.id),
|
||||
sourceWidgetName: widget.sourceWidgetName,
|
||||
disambiguatingSourceNodeId: widget.disambiguatingSourceNodeId
|
||||
}
|
||||
promotionStore.demote(parent.rootGraph.id, parent.id, source)
|
||||
parent.computeSize(parent.size)
|
||||
const sourceNodeId =
|
||||
String(node.id) === String(parent.id)
|
||||
? widget.sourceNodeId
|
||||
: String(node.id)
|
||||
demoteWidget(
|
||||
{
|
||||
id: sourceNodeId,
|
||||
title: node.title,
|
||||
type: node.type
|
||||
},
|
||||
widget,
|
||||
[parent]
|
||||
)
|
||||
}
|
||||
canvasStore.canvas?.setDirty(true, true)
|
||||
} else {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed, onMounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import DraggableList from '@/components/common/DraggableList.vue'
|
||||
import Button from '@/components/ui/button/Button.vue'
|
||||
@@ -22,24 +21,18 @@ import { SubgraphNode } from '@/lib/litegraph/src/subgraph/SubgraphNode'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import FormSearchInput from '@/renderer/extensions/vueNodes/widgets/components/form/FormSearchInput.vue'
|
||||
import { useLitegraphService } from '@/services/litegraphService'
|
||||
import { usePromotionStore } from '@/stores/promotionStore'
|
||||
import { usePreviewExposureStore } from '@/stores/previewExposureStore'
|
||||
import { useRightSidePanelStore } from '@/stores/workspace/rightSidePanelStore'
|
||||
import { createNodeLocatorId } from '@/types/nodeIdentification'
|
||||
import { cn } from '@comfyorg/tailwind-utils'
|
||||
|
||||
import SubgraphNodeWidget from './SubgraphNodeWidget.vue'
|
||||
|
||||
const { t } = useI18n()
|
||||
const canvasStore = useCanvasStore()
|
||||
const promotionStore = usePromotionStore()
|
||||
const previewExposureStore = usePreviewExposureStore()
|
||||
const rightSidePanelStore = useRightSidePanelStore()
|
||||
const { searchQuery } = storeToRefs(rightSidePanelStore)
|
||||
|
||||
const promotionEntries = computed(() => {
|
||||
const node = activeNode.value
|
||||
if (!node) return []
|
||||
return promotionStore.getPromotions(node.rootGraph.id, node.id)
|
||||
})
|
||||
|
||||
const activeNode = computed(() => {
|
||||
const node = canvasStore.selectedItems[0]
|
||||
if (node instanceof SubgraphNode) return node
|
||||
@@ -51,34 +44,26 @@ const activeWidgets = computed<WidgetItem[]>({
|
||||
const node = activeNode.value
|
||||
if (!node) return []
|
||||
|
||||
return promotionEntries.value.flatMap(
|
||||
({
|
||||
sourceNodeId,
|
||||
sourceWidgetName,
|
||||
disambiguatingSourceNodeId
|
||||
}): WidgetItem[] => {
|
||||
if (sourceNodeId === '-1') {
|
||||
const widget = node.widgets.find((w) => w.name === sourceWidgetName)
|
||||
if (!widget) return []
|
||||
return [
|
||||
[{ id: -1, title: t('subgraphStore.linked'), type: '' }, widget]
|
||||
]
|
||||
}
|
||||
const wNode = node.subgraph._nodes_by_id[sourceNodeId]
|
||||
if (!wNode) return []
|
||||
const widget = getPromotableWidgets(wNode).find((w) => {
|
||||
if (w.name !== sourceWidgetName) return false
|
||||
if (disambiguatingSourceNodeId && isPromotedWidgetView(w))
|
||||
return (
|
||||
(w.disambiguatingSourceNodeId ?? w.sourceNodeId) ===
|
||||
disambiguatingSourceNodeId
|
||||
)
|
||||
return true
|
||||
})
|
||||
if (!widget) return []
|
||||
return [[wNode, widget]]
|
||||
}
|
||||
)
|
||||
const linkedWidgets = node.widgets.flatMap((widget): WidgetItem[] => {
|
||||
if (!isPromotedWidgetView(widget)) return []
|
||||
const sourceNode = node.subgraph._nodes_by_id[widget.sourceNodeId]
|
||||
if (!sourceNode) return []
|
||||
return [[sourceNode, widget]]
|
||||
})
|
||||
|
||||
const hostLocator = createNodeLocatorId(node.rootGraph.id, node.id)
|
||||
const previewWidgets = previewExposureStore
|
||||
.getExposures(node.rootGraph.id, hostLocator)
|
||||
.flatMap((exposure): WidgetItem[] => {
|
||||
const sourceNode = node.subgraph._nodes_by_id[exposure.sourceNodeId]
|
||||
if (!sourceNode) return []
|
||||
const widget = getPromotableWidgets(sourceNode).find(
|
||||
(candidate) => candidate.name === exposure.sourcePreviewName
|
||||
)
|
||||
return widget ? [[sourceNode, widget]] : []
|
||||
})
|
||||
|
||||
return [...linkedWidgets, ...previewWidgets]
|
||||
},
|
||||
set(value: WidgetItem[]) {
|
||||
const node = activeNode.value
|
||||
@@ -86,17 +71,15 @@ const activeWidgets = computed<WidgetItem[]>({
|
||||
console.error('Attempted to toggle widgets with no node selected')
|
||||
return
|
||||
}
|
||||
promotionStore.setPromotions(
|
||||
node.rootGraph.id,
|
||||
node.id,
|
||||
value.map(([n, w]) => ({
|
||||
sourceNodeId: String(n.id),
|
||||
sourceWidgetName: getWidgetName(w),
|
||||
disambiguatingSourceNodeId: isPromotedWidgetView(w)
|
||||
? w.disambiguatingSourceNodeId
|
||||
: undefined
|
||||
}))
|
||||
)
|
||||
const currentItems = activeWidgets.value
|
||||
const currentKeys = new Set(currentItems.map(toKey))
|
||||
const nextKeys = new Set(value.map(toKey))
|
||||
for (const item of value) {
|
||||
if (!currentKeys.has(toKey(item))) promote(item)
|
||||
}
|
||||
for (const item of currentItems) {
|
||||
if (!nextKeys.has(toKey(item))) demote(item)
|
||||
}
|
||||
refreshPromotedWidgetRendering()
|
||||
}
|
||||
})
|
||||
@@ -119,14 +102,8 @@ const candidateWidgets = computed<WidgetItem[]>(() => {
|
||||
const node = activeNode.value
|
||||
if (!node) return []
|
||||
return interiorWidgets.value.filter(
|
||||
([n, w]: WidgetItem) =>
|
||||
!promotionStore.isPromoted(node.rootGraph.id, node.id, {
|
||||
sourceNodeId: String(n.id),
|
||||
sourceWidgetName: getWidgetName(w),
|
||||
disambiguatingSourceNodeId: isPromotedWidgetView(w)
|
||||
? w.disambiguatingSourceNodeId
|
||||
: undefined
|
||||
})
|
||||
(item: WidgetItem) =>
|
||||
!activeWidgets.value.some((active) => toKey(active) === toKey(item))
|
||||
)
|
||||
})
|
||||
const filteredCandidates = computed<WidgetItem[]>(() => {
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
createTestSubgraphNode
|
||||
} from '@/lib/litegraph/src/subgraph/__fixtures__/subgraphHelpers'
|
||||
import { useNodeOutputStore } from '@/stores/nodeOutputStore'
|
||||
import { usePromotionStore } from '@/stores/promotionStore'
|
||||
import { usePreviewExposureStore } from '@/stores/previewExposureStore'
|
||||
import { createNodeLocatorId } from '@/types/nodeIdentification'
|
||||
|
||||
import { usePromotedPreviews } from './usePromotedPreviews'
|
||||
@@ -83,6 +83,18 @@ function seedPreviewImages(
|
||||
}
|
||||
}
|
||||
|
||||
function exposePreview(
|
||||
setup: ReturnType<typeof createSetup>,
|
||||
sourceNodeId: string,
|
||||
sourcePreviewName = '$$canvas-image-preview'
|
||||
) {
|
||||
usePreviewExposureStore().addExposure(
|
||||
setup.subgraphNode.rootGraph.id,
|
||||
createNodeLocatorId(setup.subgraphNode.rootGraph.id, setup.subgraphNode.id),
|
||||
{ sourceNodeId, sourcePreviewName }
|
||||
)
|
||||
}
|
||||
|
||||
describe(usePromotedPreviews, () => {
|
||||
let nodeOutputStore: MockNodeOutputStore
|
||||
|
||||
@@ -109,11 +121,6 @@ describe(usePromotedPreviews, () => {
|
||||
it('returns empty array when no $$ promotions exist', () => {
|
||||
const setup = createSetup()
|
||||
addInteriorNode(setup, { id: 10 })
|
||||
usePromotionStore().promote(
|
||||
setup.subgraphNode.rootGraph.id,
|
||||
setup.subgraphNode.id,
|
||||
{ sourceNodeId: '10', sourceWidgetName: 'seed' }
|
||||
)
|
||||
|
||||
const { promotedPreviews } = usePromotedPreviews(() => setup.subgraphNode)
|
||||
expect(promotedPreviews.value).toEqual([])
|
||||
@@ -122,11 +129,7 @@ describe(usePromotedPreviews, () => {
|
||||
it('returns image preview for promoted $$ widget with outputs', () => {
|
||||
const setup = createSetup()
|
||||
addInteriorNode(setup, { id: 10, previewMediaType: 'image' })
|
||||
usePromotionStore().promote(
|
||||
setup.subgraphNode.rootGraph.id,
|
||||
setup.subgraphNode.id,
|
||||
{ sourceNodeId: '10', sourceWidgetName: '$$canvas-image-preview' }
|
||||
)
|
||||
exposePreview(setup, '10')
|
||||
|
||||
const mockUrls = ['/view?filename=output.png']
|
||||
seedOutputs(setup.subgraph.id, [10])
|
||||
@@ -146,11 +149,7 @@ describe(usePromotedPreviews, () => {
|
||||
it('returns video type when interior node has video previewMediaType', () => {
|
||||
const setup = createSetup()
|
||||
addInteriorNode(setup, { id: 10, previewMediaType: 'video' })
|
||||
usePromotionStore().promote(
|
||||
setup.subgraphNode.rootGraph.id,
|
||||
setup.subgraphNode.id,
|
||||
{ sourceNodeId: '10', sourceWidgetName: '$$canvas-image-preview' }
|
||||
)
|
||||
exposePreview(setup, '10')
|
||||
|
||||
seedOutputs(setup.subgraph.id, [10])
|
||||
getNodeImageUrls.mockReturnValue(['/view?filename=output.webm'])
|
||||
@@ -162,11 +161,7 @@ describe(usePromotedPreviews, () => {
|
||||
it('returns audio type when interior node has audio previewMediaType', () => {
|
||||
const setup = createSetup()
|
||||
addInteriorNode(setup, { id: 10, previewMediaType: 'audio' })
|
||||
usePromotionStore().promote(
|
||||
setup.subgraphNode.rootGraph.id,
|
||||
setup.subgraphNode.id,
|
||||
{ sourceNodeId: '10', sourceWidgetName: '$$canvas-image-preview' }
|
||||
)
|
||||
exposePreview(setup, '10')
|
||||
|
||||
seedOutputs(setup.subgraph.id, [10])
|
||||
getNodeImageUrls.mockReturnValue(['/view?filename=output.mp3'])
|
||||
@@ -185,16 +180,8 @@ describe(usePromotedPreviews, () => {
|
||||
id: 20,
|
||||
previewMediaType: 'image'
|
||||
})
|
||||
usePromotionStore().promote(
|
||||
setup.subgraphNode.rootGraph.id,
|
||||
setup.subgraphNode.id,
|
||||
{ sourceNodeId: '10', sourceWidgetName: '$$canvas-image-preview' }
|
||||
)
|
||||
usePromotionStore().promote(
|
||||
setup.subgraphNode.rootGraph.id,
|
||||
setup.subgraphNode.id,
|
||||
{ sourceNodeId: '20', sourceWidgetName: '$$canvas-image-preview' }
|
||||
)
|
||||
exposePreview(setup, '10')
|
||||
exposePreview(setup, '20')
|
||||
|
||||
seedOutputs(setup.subgraph.id, [10, 20])
|
||||
getNodeImageUrls.mockImplementation((node: LGraphNode) => {
|
||||
@@ -212,11 +199,7 @@ describe(usePromotedPreviews, () => {
|
||||
it('returns preview when only nodePreviewImages exist (e.g. GLSL live preview)', () => {
|
||||
const setup = createSetup()
|
||||
addInteriorNode(setup, { id: 10, previewMediaType: 'image' })
|
||||
usePromotionStore().promote(
|
||||
setup.subgraphNode.rootGraph.id,
|
||||
setup.subgraphNode.id,
|
||||
{ sourceNodeId: '10', sourceWidgetName: '$$canvas-image-preview' }
|
||||
)
|
||||
exposePreview(setup, '10')
|
||||
|
||||
const blobUrl = 'blob:http://localhost/glsl-preview'
|
||||
seedPreviewImages(setup.subgraph.id, [{ nodeId: 10, urls: [blobUrl] }])
|
||||
@@ -236,11 +219,7 @@ describe(usePromotedPreviews, () => {
|
||||
it('recomputes when preview images are populated after first evaluation', () => {
|
||||
const setup = createSetup()
|
||||
addInteriorNode(setup, { id: 10, previewMediaType: 'image' })
|
||||
usePromotionStore().promote(
|
||||
setup.subgraphNode.rootGraph.id,
|
||||
setup.subgraphNode.id,
|
||||
{ sourceNodeId: '10', sourceWidgetName: '$$canvas-image-preview' }
|
||||
)
|
||||
exposePreview(setup, '10')
|
||||
|
||||
const { promotedPreviews } = usePromotedPreviews(() => setup.subgraphNode)
|
||||
expect(promotedPreviews.value).toEqual([])
|
||||
@@ -262,11 +241,7 @@ describe(usePromotedPreviews, () => {
|
||||
it('skips interior nodes with no image output', () => {
|
||||
const setup = createSetup()
|
||||
addInteriorNode(setup, { id: 10 })
|
||||
usePromotionStore().promote(
|
||||
setup.subgraphNode.rootGraph.id,
|
||||
setup.subgraphNode.id,
|
||||
{ sourceNodeId: '10', sourceWidgetName: '$$canvas-image-preview' }
|
||||
)
|
||||
exposePreview(setup, '10')
|
||||
|
||||
const { promotedPreviews } = usePromotedPreviews(() => setup.subgraphNode)
|
||||
expect(promotedPreviews.value).toEqual([])
|
||||
@@ -274,29 +249,16 @@ describe(usePromotedPreviews, () => {
|
||||
|
||||
it('skips missing interior nodes', () => {
|
||||
const setup = createSetup()
|
||||
usePromotionStore().promote(
|
||||
setup.subgraphNode.rootGraph.id,
|
||||
setup.subgraphNode.id,
|
||||
{ sourceNodeId: '99', sourceWidgetName: '$$canvas-image-preview' }
|
||||
)
|
||||
exposePreview(setup, '99')
|
||||
|
||||
const { promotedPreviews } = usePromotedPreviews(() => setup.subgraphNode)
|
||||
expect(promotedPreviews.value).toEqual([])
|
||||
})
|
||||
|
||||
it('ignores non-$$ promoted widgets', () => {
|
||||
it('uses preview exposures by source preview name', () => {
|
||||
const setup = createSetup()
|
||||
addInteriorNode(setup, { id: 10 })
|
||||
usePromotionStore().promote(
|
||||
setup.subgraphNode.rootGraph.id,
|
||||
setup.subgraphNode.id,
|
||||
{ sourceNodeId: '10', sourceWidgetName: 'seed' }
|
||||
)
|
||||
usePromotionStore().promote(
|
||||
setup.subgraphNode.rootGraph.id,
|
||||
setup.subgraphNode.id,
|
||||
{ sourceNodeId: '10', sourceWidgetName: '$$canvas-image-preview' }
|
||||
)
|
||||
exposePreview(setup, '10')
|
||||
|
||||
const mockUrls = ['/view?filename=img.png']
|
||||
seedOutputs(setup.subgraph.id, [10])
|
||||
|
||||
@@ -5,7 +5,6 @@ import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
|
||||
import { SubgraphNode } from '@/lib/litegraph/src/subgraph/SubgraphNode'
|
||||
import { useNodeOutputStore } from '@/stores/nodeOutputStore'
|
||||
import { usePreviewExposureStore } from '@/stores/previewExposureStore'
|
||||
import { usePromotionStore } from '@/stores/promotionStore'
|
||||
import { createNodeLocatorId } from '@/types/nodeIdentification'
|
||||
|
||||
interface PromotedPreview {
|
||||
@@ -20,17 +19,13 @@ interface PromotedPreview {
|
||||
/**
|
||||
* Returns reactive preview media exposed by a host SubgraphNode.
|
||||
*
|
||||
* Reads first from the host-scoped {@link usePreviewExposureStore} (the
|
||||
* canonical post-ADR-0009 source). Falls back to the legacy promotion-store
|
||||
* `$$`-prefixed entries when no exposures have been migrated yet — the
|
||||
* configure-time flush will normalize both into the exposure store on next
|
||||
* load.
|
||||
* Reads from the host-scoped {@link usePreviewExposureStore}, the canonical
|
||||
* post-ADR-0009 source for display-only preview promotion.
|
||||
*/
|
||||
export function usePromotedPreviews(
|
||||
lgraphNode: MaybeRefOrGetter<LGraphNode | null | undefined>
|
||||
) {
|
||||
const previewExposureStore = usePreviewExposureStore()
|
||||
const promotionStore = usePromotionStore()
|
||||
const nodeOutputStore = useNodeOutputStore()
|
||||
|
||||
const promotedPreviews = computed((): PromotedPreview[] => {
|
||||
@@ -45,18 +40,10 @@ export function usePromotedPreviews(
|
||||
hostLocator
|
||||
)
|
||||
|
||||
const exposurePairs = exposures.length
|
||||
? exposures.map((exposure) => ({
|
||||
sourceNodeId: exposure.sourceNodeId,
|
||||
sourceWidgetName: exposure.sourcePreviewName
|
||||
}))
|
||||
: promotionStore
|
||||
.getPromotions(rootGraphId, node.id)
|
||||
.filter((entry) => entry.sourceWidgetName.startsWith('$$'))
|
||||
.map((entry) => ({
|
||||
sourceNodeId: entry.sourceNodeId,
|
||||
sourceWidgetName: entry.sourceWidgetName
|
||||
}))
|
||||
const exposurePairs = exposures.map((exposure) => ({
|
||||
sourceNodeId: exposure.sourceNodeId,
|
||||
sourceWidgetName: exposure.sourcePreviewName
|
||||
}))
|
||||
|
||||
if (!exposurePairs.length) return []
|
||||
|
||||
|
||||
@@ -39,9 +39,14 @@ function buildHost(): SubgraphNode {
|
||||
|
||||
function makeSource(
|
||||
sourceNodeId: string,
|
||||
sourceWidgetName: string
|
||||
sourceWidgetName: string,
|
||||
disambiguatingSourceNodeId?: string
|
||||
): PromotedWidgetSource {
|
||||
return { sourceNodeId, sourceWidgetName }
|
||||
return {
|
||||
sourceNodeId,
|
||||
sourceWidgetName,
|
||||
...(disambiguatingSourceNodeId && { disambiguatingSourceNodeId })
|
||||
}
|
||||
}
|
||||
|
||||
describe(classifyProxyEntry, () => {
|
||||
@@ -73,6 +78,42 @@ describe(classifyProxyEntry, () => {
|
||||
subgraphInputName: 'seed_link'
|
||||
})
|
||||
})
|
||||
|
||||
it('matches already-linked inputs by disambiguatingSourceNodeId when provided', () => {
|
||||
const host = buildHost()
|
||||
const innerNode = new LGraphNode('Inner')
|
||||
innerNode.addWidget('number', 'seed', 0, () => {})
|
||||
host.subgraph.add(innerNode)
|
||||
|
||||
const firstInput = host.addInput('first_seed', '*')
|
||||
firstInput._widget = fromPartial<PromotedWidgetView>({
|
||||
node: host,
|
||||
name: 'seed',
|
||||
sourceNodeId: String(innerNode.id),
|
||||
sourceWidgetName: 'seed',
|
||||
disambiguatingSourceNodeId: 'first'
|
||||
})
|
||||
const secondInput = host.addInput('second_seed', '*')
|
||||
secondInput._widget = fromPartial<PromotedWidgetView>({
|
||||
node: host,
|
||||
name: 'seed',
|
||||
sourceNodeId: String(innerNode.id),
|
||||
sourceWidgetName: 'seed',
|
||||
disambiguatingSourceNodeId: 'second'
|
||||
})
|
||||
|
||||
const normalized = makeSource(String(innerNode.id), 'seed', 'second')
|
||||
const result = classifyProxyEntry({
|
||||
hostNode: host,
|
||||
normalized,
|
||||
cohort: [normalized]
|
||||
})
|
||||
|
||||
expect(result.plan).toEqual({
|
||||
kind: 'alreadyLinked',
|
||||
subgraphInputName: 'second_seed'
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('quarantine branches', () => {
|
||||
|
||||
@@ -35,7 +35,10 @@ function findLinkedSubgraphInputName(
|
||||
if (!widget || !isPromotedWidgetView(widget)) continue
|
||||
if (
|
||||
widget.sourceNodeId === normalized.sourceNodeId &&
|
||||
widget.sourceWidgetName === normalized.sourceWidgetName
|
||||
widget.sourceWidgetName === normalized.sourceWidgetName &&
|
||||
(!normalized.disambiguatingSourceNodeId ||
|
||||
widget.disambiguatingSourceNodeId ===
|
||||
normalized.disambiguatingSourceNodeId)
|
||||
) {
|
||||
return input.name
|
||||
}
|
||||
|
||||
@@ -108,6 +108,34 @@ describe(migratePreviewExposure, () => {
|
||||
expect(store.getExposures(host.rootGraph.id, locator)).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('reuses an existing exposure for the same source preview', () => {
|
||||
const host = buildHost()
|
||||
const innerNode = new LGraphNode('Inner')
|
||||
host.subgraph.add(innerNode)
|
||||
|
||||
const store = usePreviewExposureStore()
|
||||
const locator = createNodeLocatorId(host.rootGraph.id, host.id)
|
||||
store.addExposure(host.rootGraph.id, locator, {
|
||||
sourceNodeId: String(innerNode.id),
|
||||
sourcePreviewName: '$$canvas-image-preview'
|
||||
})
|
||||
|
||||
const result = migratePreviewExposure({
|
||||
hostNode: host,
|
||||
entry: buildEntry({
|
||||
sourceNodeId: String(innerNode.id),
|
||||
sourcePreviewName: '$$canvas-image-preview'
|
||||
}),
|
||||
store
|
||||
})
|
||||
|
||||
expect(result).toEqual({
|
||||
ok: true,
|
||||
previewName: '$$canvas-image-preview'
|
||||
})
|
||||
expect(store.getExposures(host.rootGraph.id, locator)).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('returns missingSourceNode when the source node is absent', () => {
|
||||
const host = buildHost()
|
||||
const store = usePreviewExposureStore()
|
||||
|
||||
@@ -54,6 +54,15 @@ export function migratePreviewExposure(
|
||||
hostNode.rootGraph.id,
|
||||
hostNode.id
|
||||
)
|
||||
const existing = store
|
||||
.getExposures(hostNode.rootGraph.id, hostNodeLocator)
|
||||
.find(
|
||||
(exposure) =>
|
||||
exposure.sourceNodeId === entry.normalized.sourceNodeId &&
|
||||
exposure.sourcePreviewName === plan.sourcePreviewName
|
||||
)
|
||||
if (existing) return { ok: true, previewName: existing.name }
|
||||
|
||||
const added = store.addExposure(hostNode.rootGraph.id, hostNodeLocator, {
|
||||
sourceNodeId: entry.normalized.sourceNodeId,
|
||||
sourcePreviewName: plan.sourcePreviewName
|
||||
|
||||
@@ -39,13 +39,17 @@ function buildHost(): SubgraphNode {
|
||||
function buildEntry(args: {
|
||||
sourceNodeId: string
|
||||
sourceWidgetName: string
|
||||
disambiguatingSourceNodeId?: string
|
||||
plan: PendingMigrationEntry['plan']
|
||||
hostValue?: PendingMigrationEntry['hostValue']
|
||||
}): PendingMigrationEntry {
|
||||
return {
|
||||
normalized: {
|
||||
sourceNodeId: args.sourceNodeId,
|
||||
sourceWidgetName: args.sourceWidgetName
|
||||
sourceWidgetName: args.sourceWidgetName,
|
||||
...(args.disambiguatingSourceNodeId && {
|
||||
disambiguatingSourceNodeId: args.disambiguatingSourceNodeId
|
||||
})
|
||||
},
|
||||
legacyOrderIndex: 0,
|
||||
hostValue: args.hostValue ?? HOST_VALUE_HOLE,
|
||||
@@ -113,6 +117,47 @@ describe(repairValueWidget, () => {
|
||||
expect(inputSlot._widget?.value).toBe(7)
|
||||
})
|
||||
|
||||
it('applies host value to the linked input with the matching disambiguator', () => {
|
||||
const host = buildHost()
|
||||
const innerNode = new LGraphNode('Inner')
|
||||
innerNode.addWidget('number', 'seed', 0, () => {})
|
||||
host.subgraph.add(innerNode)
|
||||
|
||||
const firstInput = host.addInput('first_seed', '*')
|
||||
firstInput._widget = fromPartial<PromotedWidgetView>({
|
||||
node: host,
|
||||
name: 'seed',
|
||||
sourceNodeId: String(innerNode.id),
|
||||
sourceWidgetName: 'seed',
|
||||
disambiguatingSourceNodeId: 'first',
|
||||
value: 1
|
||||
})
|
||||
const secondInput = host.addInput('second_seed', '*')
|
||||
secondInput._widget = fromPartial<PromotedWidgetView>({
|
||||
node: host,
|
||||
name: 'seed',
|
||||
sourceNodeId: String(innerNode.id),
|
||||
sourceWidgetName: 'seed',
|
||||
disambiguatingSourceNodeId: 'second',
|
||||
value: 2
|
||||
})
|
||||
|
||||
const result = repairValueWidget({
|
||||
hostNode: host,
|
||||
entry: buildEntry({
|
||||
sourceNodeId: String(innerNode.id),
|
||||
sourceWidgetName: 'seed',
|
||||
disambiguatingSourceNodeId: 'second',
|
||||
plan: { kind: 'alreadyLinked', subgraphInputName: 'second_seed' },
|
||||
hostValue: 99
|
||||
})
|
||||
})
|
||||
|
||||
expect(result).toEqual({ ok: true, subgraphInputName: 'second_seed' })
|
||||
expect(firstInput._widget?.value).toBe(1)
|
||||
expect(secondInput._widget?.value).toBe(99)
|
||||
})
|
||||
|
||||
it('returns missingSubgraphInput when the linked SubgraphInput is gone', () => {
|
||||
const host = buildHost()
|
||||
const innerNode = new LGraphNode('Inner')
|
||||
|
||||
@@ -24,14 +24,17 @@ interface RepairValueWidgetArgs {
|
||||
function findHostInputForLinkedSource(
|
||||
hostNode: SubgraphNode,
|
||||
sourceNodeId: string,
|
||||
sourceWidgetName: string
|
||||
sourceWidgetName: string,
|
||||
disambiguatingSourceNodeId?: string
|
||||
) {
|
||||
return hostNode.inputs.find((input) => {
|
||||
const widget = input._widget
|
||||
if (!widget || !isPromotedWidgetView(widget)) return false
|
||||
return (
|
||||
widget.sourceNodeId === sourceNodeId &&
|
||||
widget.sourceWidgetName === sourceWidgetName
|
||||
widget.sourceWidgetName === sourceWidgetName &&
|
||||
(!disambiguatingSourceNodeId ||
|
||||
widget.disambiguatingSourceNodeId === disambiguatingSourceNodeId)
|
||||
)
|
||||
})
|
||||
}
|
||||
@@ -51,7 +54,8 @@ function repairAlreadyLinked(
|
||||
const hostInput = findHostInputForLinkedSource(
|
||||
hostNode,
|
||||
entry.normalized.sourceNodeId,
|
||||
entry.normalized.sourceWidgetName
|
||||
entry.normalized.sourceWidgetName,
|
||||
entry.normalized.disambiguatingSourceNodeId
|
||||
)
|
||||
if (!hostInput?._widget) {
|
||||
return { ok: false, reason: 'missingSubgraphInput' }
|
||||
|
||||
@@ -9,7 +9,13 @@ import {
|
||||
createTestSubgraphNode
|
||||
} from '@/lib/litegraph/src/subgraph/__fixtures__/subgraphHelpers'
|
||||
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
|
||||
import { usePromotionStore } from '@/stores/promotionStore'
|
||||
import { usePreviewExposureStore } from '@/stores/previewExposureStore'
|
||||
import { createNodeLocatorId } from '@/types/nodeIdentification'
|
||||
|
||||
type TestPromotedWidget = IBaseWidget & {
|
||||
sourceNodeId: string
|
||||
sourceWidgetName: string
|
||||
}
|
||||
|
||||
const updatePreviewsMock = vi.hoisted(() => vi.fn())
|
||||
vi.mock('@/services/litegraphService', () => ({
|
||||
@@ -22,6 +28,7 @@ import {
|
||||
hasUnpromotedWidgets,
|
||||
isLinkedPromotion,
|
||||
isPreviewPseudoWidget,
|
||||
promoteValueWidgetViaSubgraphInput,
|
||||
promoteRecommendedWidgets,
|
||||
pruneDisconnected
|
||||
} from './promotionUtils'
|
||||
@@ -112,58 +119,67 @@ describe('pruneDisconnected', () => {
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
it('removes disconnected entries and emits a dev warning', () => {
|
||||
it('removes disconnected linked inputs and emits a dev warning', () => {
|
||||
const subgraph = createTestSubgraph()
|
||||
const subgraphNode = createTestSubgraphNode(subgraph)
|
||||
const interiorNode = new LGraphNode('TestNode')
|
||||
subgraphNode.subgraph.add(interiorNode)
|
||||
interiorNode.addWidget('text', 'kept', 'value', () => {})
|
||||
const keptInput = interiorNode.addInput('kept', 'STRING')
|
||||
const keptWidget = interiorNode.addWidget('text', 'kept', 'value', () => {})
|
||||
keptInput.widget = { name: keptWidget.name }
|
||||
promoteValueWidgetViaSubgraphInput(subgraphNode, interiorNode, keptWidget)
|
||||
|
||||
const store = usePromotionStore()
|
||||
store.setPromotions(subgraphNode.rootGraph.id, subgraphNode.id, [
|
||||
{ sourceNodeId: String(interiorNode.id), sourceWidgetName: 'kept' },
|
||||
{
|
||||
sourceNodeId: String(interiorNode.id),
|
||||
sourceWidgetName: 'missing-widget'
|
||||
},
|
||||
{ sourceNodeId: '9999', sourceWidgetName: 'missing-node' }
|
||||
])
|
||||
const missingWidgetInput = subgraph.addInput('missing-widget', 'STRING')
|
||||
missingWidgetInput._widget = fromPartial<TestPromotedWidget>({
|
||||
sourceNodeId: String(interiorNode.id),
|
||||
sourceWidgetName: 'missing-widget'
|
||||
})
|
||||
const missingNodeInput = subgraph.addInput('missing-node', 'STRING')
|
||||
missingNodeInput._widget = fromPartial<TestPromotedWidget>({
|
||||
sourceNodeId: '9999',
|
||||
sourceWidgetName: 'missing-node'
|
||||
})
|
||||
|
||||
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
||||
|
||||
pruneDisconnected(subgraphNode)
|
||||
|
||||
expect(
|
||||
store.getPromotions(subgraphNode.rootGraph.id, subgraphNode.id)
|
||||
).toEqual([
|
||||
{ sourceNodeId: String(interiorNode.id), sourceWidgetName: 'kept' }
|
||||
])
|
||||
expect(subgraph.inputs.map((input) => input.name)).toEqual(['kept'])
|
||||
expect(warnSpy).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('keeps virtual canvas preview promotions for PreviewImage nodes', () => {
|
||||
it('does not prune preview exposures for PreviewImage nodes', () => {
|
||||
const subgraph = createTestSubgraph()
|
||||
const subgraphNode = createTestSubgraphNode(subgraph)
|
||||
const interiorNode = new LGraphNode('PreviewImage')
|
||||
interiorNode.type = 'PreviewImage'
|
||||
subgraphNode.subgraph.add(interiorNode)
|
||||
|
||||
const store = usePromotionStore()
|
||||
store.setPromotions(subgraphNode.rootGraph.id, subgraphNode.id, [
|
||||
const hostLocator = createNodeLocatorId(
|
||||
subgraphNode.rootGraph.id,
|
||||
subgraphNode.id
|
||||
)
|
||||
usePreviewExposureStore().addExposure(
|
||||
subgraphNode.rootGraph.id,
|
||||
hostLocator,
|
||||
{
|
||||
sourceNodeId: String(interiorNode.id),
|
||||
sourceWidgetName: CANVAS_IMAGE_PREVIEW_WIDGET
|
||||
sourcePreviewName: CANVAS_IMAGE_PREVIEW_WIDGET
|
||||
}
|
||||
])
|
||||
)
|
||||
|
||||
pruneDisconnected(subgraphNode)
|
||||
|
||||
expect(
|
||||
store.getPromotions(subgraphNode.rootGraph.id, subgraphNode.id)
|
||||
usePreviewExposureStore().getExposures(
|
||||
subgraphNode.rootGraph.id,
|
||||
hostLocator
|
||||
)
|
||||
).toEqual([
|
||||
{
|
||||
name: CANVAS_IMAGE_PREVIEW_WIDGET,
|
||||
sourceNodeId: String(interiorNode.id),
|
||||
sourceWidgetName: CANVAS_IMAGE_PREVIEW_WIDGET
|
||||
sourcePreviewName: CANVAS_IMAGE_PREVIEW_WIDGET
|
||||
}
|
||||
])
|
||||
})
|
||||
@@ -232,6 +248,53 @@ describe('promoteRecommendedWidgets', () => {
|
||||
updatePreviewsMock.mockReset()
|
||||
})
|
||||
|
||||
it('promotes recommended value widgets through linked subgraph inputs', () => {
|
||||
const subgraph = createTestSubgraph()
|
||||
const subgraphNode = createTestSubgraphNode(subgraph)
|
||||
const interiorNode = new LGraphNode('Sampler')
|
||||
const input = interiorNode.addInput('seed', 'INT')
|
||||
const seedWidget = interiorNode.addWidget('number', 'seed', 123, () => {})
|
||||
input.widget = { name: seedWidget.name }
|
||||
subgraph.add(interiorNode)
|
||||
|
||||
promoteRecommendedWidgets(subgraphNode)
|
||||
|
||||
const linkedInput = subgraph.inputs.find((slot) => slot.name === 'seed')
|
||||
expect(linkedInput).toBeDefined()
|
||||
expect(input.link).not.toBeNull()
|
||||
expect(linkedInput?.linkIds).toContain(input.link)
|
||||
expect(subgraphNode.serialize().properties?.proxyWidgets).toBeUndefined()
|
||||
})
|
||||
|
||||
it('promotes virtual previews through preview exposures', () => {
|
||||
const subgraph = createTestSubgraph()
|
||||
const subgraphNode = createTestSubgraphNode(subgraph)
|
||||
const glslNode = new LGraphNode('GLSLShader')
|
||||
glslNode.type = 'GLSLShader'
|
||||
subgraph.add(glslNode)
|
||||
|
||||
promoteRecommendedWidgets(subgraphNode)
|
||||
|
||||
const hostLocator = createNodeLocatorId(
|
||||
subgraphNode.rootGraph.id,
|
||||
subgraphNode.id
|
||||
)
|
||||
expect(
|
||||
usePreviewExposureStore().getExposures(
|
||||
subgraphNode.rootGraph.id,
|
||||
hostLocator
|
||||
)
|
||||
).toEqual([
|
||||
{
|
||||
name: CANVAS_IMAGE_PREVIEW_WIDGET,
|
||||
sourceNodeId: String(glslNode.id),
|
||||
sourcePreviewName: CANVAS_IMAGE_PREVIEW_WIDGET
|
||||
}
|
||||
])
|
||||
expect(subgraph.inputs).toHaveLength(0)
|
||||
expect(subgraphNode.serialize().properties?.proxyWidgets).toBeUndefined()
|
||||
})
|
||||
|
||||
it('skips deferred updatePreviews when a preview widget already exists', () => {
|
||||
const subgraph = createTestSubgraph()
|
||||
const subgraphNode = createTestSubgraphNode(subgraph)
|
||||
@@ -252,7 +315,7 @@ describe('promoteRecommendedWidgets', () => {
|
||||
expect(updatePreviewsMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('eagerly promotes virtual preview widget for CANVAS_IMAGE_PREVIEW nodes', () => {
|
||||
it('eagerly exposes virtual preview widget for CANVAS_IMAGE_PREVIEW nodes', () => {
|
||||
const subgraph = createTestSubgraph()
|
||||
const subgraphNode = createTestSubgraphNode(subgraph)
|
||||
const glslNode = new LGraphNode('GLSLShader')
|
||||
@@ -261,17 +324,24 @@ describe('promoteRecommendedWidgets', () => {
|
||||
|
||||
promoteRecommendedWidgets(subgraphNode)
|
||||
|
||||
const store = usePromotionStore()
|
||||
const hostLocator = createNodeLocatorId(
|
||||
subgraphNode.rootGraph.id,
|
||||
subgraphNode.id
|
||||
)
|
||||
expect(
|
||||
store.isPromoted(subgraphNode.rootGraph.id, subgraphNode.id, {
|
||||
sourceNodeId: String(glslNode.id),
|
||||
sourceWidgetName: CANVAS_IMAGE_PREVIEW_WIDGET
|
||||
})
|
||||
).toBe(true)
|
||||
usePreviewExposureStore().getExposures(
|
||||
subgraphNode.rootGraph.id,
|
||||
hostLocator
|
||||
)
|
||||
).toContainEqual({
|
||||
name: CANVAS_IMAGE_PREVIEW_WIDGET,
|
||||
sourceNodeId: String(glslNode.id),
|
||||
sourcePreviewName: CANVAS_IMAGE_PREVIEW_WIDGET
|
||||
})
|
||||
expect(updatePreviewsMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('registers $$canvas-image-preview on configure for GLSLShader in saved workflow', () => {
|
||||
it('hydrates $$canvas-image-preview exposure on configure for GLSLShader in saved workflow', () => {
|
||||
// Simulate loading a saved workflow where proxyWidgets does NOT contain
|
||||
// the $$canvas-image-preview entry (e.g. blueprint authored before the
|
||||
// promotion system, or old workflow save).
|
||||
@@ -284,13 +354,20 @@ describe('promoteRecommendedWidgets', () => {
|
||||
// which eagerly registers $$canvas-image-preview for supported node types
|
||||
const subgraphNode = createTestSubgraphNode(subgraph)
|
||||
|
||||
const store = usePromotionStore()
|
||||
const hostLocator = createNodeLocatorId(
|
||||
subgraphNode.rootGraph.id,
|
||||
subgraphNode.id
|
||||
)
|
||||
expect(
|
||||
store.isPromoted(subgraphNode.rootGraph.id, subgraphNode.id, {
|
||||
sourceNodeId: String(glslNode.id),
|
||||
sourceWidgetName: CANVAS_IMAGE_PREVIEW_WIDGET
|
||||
})
|
||||
).toBe(true)
|
||||
usePreviewExposureStore().getExposures(
|
||||
subgraphNode.rootGraph.id,
|
||||
hostLocator
|
||||
)
|
||||
).toContainEqual({
|
||||
name: CANVAS_IMAGE_PREVIEW_WIDGET,
|
||||
sourceNodeId: String(glslNode.id),
|
||||
sourcePreviewName: CANVAS_IMAGE_PREVIEW_WIDGET
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -314,12 +391,11 @@ describe('hasUnpromotedWidgets', () => {
|
||||
const subgraphNode = createTestSubgraphNode(subgraph)
|
||||
const interiorNode = new LGraphNode('InnerNode')
|
||||
subgraph.add(interiorNode)
|
||||
interiorNode.addWidget('text', 'seed', '123', () => {})
|
||||
const input = interiorNode.addInput('seed', 'STRING')
|
||||
const widget = interiorNode.addWidget('text', 'seed', '123', () => {})
|
||||
input.widget = { name: widget.name }
|
||||
|
||||
usePromotionStore().promote(subgraphNode.rootGraph.id, subgraphNode.id, {
|
||||
sourceNodeId: String(interiorNode.id),
|
||||
sourceWidgetName: 'seed'
|
||||
})
|
||||
subgraph.addInput('seed', 'STRING').connect(input, interiorNode)
|
||||
|
||||
expect(hasUnpromotedWidgets(subgraphNode)).toBe(false)
|
||||
})
|
||||
|
||||
@@ -8,6 +8,7 @@ import type {
|
||||
} 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 { nextUniqueName } from '@/lib/litegraph/src/strings'
|
||||
import { useToastStore } from '@/platform/updates/common/toastStore'
|
||||
import {
|
||||
CANVAS_IMAGE_PREVIEW_WIDGET,
|
||||
@@ -15,12 +16,13 @@ import {
|
||||
} from '@/composables/node/canvasImagePreviewTypes'
|
||||
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||
import { useLitegraphService } from '@/services/litegraphService'
|
||||
import { usePromotionStore } from '@/stores/promotionStore'
|
||||
import { usePreviewExposureStore } from '@/stores/previewExposureStore'
|
||||
import { useSubgraphNavigationStore } from '@/stores/subgraphNavigationStore'
|
||||
import { createNodeLocatorId } from '@/types/nodeIdentification'
|
||||
|
||||
type PartialNode = Pick<LGraphNode, 'title' | 'id' | 'type'>
|
||||
|
||||
export type WidgetItem = [PartialNode, IBaseWidget]
|
||||
export type WidgetItem = [LGraphNode, IBaseWidget]
|
||||
export { CANVAS_IMAGE_PREVIEW_WIDGET }
|
||||
|
||||
export function getWidgetName(w: IBaseWidget): string {
|
||||
@@ -35,7 +37,8 @@ export function getWidgetName(w: IBaseWidget): string {
|
||||
export function isLinkedPromotion(
|
||||
subgraphNode: SubgraphNode,
|
||||
sourceNodeId: string,
|
||||
sourceWidgetName: string
|
||||
sourceWidgetName: string,
|
||||
disambiguatingSourceNodeId?: string
|
||||
): boolean {
|
||||
return subgraphNode.inputs.some((input) => {
|
||||
const w = input._widget
|
||||
@@ -43,7 +46,9 @@ export function isLinkedPromotion(
|
||||
w &&
|
||||
isPromotedWidgetView(w) &&
|
||||
w.sourceNodeId === sourceNodeId &&
|
||||
w.sourceWidgetName === sourceWidgetName
|
||||
w.sourceWidgetName === sourceWidgetName &&
|
||||
(!disambiguatingSourceNodeId ||
|
||||
w.disambiguatingSourceNodeId === disambiguatingSourceNodeId)
|
||||
)
|
||||
})
|
||||
}
|
||||
@@ -53,6 +58,69 @@ export function getSourceNodeId(w: IBaseWidget): string | undefined {
|
||||
return w.disambiguatingSourceNodeId ?? w.sourceNodeId
|
||||
}
|
||||
|
||||
function isPreviewExposed(
|
||||
subgraphNode: SubgraphNode,
|
||||
source: PromotedWidgetSource
|
||||
): boolean {
|
||||
const hostLocator = createNodeLocatorId(
|
||||
subgraphNode.rootGraph.id,
|
||||
subgraphNode.id
|
||||
)
|
||||
return usePreviewExposureStore()
|
||||
.getExposures(subgraphNode.rootGraph.id, hostLocator)
|
||||
.some(
|
||||
(exposure) =>
|
||||
exposure.sourceNodeId === source.sourceNodeId &&
|
||||
exposure.sourcePreviewName === source.sourceWidgetName
|
||||
)
|
||||
}
|
||||
|
||||
function isPromotedOnParent(
|
||||
subgraphNode: SubgraphNode,
|
||||
widget: IBaseWidget,
|
||||
source: PromotedWidgetSource
|
||||
): boolean {
|
||||
if (isPreviewPseudoWidget(widget))
|
||||
return isPreviewExposed(subgraphNode, source)
|
||||
return isLinkedPromotion(
|
||||
subgraphNode,
|
||||
source.sourceNodeId,
|
||||
source.sourceWidgetName
|
||||
)
|
||||
}
|
||||
|
||||
export function isWidgetPromotedOnSubgraphNode(
|
||||
subgraphNode: SubgraphNode,
|
||||
source: PromotedWidgetSource
|
||||
): boolean {
|
||||
return (
|
||||
isLinkedPromotion(
|
||||
subgraphNode,
|
||||
source.sourceNodeId,
|
||||
source.sourceWidgetName,
|
||||
source.disambiguatingSourceNodeId
|
||||
) || isPreviewExposed(subgraphNode, source)
|
||||
)
|
||||
}
|
||||
|
||||
export function isWidgetPromotedByAnySubgraphNode(
|
||||
node: PartialNode,
|
||||
widgetName: string
|
||||
): boolean {
|
||||
if (!('graph' in node)) return false
|
||||
const graph = (node as LGraphNode).graph
|
||||
if (!graph) return false
|
||||
|
||||
return graph.rootGraph.nodes.some(
|
||||
(candidate) =>
|
||||
candidate.isSubgraphNode() &&
|
||||
isWidgetPromotedOnSubgraphNode(candidate, {
|
||||
sourceNodeId: String(node.id),
|
||||
sourceWidgetName: widgetName
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
function toPromotionSource(
|
||||
node: PartialNode,
|
||||
widget: IBaseWidget
|
||||
@@ -69,11 +137,68 @@ function toPromotionSource(
|
||||
function refreshPromotedWidgetRendering(parents: SubgraphNode[]): void {
|
||||
for (const parent of parents) {
|
||||
parent.computeSize(parent.size)
|
||||
parent.setDirtyCanvas(true, true)
|
||||
parent.setDirtyCanvas?.(true, true)
|
||||
}
|
||||
useCanvasStore().canvas?.setDirty(true, true)
|
||||
}
|
||||
|
||||
type CanonicalPromotionResult =
|
||||
| { ok: true }
|
||||
| { ok: false; reason: 'missingSourceSlot' | 'connectFailed' }
|
||||
|
||||
export function promoteValueWidgetViaSubgraphInput(
|
||||
subgraphNode: SubgraphNode,
|
||||
sourceNode: LGraphNode,
|
||||
sourceWidget: IBaseWidget
|
||||
): CanonicalPromotionResult {
|
||||
const sourceWidgetName = getWidgetName(sourceWidget)
|
||||
if (
|
||||
isLinkedPromotion(subgraphNode, String(sourceNode.id), sourceWidgetName)
|
||||
) {
|
||||
return { ok: true }
|
||||
}
|
||||
|
||||
const sourceSlot = sourceNode.getSlotFromWidget(sourceWidget)
|
||||
if (!sourceSlot) return { ok: false, reason: 'missingSourceSlot' }
|
||||
|
||||
const existingNames = subgraphNode.subgraph.inputs.map((input) => input.name)
|
||||
const inputName = nextUniqueName(sourceWidgetName, existingNames)
|
||||
const subgraphInput = subgraphNode.subgraph.addInput(
|
||||
inputName,
|
||||
String(sourceSlot.type ?? sourceWidget.type ?? '*')
|
||||
)
|
||||
const link = subgraphInput.connect(sourceSlot, sourceNode)
|
||||
if (!link) {
|
||||
subgraphNode.subgraph.removeInput(subgraphInput)
|
||||
return { ok: false, reason: 'connectFailed' }
|
||||
}
|
||||
|
||||
return { ok: true }
|
||||
}
|
||||
|
||||
export function promotePreviewViaExposure(
|
||||
subgraphNode: SubgraphNode,
|
||||
sourceNode: LGraphNode,
|
||||
sourcePreviewName: string
|
||||
): void {
|
||||
const store = usePreviewExposureStore()
|
||||
const rootGraphId = subgraphNode.rootGraph.id
|
||||
const hostLocator = createNodeLocatorId(rootGraphId, subgraphNode.id)
|
||||
const existing = store
|
||||
.getExposures(rootGraphId, hostLocator)
|
||||
.some(
|
||||
(exposure) =>
|
||||
exposure.sourceNodeId === String(sourceNode.id) &&
|
||||
exposure.sourcePreviewName === sourcePreviewName
|
||||
)
|
||||
if (existing) return
|
||||
|
||||
store.addExposure(rootGraphId, hostLocator, {
|
||||
sourceNodeId: String(sourceNode.id),
|
||||
sourcePreviewName
|
||||
})
|
||||
}
|
||||
|
||||
/** Known non-$$ preview widget types added by core or popular extensions. */
|
||||
const PREVIEW_WIDGET_TYPES = new Set(['preview', 'video', 'audioUI'])
|
||||
|
||||
@@ -98,10 +223,19 @@ export function promoteWidget(
|
||||
widget: IBaseWidget,
|
||||
parents: SubgraphNode[]
|
||||
) {
|
||||
const store = usePromotionStore()
|
||||
const source = toPromotionSource(node, widget)
|
||||
for (const parent of parents) {
|
||||
store.promote(parent.rootGraph.id, parent.id, source)
|
||||
if (isPreviewPseudoWidget(widget)) {
|
||||
promotePreviewViaExposure(
|
||||
parent,
|
||||
node as LGraphNode,
|
||||
source.sourceWidgetName
|
||||
)
|
||||
continue
|
||||
}
|
||||
if ('getSlotFromWidget' in node) {
|
||||
promoteValueWidgetViaSubgraphInput(parent, node as LGraphNode, widget)
|
||||
}
|
||||
}
|
||||
refreshPromotedWidgetRendering(parents)
|
||||
Sentry.addBreadcrumb({
|
||||
@@ -116,10 +250,45 @@ export function demoteWidget(
|
||||
widget: IBaseWidget,
|
||||
parents: SubgraphNode[]
|
||||
) {
|
||||
const store = usePromotionStore()
|
||||
const source = toPromotionSource(node, widget)
|
||||
for (const parent of parents) {
|
||||
store.demote(parent.rootGraph.id, parent.id, source)
|
||||
if (!parent.subgraph) continue
|
||||
|
||||
const linkedInput = parent.subgraph.inputs.find((input) => {
|
||||
const linkedWidget = input._widget
|
||||
return (
|
||||
linkedWidget &&
|
||||
isPromotedWidgetView(linkedWidget) &&
|
||||
linkedWidget.sourceNodeId === source.sourceNodeId &&
|
||||
linkedWidget.sourceWidgetName === source.sourceWidgetName &&
|
||||
linkedWidget.disambiguatingSourceNodeId ===
|
||||
source.disambiguatingSourceNodeId
|
||||
)
|
||||
})
|
||||
if (linkedInput) {
|
||||
parent.subgraph.removeInput(linkedInput)
|
||||
continue
|
||||
}
|
||||
|
||||
if (isPreviewPseudoWidget(widget)) {
|
||||
const previewStore = usePreviewExposureStore()
|
||||
const hostLocator = createNodeLocatorId(parent.rootGraph.id, parent.id)
|
||||
const exposure = previewStore
|
||||
.getExposures(parent.rootGraph.id, hostLocator)
|
||||
.find(
|
||||
(entry) =>
|
||||
entry.sourceNodeId === source.sourceNodeId &&
|
||||
entry.sourcePreviewName === source.sourceWidgetName
|
||||
)
|
||||
if (exposure) {
|
||||
previewStore.removeExposure(
|
||||
parent.rootGraph.id,
|
||||
hostLocator,
|
||||
exposure.name
|
||||
)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
refreshPromotedWidgetRendering(parents)
|
||||
Sentry.addBreadcrumb({
|
||||
@@ -152,11 +321,10 @@ export function addWidgetPromotionOptions(
|
||||
widget: IBaseWidget,
|
||||
node: LGraphNode
|
||||
) {
|
||||
const store = usePromotionStore()
|
||||
const parents = getParentNodes()
|
||||
const source = toPromotionSource(node, widget)
|
||||
const promotableParents = parents.filter(
|
||||
(s) => !store.isPromoted(s.rootGraph.id, s.id, source)
|
||||
(parent) => !isPromotedOnParent(parent, widget, source)
|
||||
)
|
||||
if (promotableParents.length > 0)
|
||||
options.unshift({
|
||||
@@ -189,10 +357,9 @@ export function tryToggleWidgetPromotion() {
|
||||
const widget = node.getWidgetOnPos(x, y, true)
|
||||
const parents = getParentNodes()
|
||||
if (!parents.length || !widget) return
|
||||
const store = usePromotionStore()
|
||||
const source = toPromotionSource(node, widget)
|
||||
const promotableParents = parents.filter(
|
||||
(s) => !store.isPromoted(s.rootGraph.id, s.id, source)
|
||||
(parent) => !isPromotedOnParent(parent, widget, source)
|
||||
)
|
||||
if (promotableParents.length > 0)
|
||||
promoteWidget(node, widget, promotableParents)
|
||||
@@ -248,7 +415,6 @@ function nodeWidgets(n: LGraphNode): WidgetItem[] {
|
||||
}
|
||||
|
||||
export function promoteRecommendedWidgets(subgraphNode: SubgraphNode) {
|
||||
const store = usePromotionStore()
|
||||
const { updatePreviews } = useLitegraphService()
|
||||
const interiorNodes = subgraphNode.subgraph.nodes
|
||||
for (const node of interiorNodes) {
|
||||
@@ -260,14 +426,7 @@ export function promoteRecommendedWidgets(subgraphNode: SubgraphNode) {
|
||||
function promotePreviewWidget() {
|
||||
const widget = node.widgets?.find(isPreviewPseudoWidget)
|
||||
if (!widget) return
|
||||
if (
|
||||
store.isPromoted(subgraphNode.rootGraph.id, subgraphNode.id, {
|
||||
sourceNodeId: String(node.id),
|
||||
sourceWidgetName: widget.name
|
||||
})
|
||||
)
|
||||
return
|
||||
promoteWidget(node, widget, [subgraphNode])
|
||||
promotePreviewViaExposure(subgraphNode, node, widget.name)
|
||||
}
|
||||
// Promote preview widgets that already exist (e.g. custom node DOM widgets
|
||||
// like VHS videopreview that are created in onNodeCreated).
|
||||
@@ -282,19 +441,7 @@ export function promoteRecommendedWidgets(subgraphNode: SubgraphNode) {
|
||||
// includes this node and onDrawBackground can call updatePreviews on it
|
||||
// once execution outputs arrive.
|
||||
if (supportsVirtualCanvasImagePreview(node)) {
|
||||
const canvasSource: PromotedWidgetSource = {
|
||||
sourceNodeId: String(node.id),
|
||||
sourceWidgetName: CANVAS_IMAGE_PREVIEW_WIDGET
|
||||
}
|
||||
if (
|
||||
!store.isPromoted(
|
||||
subgraphNode.rootGraph.id,
|
||||
subgraphNode.id,
|
||||
canvasSource
|
||||
)
|
||||
) {
|
||||
store.promote(subgraphNode.rootGraph.id, subgraphNode.id, canvasSource)
|
||||
}
|
||||
promotePreviewViaExposure(subgraphNode, node, CANVAS_IMAGE_PREVIEW_WIDGET)
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -305,40 +452,39 @@ export function promoteRecommendedWidgets(subgraphNode: SubgraphNode) {
|
||||
const filteredWidgets: WidgetItem[] = interiorNodes
|
||||
.flatMap(nodeWidgets)
|
||||
.filter(isRecommendedWidget)
|
||||
.filter(([, widget]) => !isPreviewPseudoWidget(widget))
|
||||
for (const [n, w] of filteredWidgets) {
|
||||
store.promote(
|
||||
subgraphNode.rootGraph.id,
|
||||
subgraphNode.id,
|
||||
toPromotionSource(n, w)
|
||||
)
|
||||
promoteValueWidgetViaSubgraphInput(subgraphNode, n, w)
|
||||
}
|
||||
subgraphNode.computeSize(subgraphNode.size)
|
||||
}
|
||||
|
||||
export function pruneDisconnected(subgraphNode: SubgraphNode) {
|
||||
const store = usePromotionStore()
|
||||
const subgraph = subgraphNode.subgraph
|
||||
const entries = store.getPromotions(
|
||||
subgraphNode.rootGraph.id,
|
||||
subgraphNode.id
|
||||
)
|
||||
const removedEntries: PromotedWidgetSource[] = []
|
||||
|
||||
const validEntries = entries.filter((entry) => {
|
||||
const node = subgraph.getNodeById(entry.sourceNodeId)
|
||||
const staleInputs = subgraph.inputs.filter((input) => {
|
||||
const widget = input._widget
|
||||
if (!widget || !isPromotedWidgetView(widget)) return false
|
||||
|
||||
const node = subgraph.getNodeById(widget.sourceNodeId)
|
||||
if (!node) {
|
||||
removedEntries.push(entry)
|
||||
return false
|
||||
removedEntries.push(widget)
|
||||
return true
|
||||
}
|
||||
const hasWidget = getPromotableWidgets(node).some(
|
||||
(iw) => iw.name === entry.sourceWidgetName
|
||||
(iw) => iw.name === widget.sourceWidgetName
|
||||
)
|
||||
if (!hasWidget) {
|
||||
removedEntries.push(entry)
|
||||
removedEntries.push(widget)
|
||||
}
|
||||
return hasWidget
|
||||
return !hasWidget
|
||||
})
|
||||
|
||||
for (const input of staleInputs) {
|
||||
subgraph.removeInput(input)
|
||||
}
|
||||
|
||||
if (removedEntries.length > 0 && import.meta.env.DEV) {
|
||||
console.warn(
|
||||
'[proxyWidgetUtils] Pruned disconnected promotions',
|
||||
@@ -350,7 +496,6 @@ export function pruneDisconnected(subgraphNode: SubgraphNode) {
|
||||
)
|
||||
}
|
||||
|
||||
store.setPromotions(subgraphNode.rootGraph.id, subgraphNode.id, validEntries)
|
||||
refreshPromotedWidgetRendering([subgraphNode])
|
||||
Sentry.addBreadcrumb({
|
||||
category: 'subgraph',
|
||||
@@ -360,14 +505,13 @@ export function pruneDisconnected(subgraphNode: SubgraphNode) {
|
||||
}
|
||||
|
||||
export function hasUnpromotedWidgets(subgraphNode: SubgraphNode): boolean {
|
||||
const promotionStore = usePromotionStore()
|
||||
const { id: subgraphNodeId, rootGraph, subgraph } = subgraphNode
|
||||
const { subgraph } = subgraphNode
|
||||
|
||||
return subgraph.nodes.some((interiorNode) =>
|
||||
(interiorNode.widgets ?? []).some(
|
||||
getPromotableWidgets(interiorNode).some(
|
||||
(widget) =>
|
||||
!widget.computedDisabled &&
|
||||
!promotionStore.isPromoted(rootGraph.id, subgraphNodeId, {
|
||||
!isPromotedOnParent(subgraphNode, widget, {
|
||||
sourceNodeId: String(interiorNode.id),
|
||||
sourceWidgetName: widget.name
|
||||
})
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
import type { SerialisableGraph } from '@/lib/litegraph/src/types/serialisation'
|
||||
import type { UUID } from '@/lib/litegraph/src/utils/uuid'
|
||||
import { zeroUuid } from '@/lib/litegraph/src/utils/uuid'
|
||||
import { usePreviewExposureStore } from '@/stores/previewExposureStore'
|
||||
import { usePromotionStore } from '@/stores/promotionStore'
|
||||
import { useWidgetValueStore } from '@/stores/widgetValueStore'
|
||||
import {
|
||||
@@ -280,7 +281,7 @@ describe('Graph Clearing and Callbacks', () => {
|
||||
expect(graph.nodes.length).toBe(0)
|
||||
})
|
||||
|
||||
test('clear() removes graph-scoped promotion and widget-value state', () => {
|
||||
test('clear() removes graph-scoped promotion, preview, and widget-value state', () => {
|
||||
setActivePinia(createTestingPinia({ stubActions: false }))
|
||||
|
||||
const graph = new LGraph()
|
||||
@@ -293,6 +294,12 @@ describe('Graph Clearing and Callbacks', () => {
|
||||
sourceWidgetName: 'seed'
|
||||
})
|
||||
|
||||
const previewExposureStore = usePreviewExposureStore()
|
||||
previewExposureStore.addExposure(graphId, `${graphId}:1`, {
|
||||
sourceNodeId: '10',
|
||||
sourcePreviewName: '$$canvas-image-preview'
|
||||
})
|
||||
|
||||
const widgetValueStore = useWidgetValueStore()
|
||||
widgetValueStore.registerWidget(graphId, {
|
||||
nodeId: '10' as NodeId,
|
||||
@@ -314,6 +321,9 @@ describe('Graph Clearing and Callbacks', () => {
|
||||
expect(widgetValueStore.getWidget(graphId, '10' as NodeId, 'seed')).toEqual(
|
||||
expect.objectContaining({ value: 1 })
|
||||
)
|
||||
expect(
|
||||
previewExposureStore.getExposures(graphId, `${graphId}:1`)
|
||||
).toHaveLength(1)
|
||||
|
||||
graph.clear()
|
||||
|
||||
@@ -326,6 +336,9 @@ describe('Graph Clearing and Callbacks', () => {
|
||||
expect(
|
||||
widgetValueStore.getWidget(graphId, '10' as NodeId, 'seed')
|
||||
).toBeUndefined()
|
||||
expect(previewExposureStore.getExposures(graphId, `${graphId}:1`)).toEqual(
|
||||
[]
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { toString } from 'es-toolkit/compat'
|
||||
|
||||
import type { PromotedWidgetSource } from '@/core/graph/subgraph/promotedWidgetTypes'
|
||||
import {
|
||||
SUBGRAPH_INPUT_ID,
|
||||
SUBGRAPH_OUTPUT_ID
|
||||
@@ -10,10 +9,7 @@ import type { UUID } from '@/lib/litegraph/src/utils/uuid'
|
||||
import { createUuidv4, zeroUuid } from '@/lib/litegraph/src/utils/uuid'
|
||||
import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMutations'
|
||||
import { LayoutSource } from '@/renderer/core/layout/types'
|
||||
import {
|
||||
makePromotionEntryKey,
|
||||
usePromotionStore
|
||||
} from '@/stores/promotionStore'
|
||||
import { usePreviewExposureStore } from '@/stores/previewExposureStore'
|
||||
import { useWidgetValueStore } from '@/stores/widgetValueStore'
|
||||
import { forEachNode } from '@/utils/graphTraversalUtil'
|
||||
|
||||
@@ -380,7 +376,7 @@ export class LGraph
|
||||
|
||||
const graphId = this.id
|
||||
if (this.isRootGraph && graphId !== zeroUuid) {
|
||||
usePromotionStore().clearGraph(graphId)
|
||||
usePreviewExposureStore().clearGraph(graphId)
|
||||
useWidgetValueStore().clearGraph(graphId)
|
||||
}
|
||||
|
||||
@@ -1928,13 +1924,6 @@ export class LGraph
|
||||
subgraphNode._setConcreteSlots()
|
||||
subgraphNode.arrange()
|
||||
|
||||
// Repair ancestor promotions: when nodes are packed into a nested
|
||||
// subgraph, any host SubgraphNode whose proxyWidgets referenced the
|
||||
// moved nodes must be repointed to chain through the new nested node.
|
||||
if (!this.isRootGraph) {
|
||||
this._repointAncestorPromotions(nodes, subgraphNode as SubgraphNode)
|
||||
}
|
||||
|
||||
this.canvasAction((c) =>
|
||||
c.canvas.dispatchEvent(
|
||||
new CustomEvent('subgraph-converted', {
|
||||
@@ -1947,75 +1936,6 @@ export class LGraph
|
||||
return { subgraph, node: subgraphNode as SubgraphNode }
|
||||
}
|
||||
|
||||
/**
|
||||
* After packing nodes into a nested subgraph, repoint any ancestor
|
||||
* SubgraphNode promotions that referenced the moved nodes so they
|
||||
* chain through the newly created nested SubgraphNode.
|
||||
*/
|
||||
private _repointAncestorPromotions(
|
||||
movedNodes: Set<LGraphNode>,
|
||||
nestedSubgraphNode: SubgraphNode
|
||||
): void {
|
||||
const movedNodeIds = new Set([...movedNodes].map((n) => String(n.id)))
|
||||
const store = usePromotionStore()
|
||||
const nestedNodeId = String(nestedSubgraphNode.id)
|
||||
const graphId = this.rootGraph.id
|
||||
const nestedEntries = store.getPromotions(graphId, nestedSubgraphNode.id)
|
||||
const nextNestedEntries = [...nestedEntries]
|
||||
const nestedEntryKeys = new Set(
|
||||
nestedEntries.map((entry) => makePromotionEntryKey(entry))
|
||||
)
|
||||
const hostUpdates: Array<{
|
||||
node: SubgraphNode
|
||||
entries: PromotedWidgetSource[]
|
||||
}> = []
|
||||
|
||||
// Find all SubgraphNode instances that host `this` subgraph.
|
||||
// They live in any graph and have `type === this.id`.
|
||||
const allGraphs: LGraph[] = [
|
||||
this.rootGraph,
|
||||
...this.rootGraph._subgraphs.values()
|
||||
]
|
||||
for (const graph of allGraphs) {
|
||||
for (const node of graph._nodes) {
|
||||
if (!node.isSubgraphNode() || node.type !== this.id) continue
|
||||
|
||||
const entries = store.getPromotions(graphId, node.id)
|
||||
const movedEntries = entries.filter((entry) =>
|
||||
movedNodeIds.has(entry.sourceNodeId)
|
||||
)
|
||||
if (movedEntries.length === 0) continue
|
||||
|
||||
for (const entry of movedEntries) {
|
||||
const key = makePromotionEntryKey(entry)
|
||||
if (nestedEntryKeys.has(key)) continue
|
||||
nestedEntryKeys.add(key)
|
||||
nextNestedEntries.push(entry)
|
||||
}
|
||||
|
||||
const nextEntries = entries.map((entry) => {
|
||||
if (!movedNodeIds.has(entry.sourceNodeId)) return entry
|
||||
return {
|
||||
sourceNodeId: nestedNodeId,
|
||||
sourceWidgetName: entry.sourceWidgetName,
|
||||
disambiguatingSourceNodeId:
|
||||
entry.disambiguatingSourceNodeId ?? entry.sourceNodeId
|
||||
}
|
||||
})
|
||||
|
||||
hostUpdates.push({ node, entries: nextEntries })
|
||||
}
|
||||
}
|
||||
|
||||
if (nextNestedEntries.length !== nestedEntries.length)
|
||||
store.setPromotions(graphId, nestedSubgraphNode.id, nextNestedEntries)
|
||||
|
||||
for (const { node, entries } of hostUpdates) {
|
||||
store.setPromotions(graphId, node.id, entries)
|
||||
node.rebuildInputWidgetBindings()
|
||||
}
|
||||
}
|
||||
|
||||
unpackSubgraph(
|
||||
subgraphNode: SubgraphNode,
|
||||
options?: { skipMissingNodes?: boolean }
|
||||
|
||||
@@ -77,7 +77,6 @@ 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'
|
||||
|
||||
@@ -135,7 +135,6 @@ 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,
|
||||
|
||||
@@ -152,6 +152,33 @@ describe('SubgraphNode.serialize (ADR 0009)', () => {
|
||||
})
|
||||
|
||||
describe('previewExposures round-trip', () => {
|
||||
it('hydrates previewExposures into the store during configure', () => {
|
||||
const subgraph = createTestSubgraph()
|
||||
const hostNode = createTestSubgraphNode(subgraph)
|
||||
const rootGraphId = hostNode.rootGraph.id
|
||||
const hostLocator = createNodeLocatorId(rootGraphId, hostNode.id)
|
||||
|
||||
hostNode.properties.previewExposures = [
|
||||
{
|
||||
name: 'preview',
|
||||
sourceNodeId: '12',
|
||||
sourcePreviewName: '$$canvas-image-preview'
|
||||
}
|
||||
]
|
||||
|
||||
hostNode._internalConfigureAfterSlots()
|
||||
|
||||
expect(
|
||||
usePreviewExposureStore().getExposures(rootGraphId, hostLocator)
|
||||
).toEqual([
|
||||
{
|
||||
name: 'preview',
|
||||
sourceNodeId: '12',
|
||||
sourcePreviewName: '$$canvas-image-preview'
|
||||
}
|
||||
])
|
||||
})
|
||||
|
||||
it('writes previewExposures from the store on serialize', () => {
|
||||
const subgraph = createTestSubgraph()
|
||||
const hostNode = createTestSubgraphNode(subgraph)
|
||||
|
||||
@@ -37,24 +37,17 @@ import {
|
||||
getPromotedWidgetHostStateName,
|
||||
isPromotedWidgetView
|
||||
} from '@/core/graph/subgraph/promotedWidgetView'
|
||||
import { normalizeLegacyProxyWidgetEntry } from '@/core/graph/subgraph/legacyProxyWidgetNormalization'
|
||||
import type { PromotedWidgetView } from '@/core/graph/subgraph/promotedWidgetView'
|
||||
import type { PromotedWidgetSource } from '@/core/graph/subgraph/promotedWidgetTypes'
|
||||
import { resolveConcretePromotedWidget } from '@/core/graph/subgraph/resolveConcretePromotedWidget'
|
||||
import { resolveSubgraphInputTarget } from '@/core/graph/subgraph/resolveSubgraphInputTarget'
|
||||
import { hasWidgetNode } from '@/core/graph/subgraph/widgetNodeTypeGuard'
|
||||
import {
|
||||
CANVAS_IMAGE_PREVIEW_WIDGET,
|
||||
supportsVirtualCanvasImagePreview
|
||||
} from '@/composables/node/canvasImagePreviewTypes'
|
||||
import { readHostQuarantine } from '@/core/graph/subgraph/migration/quarantineEntry'
|
||||
import { parseProxyWidgets } from '@/core/schemas/promotionSchema'
|
||||
import { parsePreviewExposures } from '@/core/schemas/previewExposureSchema'
|
||||
import { useDomWidgetStore } from '@/stores/domWidgetStore'
|
||||
import { usePreviewExposureStore } from '@/stores/previewExposureStore'
|
||||
import {
|
||||
makePromotionEntryKey,
|
||||
usePromotionStore
|
||||
} from '@/stores/promotionStore'
|
||||
import { useWidgetValueStore } from '@/stores/widgetValueStore'
|
||||
import { createNodeLocatorId } from '@/types/nodeIdentification'
|
||||
|
||||
@@ -112,13 +105,6 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
|
||||
|
||||
private _promotedViewManager =
|
||||
new PromotedWidgetViewManager<PromotedWidgetView>()
|
||||
/**
|
||||
* Promotions buffered before this node is attached to a graph (`id === -1`).
|
||||
* They are flushed in `_flushPendingPromotions()` from `_setWidget()` and
|
||||
* `onAdded()`, so construction-time promotions require normal add-to-graph
|
||||
* lifecycle to persist.
|
||||
*/
|
||||
private _pendingPromotions: PromotedWidgetSource[] = []
|
||||
private _cacheVersion = 0
|
||||
private _linkedEntriesCache?: {
|
||||
version: number
|
||||
@@ -127,7 +113,6 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
|
||||
}
|
||||
private _promotedViewsCache?: {
|
||||
version: number
|
||||
entriesRef: PromotedWidgetSource[]
|
||||
hasMissingBoundSourceWidget: boolean
|
||||
views: PromotedWidgetView[]
|
||||
}
|
||||
@@ -273,21 +258,17 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
|
||||
}
|
||||
|
||||
private _getPromotedViews(): PromotedWidgetView[] {
|
||||
const store = usePromotionStore()
|
||||
const entries = store.getPromotionsRef(this.rootGraph.id, this.id)
|
||||
const hasMissingBoundSourceWidget = this._hasMissingBoundSourceWidget()
|
||||
const cachedViews = this._promotedViewsCache
|
||||
if (
|
||||
cachedViews?.version === this._cacheVersion &&
|
||||
cachedViews.entriesRef === entries &&
|
||||
cachedViews.hasMissingBoundSourceWidget === hasMissingBoundSourceWidget
|
||||
)
|
||||
return cachedViews.views
|
||||
|
||||
const linkedEntries = this._getLinkedPromotionEntries()
|
||||
|
||||
const { displayNameByViewKey, reconcileEntries } =
|
||||
this._buildPromotionReconcileState(entries, linkedEntries)
|
||||
const displayNameByViewKey = this._buildDisplayNameByViewKey(linkedEntries)
|
||||
const reconcileEntries = this._buildLinkedReconcileEntries(linkedEntries)
|
||||
|
||||
const views = this._promotedViewManager.reconcile(
|
||||
reconcileEntries,
|
||||
@@ -304,7 +285,6 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
|
||||
|
||||
this._promotedViewsCache = {
|
||||
version: this._cacheVersion,
|
||||
entriesRef: entries,
|
||||
hasMissingBoundSourceWidget,
|
||||
views
|
||||
}
|
||||
@@ -317,268 +297,7 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
|
||||
}
|
||||
|
||||
private _syncPromotions(): void {
|
||||
if (this.id === -1) return
|
||||
|
||||
const store = usePromotionStore()
|
||||
const entries = store.getPromotionsRef(this.rootGraph.id, this.id)
|
||||
const linkedEntries = this._getLinkedPromotionEntries(false)
|
||||
// Intentionally preserve independent store promotions when linked coverage is partial;
|
||||
// tests assert that mixed linked/independent states must not collapse to linked-only.
|
||||
const { mergedEntries } = this._buildPromotionPersistenceState(
|
||||
entries,
|
||||
linkedEntries
|
||||
)
|
||||
|
||||
const hasChanged =
|
||||
mergedEntries.length !== entries.length ||
|
||||
mergedEntries.some(
|
||||
(entry, index) =>
|
||||
entry.sourceNodeId !== entries[index]?.sourceNodeId ||
|
||||
entry.sourceWidgetName !== entries[index]?.sourceWidgetName ||
|
||||
entry.disambiguatingSourceNodeId !==
|
||||
entries[index]?.disambiguatingSourceNodeId
|
||||
)
|
||||
|
||||
if (!hasChanged) return
|
||||
|
||||
store.setPromotions(this.rootGraph.id, this.id, mergedEntries)
|
||||
}
|
||||
|
||||
private _buildPromotionReconcileState(
|
||||
entries: PromotedWidgetSource[],
|
||||
linkedEntries: LinkedPromotionEntry[]
|
||||
): {
|
||||
displayNameByViewKey: Map<string, string>
|
||||
reconcileEntries: Array<{
|
||||
sourceNodeId: string
|
||||
sourceWidgetName: string
|
||||
viewKey?: string
|
||||
disambiguatingSourceNodeId?: string
|
||||
slotName?: string
|
||||
}>
|
||||
} {
|
||||
const { fallbackStoredEntries } = this._collectLinkedAndFallbackEntries(
|
||||
entries,
|
||||
linkedEntries
|
||||
)
|
||||
const linkedReconcileEntries =
|
||||
this._buildLinkedReconcileEntries(linkedEntries)
|
||||
const shouldPersistLinkedOnly = this._shouldPersistLinkedOnly(
|
||||
linkedEntries,
|
||||
fallbackStoredEntries
|
||||
)
|
||||
const fallbackReconcileEntries = fallbackStoredEntries.map((e) =>
|
||||
e.disambiguatingSourceNodeId
|
||||
? {
|
||||
sourceNodeId: e.sourceNodeId,
|
||||
sourceWidgetName: e.sourceWidgetName,
|
||||
disambiguatingSourceNodeId: e.disambiguatingSourceNodeId,
|
||||
viewKey: `src:${e.sourceNodeId}:${e.sourceWidgetName}:${e.disambiguatingSourceNodeId}`
|
||||
}
|
||||
: e
|
||||
)
|
||||
const reconcileEntries = shouldPersistLinkedOnly
|
||||
? linkedReconcileEntries
|
||||
: [...linkedReconcileEntries, ...fallbackReconcileEntries]
|
||||
|
||||
return {
|
||||
displayNameByViewKey: this._buildDisplayNameByViewKey(linkedEntries),
|
||||
reconcileEntries
|
||||
}
|
||||
}
|
||||
|
||||
private _buildPromotionPersistenceState(
|
||||
entries: PromotedWidgetSource[],
|
||||
linkedEntries: LinkedPromotionEntry[]
|
||||
): {
|
||||
mergedEntries: PromotedWidgetSource[]
|
||||
} {
|
||||
const { linkedPromotionEntries, fallbackStoredEntries } =
|
||||
this._collectLinkedAndFallbackEntries(entries, linkedEntries)
|
||||
const shouldPersistLinkedOnly = this._shouldPersistLinkedOnly(
|
||||
linkedEntries,
|
||||
fallbackStoredEntries
|
||||
)
|
||||
|
||||
return {
|
||||
mergedEntries: shouldPersistLinkedOnly
|
||||
? linkedPromotionEntries
|
||||
: [...linkedPromotionEntries, ...fallbackStoredEntries]
|
||||
}
|
||||
}
|
||||
|
||||
private _collectLinkedAndFallbackEntries(
|
||||
entries: PromotedWidgetSource[],
|
||||
linkedEntries: LinkedPromotionEntry[]
|
||||
): {
|
||||
linkedPromotionEntries: PromotedWidgetSource[]
|
||||
fallbackStoredEntries: PromotedWidgetSource[]
|
||||
} {
|
||||
const linkedPromotionEntries = this._toPromotionEntries(linkedEntries)
|
||||
const excludedEntryKeys = new Set(
|
||||
linkedPromotionEntries.map((entry) =>
|
||||
this._makePromotionEntryKey(
|
||||
entry.sourceNodeId,
|
||||
entry.sourceWidgetName,
|
||||
entry.disambiguatingSourceNodeId
|
||||
)
|
||||
)
|
||||
)
|
||||
const connectedEntryKeys = this._getConnectedPromotionEntryKeys()
|
||||
for (const key of connectedEntryKeys) {
|
||||
excludedEntryKeys.add(key)
|
||||
}
|
||||
|
||||
const prePruneFallbackStoredEntries = this._getFallbackStoredEntries(
|
||||
entries,
|
||||
excludedEntryKeys
|
||||
)
|
||||
const fallbackStoredEntries = this._pruneStaleAliasFallbackEntries(
|
||||
prePruneFallbackStoredEntries,
|
||||
linkedPromotionEntries
|
||||
)
|
||||
|
||||
return {
|
||||
linkedPromotionEntries,
|
||||
fallbackStoredEntries
|
||||
}
|
||||
}
|
||||
|
||||
private _shouldPersistLinkedOnly(
|
||||
linkedEntries: LinkedPromotionEntry[],
|
||||
fallbackStoredEntries: PromotedWidgetSource[]
|
||||
): boolean {
|
||||
if (
|
||||
!(this.inputs.length > 0 && linkedEntries.length === this.inputs.length)
|
||||
)
|
||||
return false
|
||||
|
||||
const linkedEntryKeys = new Set(
|
||||
linkedEntries.map((entry) =>
|
||||
this._makePromotionEntryKey(entry.sourceNodeId, entry.sourceWidgetName)
|
||||
)
|
||||
)
|
||||
|
||||
const linkedWidgetNames = new Set(
|
||||
linkedEntries.map((entry) => entry.sourceWidgetName)
|
||||
)
|
||||
|
||||
const hasFallbackToKeep = fallbackStoredEntries.some((entry) => {
|
||||
const sourceNode = this.subgraph.getNodeById(entry.sourceNodeId)
|
||||
if (!sourceNode) return linkedWidgetNames.has(entry.sourceWidgetName)
|
||||
if (sourceNode.type === 'PrimitiveNode') return true
|
||||
|
||||
const hasSourceWidget =
|
||||
sourceNode.widgets?.some(
|
||||
(widget) => widget.name === entry.sourceWidgetName
|
||||
) === true
|
||||
if (hasSourceWidget) return true
|
||||
|
||||
// If the fallback entry overlaps a linked entry, keep it
|
||||
// until aliasing can be positively proven.
|
||||
return linkedEntryKeys.has(
|
||||
this._makePromotionEntryKey(entry.sourceNodeId, entry.sourceWidgetName)
|
||||
)
|
||||
})
|
||||
|
||||
return !hasFallbackToKeep
|
||||
}
|
||||
|
||||
private _toPromotionEntries(
|
||||
linkedEntries: LinkedPromotionEntry[]
|
||||
): PromotedWidgetSource[] {
|
||||
return linkedEntries.map(
|
||||
({ sourceNodeId, sourceWidgetName, disambiguatingSourceNodeId }) => ({
|
||||
sourceNodeId,
|
||||
sourceWidgetName,
|
||||
...(disambiguatingSourceNodeId && { disambiguatingSourceNodeId })
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
private _getFallbackStoredEntries(
|
||||
entries: PromotedWidgetSource[],
|
||||
excludedEntryKeys: Set<string>
|
||||
): PromotedWidgetSource[] {
|
||||
return entries.filter(
|
||||
(entry) =>
|
||||
!excludedEntryKeys.has(
|
||||
this._makePromotionEntryKey(
|
||||
entry.sourceNodeId,
|
||||
entry.sourceWidgetName,
|
||||
entry.disambiguatingSourceNodeId
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private _pruneStaleAliasFallbackEntries(
|
||||
fallbackStoredEntries: PromotedWidgetSource[],
|
||||
linkedPromotionEntries: PromotedWidgetSource[]
|
||||
): PromotedWidgetSource[] {
|
||||
if (
|
||||
fallbackStoredEntries.length === 0 ||
|
||||
linkedPromotionEntries.length === 0
|
||||
)
|
||||
return fallbackStoredEntries
|
||||
|
||||
const linkedConcreteKeys = new Set(
|
||||
linkedPromotionEntries
|
||||
.map((entry) => this._resolveConcretePromotionEntryKey(entry))
|
||||
.filter((key): key is string => key !== undefined)
|
||||
)
|
||||
if (linkedConcreteKeys.size === 0) return fallbackStoredEntries
|
||||
|
||||
const prunedEntries: PromotedWidgetSource[] = []
|
||||
|
||||
for (const entry of fallbackStoredEntries) {
|
||||
if (!this.subgraph.getNodeById(entry.sourceNodeId)) continue
|
||||
|
||||
const concreteKey = this._resolveConcretePromotionEntryKey(entry)
|
||||
if (concreteKey && linkedConcreteKeys.has(concreteKey)) continue
|
||||
|
||||
prunedEntries.push(entry)
|
||||
}
|
||||
|
||||
return prunedEntries
|
||||
}
|
||||
|
||||
private _resolveConcretePromotionEntryKey(
|
||||
entry: PromotedWidgetSource
|
||||
): string | undefined {
|
||||
const result = resolveConcretePromotedWidget(
|
||||
this,
|
||||
entry.sourceNodeId,
|
||||
entry.sourceWidgetName,
|
||||
entry.disambiguatingSourceNodeId
|
||||
)
|
||||
if (result.status !== 'resolved') return undefined
|
||||
|
||||
return this._makePromotionEntryKey(
|
||||
String(result.resolved.node.id),
|
||||
result.resolved.widget.name
|
||||
)
|
||||
}
|
||||
|
||||
private _getConnectedPromotionEntryKeys(): Set<string> {
|
||||
const connectedEntryKeys = new Set<string>()
|
||||
|
||||
for (const input of this.inputs) {
|
||||
const subgraphInput = input._subgraphSlot
|
||||
if (!subgraphInput) continue
|
||||
|
||||
const connectedWidgets = subgraphInput.getConnectedWidgets()
|
||||
|
||||
for (const widget of connectedWidgets) {
|
||||
if (!hasWidgetNode(widget)) continue
|
||||
|
||||
connectedEntryKeys.add(
|
||||
this._makePromotionEntryKey(String(widget.node.id), widget.name)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return connectedEntryKeys
|
||||
this._invalidatePromotedViewsCache()
|
||||
}
|
||||
|
||||
private _buildLinkedReconcileEntries(
|
||||
@@ -631,18 +350,6 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
|
||||
)
|
||||
}
|
||||
|
||||
private _makePromotionEntryKey(
|
||||
sourceNodeId: string,
|
||||
sourceWidgetName: string,
|
||||
disambiguatingSourceNodeId?: string
|
||||
): string {
|
||||
return makePromotionEntryKey({
|
||||
sourceNodeId,
|
||||
sourceWidgetName,
|
||||
disambiguatingSourceNodeId
|
||||
})
|
||||
}
|
||||
|
||||
private _makePromotionViewKey(
|
||||
inputKey: string,
|
||||
sourceNodeId: string,
|
||||
@@ -661,31 +368,6 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
|
||||
: JSON.stringify([inputKey, sourceNodeId, sourceWidgetName, inputName])
|
||||
}
|
||||
|
||||
private _resolveLegacyEntry(
|
||||
widgetName: string
|
||||
): [string, string] | undefined {
|
||||
// Legacy -1 entries use the slot name as the widget name.
|
||||
// Find the input with that name, then trace to the connected interior widget.
|
||||
const input = this.inputs.find((i) => i.name === widgetName)
|
||||
if (!input?._widget) {
|
||||
// Fallback: find via subgraph input slot connection
|
||||
const resolvedTarget = resolveSubgraphInputTarget(this, widgetName)
|
||||
if (!resolvedTarget) return undefined
|
||||
return [resolvedTarget.nodeId, resolvedTarget.widgetName]
|
||||
}
|
||||
|
||||
const widget = input._widget
|
||||
if (isPromotedWidgetView(widget)) {
|
||||
return [widget.sourceNodeId, widget.sourceWidgetName]
|
||||
}
|
||||
|
||||
// Fallback: find via subgraph input slot connection
|
||||
const resolvedTarget = resolveSubgraphInputTarget(this, widgetName)
|
||||
if (!resolvedTarget) return undefined
|
||||
|
||||
return [resolvedTarget.nodeId, resolvedTarget.widgetName]
|
||||
}
|
||||
|
||||
/** Manages lifecycle of all subgraph event listeners */
|
||||
private _eventAbortController = new AbortController()
|
||||
|
||||
@@ -880,17 +562,6 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
|
||||
// so resolve by current links would miss this new connection.
|
||||
// Keep the earliest bound view once present, and only bind from event
|
||||
// payload when this input has no representative yet.
|
||||
const nodeId = String(e.detail.node.id)
|
||||
const source: PromotedWidgetSource = {
|
||||
sourceNodeId: nodeId,
|
||||
sourceWidgetName: e.detail.widget.name
|
||||
}
|
||||
if (
|
||||
usePromotionStore().isPromoted(this.rootGraph.id, this.id, source)
|
||||
) {
|
||||
usePromotionStore().demote(this.rootGraph.id, this.id, source)
|
||||
}
|
||||
|
||||
const boundWidget =
|
||||
input._widget && isPromotedWidgetView(input._widget)
|
||||
? input._widget
|
||||
@@ -1078,39 +749,11 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
|
||||
this._promotedViewManager.clear()
|
||||
this._invalidatePromotedViewsCache()
|
||||
|
||||
// ADR 0009: hydrate the runtime PromotionStore index from any legacy
|
||||
// properties.proxyWidgets present on this host. Hydration is read-only
|
||||
// — we no longer write back resolved entries to the property; the
|
||||
// post-configure flush in LGraph.configure forward-ratchets the
|
||||
// payload into canonical state.
|
||||
const raw = parseProxyWidgets(this.properties.proxyWidgets)
|
||||
const store = usePromotionStore()
|
||||
|
||||
const entries = raw
|
||||
.map(([nodeId, widgetName, sourceNodeId]) => {
|
||||
if (nodeId === '-1') {
|
||||
const resolved = this._resolveLegacyEntry(widgetName)
|
||||
if (resolved)
|
||||
return { sourceNodeId: resolved[0], sourceWidgetName: resolved[1] }
|
||||
if (import.meta.env.DEV) {
|
||||
console.warn(
|
||||
`[SubgraphNode] Failed to resolve legacy -1 entry for widget "${widgetName}"`
|
||||
)
|
||||
}
|
||||
return null
|
||||
}
|
||||
if (!this.subgraph.getNodeById(nodeId)) return null
|
||||
|
||||
return normalizeLegacyProxyWidgetEntry(
|
||||
this,
|
||||
nodeId,
|
||||
widgetName,
|
||||
sourceNodeId
|
||||
)
|
||||
})
|
||||
.filter((e): e is NonNullable<typeof e> => e !== null)
|
||||
|
||||
store.setPromotions(this.rootGraph.id, this.id, entries)
|
||||
usePreviewExposureStore().setExposures(
|
||||
this.rootGraph.id,
|
||||
createNodeLocatorId(this.rootGraph.id, this.id),
|
||||
parsePreviewExposures(this.properties.previewExposures)
|
||||
)
|
||||
|
||||
// Check all inputs for connected widgets
|
||||
for (const input of this.inputs) {
|
||||
@@ -1132,12 +775,20 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
|
||||
|
||||
for (const node of this.subgraph.nodes) {
|
||||
if (!supportsVirtualCanvasImagePreview(node)) continue
|
||||
const source: PromotedWidgetSource = {
|
||||
const hostLocator = createNodeLocatorId(this.rootGraph.id, this.id)
|
||||
const previewStore = usePreviewExposureStore()
|
||||
const existing = previewStore
|
||||
.getExposures(this.rootGraph.id, hostLocator)
|
||||
.some(
|
||||
(exposure) =>
|
||||
exposure.sourceNodeId === String(node.id) &&
|
||||
exposure.sourcePreviewName === CANVAS_IMAGE_PREVIEW_WIDGET
|
||||
)
|
||||
if (existing) continue
|
||||
previewStore.addExposure(this.rootGraph.id, hostLocator, {
|
||||
sourceNodeId: String(node.id),
|
||||
sourceWidgetName: CANVAS_IMAGE_PREVIEW_WIDGET
|
||||
}
|
||||
if (store.isPromoted(this.rootGraph.id, this.id, source)) continue
|
||||
store.promote(this.rootGraph.id, this.id, source)
|
||||
sourcePreviewName: CANVAS_IMAGE_PREVIEW_WIDGET
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1217,7 +868,6 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
|
||||
interiorNode: LGraphNode
|
||||
) {
|
||||
this._invalidatePromotedViewsCache()
|
||||
this._flushPendingPromotions()
|
||||
|
||||
const nodeId = String(interiorNode.id)
|
||||
const widgetName = interiorWidget.name
|
||||
@@ -1234,34 +884,9 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
|
||||
(previousView.sourceNodeId !== nodeId ||
|
||||
previousView.sourceWidgetName !== widgetName)
|
||||
) {
|
||||
usePromotionStore().demote(this.rootGraph.id, this.id, previousView)
|
||||
this._removePromotedView(previousView)
|
||||
}
|
||||
|
||||
if (this.id === -1) {
|
||||
if (
|
||||
!this._pendingPromotions.some(
|
||||
(entry) =>
|
||||
entry.sourceNodeId === nodeId &&
|
||||
entry.sourceWidgetName === widgetName &&
|
||||
entry.disambiguatingSourceNodeId === sourceNodeId
|
||||
)
|
||||
) {
|
||||
this._pendingPromotions.push({
|
||||
sourceNodeId: nodeId,
|
||||
sourceWidgetName: widgetName,
|
||||
...(sourceNodeId && { disambiguatingSourceNodeId: sourceNodeId })
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// Add to promotion store
|
||||
usePromotionStore().promote(this.rootGraph.id, this.id, {
|
||||
sourceNodeId: nodeId,
|
||||
sourceWidgetName: widgetName,
|
||||
disambiguatingSourceNodeId: sourceNodeId
|
||||
})
|
||||
}
|
||||
|
||||
// Create/retrieve the view from cache.
|
||||
// The cache key uses `input.name` (the slot's internal name) rather
|
||||
// than `subgraphInput.name` because nested subgraphs may remap
|
||||
@@ -1306,18 +931,7 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
|
||||
})
|
||||
}
|
||||
|
||||
private _flushPendingPromotions() {
|
||||
if (this.id === -1 || this._pendingPromotions.length === 0) return
|
||||
|
||||
for (const entry of this._pendingPromotions) {
|
||||
usePromotionStore().promote(this.rootGraph.id, this.id, entry)
|
||||
}
|
||||
|
||||
this._pendingPromotions = []
|
||||
}
|
||||
|
||||
override onAdded(_graph: LGraph): void {
|
||||
this._flushPendingPromotions()
|
||||
this._syncPromotions()
|
||||
}
|
||||
|
||||
@@ -1513,7 +1127,6 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
|
||||
override ensureWidgetRemoved(widget: IBaseWidget): void {
|
||||
if (isPromotedWidgetView(widget)) {
|
||||
this._clearDomOverrideForView(widget)
|
||||
usePromotionStore().demote(this.rootGraph.id, this.id, widget)
|
||||
this._removePromotedView(widget)
|
||||
}
|
||||
for (const input of this.inputs) {
|
||||
@@ -1544,7 +1157,6 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
|
||||
})
|
||||
}
|
||||
|
||||
usePromotionStore().setPromotions(this.rootGraph.id, this.id, [])
|
||||
this._promotedViewManager.clear()
|
||||
|
||||
for (const input of this.inputs) {
|
||||
|
||||
@@ -17,7 +17,6 @@ import type {
|
||||
NodeBindable,
|
||||
TWidgetType
|
||||
} from '@/lib/litegraph/src/types/widgets'
|
||||
import { usePromotionStore } from '@/stores/promotionStore'
|
||||
import type { WidgetState } from '@/stores/widgetValueStore'
|
||||
import { useWidgetValueStore } from '@/stores/widgetValueStore'
|
||||
|
||||
@@ -26,7 +25,6 @@ export interface DrawWidgetOptions {
|
||||
width: number
|
||||
/** Synonym for "low quality". */
|
||||
showText?: boolean
|
||||
/** When true, suppresses the promoted outline color (e.g. for projected copies on SubgraphNode). */
|
||||
suppressPromotedOutline?: boolean
|
||||
/** Transient image source for preview widgets rendered on behalf of another node (e.g. subgraph promotion). */
|
||||
previewImages?: HTMLImageElement[]
|
||||
@@ -206,17 +204,7 @@ export abstract class BaseWidget<TWidget extends IBaseWidget = IBaseWidget>
|
||||
}
|
||||
}
|
||||
|
||||
getOutlineColor(suppressPromotedOutline = false) {
|
||||
const graphId = this.node.graph?.rootGraph.id
|
||||
if (
|
||||
graphId &&
|
||||
!suppressPromotedOutline &&
|
||||
usePromotionStore().isPromotedByAny(graphId, {
|
||||
sourceNodeId: String(this.node.id),
|
||||
sourceWidgetName: this.name
|
||||
})
|
||||
)
|
||||
return LiteGraph.WIDGET_PROMOTED_OUTLINE_COLOR
|
||||
getOutlineColor(_suppressPromotedOutline = false) {
|
||||
return this.advanced
|
||||
? LiteGraph.WIDGET_ADVANCED_OUTLINE_COLOR
|
||||
: LiteGraph.WIDGET_OUTLINE_COLOR
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
hasWidgetError,
|
||||
isWidgetVisible
|
||||
} from '@/renderer/extensions/vueNodes/composables/useProcessedWidgets'
|
||||
import { usePromotionStore } from '@/stores/promotionStore'
|
||||
import { useExecutionErrorStore } from '@/stores/executionErrorStore'
|
||||
import { useMissingModelStore } from '@/platform/missingModel/missingModelStore'
|
||||
import { useWidgetValueStore } from '@/stores/widgetValueStore'
|
||||
@@ -207,20 +206,15 @@ describe('computeProcessedWidgets borderStyle', () => {
|
||||
setActivePinia(createTestingPinia({ stubActions: false }))
|
||||
})
|
||||
|
||||
it('applies promoted border styling to intermediate promoted widgets', () => {
|
||||
it('does not apply border styling to promoted widgets', () => {
|
||||
const promotedWidget = createMockWidget({
|
||||
name: 'text',
|
||||
type: 'combo',
|
||||
nodeId: 'inner-subgraph:1',
|
||||
storeNodeId: 'inner-subgraph:1',
|
||||
storeName: 'text',
|
||||
slotName: 'text'
|
||||
})
|
||||
|
||||
usePromotionStore().promote('graph-test', '4', {
|
||||
sourceNodeId: '3',
|
||||
sourceWidgetName: 'text',
|
||||
disambiguatingSourceNodeId: '1'
|
||||
slotName: 'text',
|
||||
promotedLabel: 'Text'
|
||||
})
|
||||
|
||||
const result = computeProcessedWidgets({
|
||||
@@ -242,13 +236,12 @@ describe('computeProcessedWidgets borderStyle', () => {
|
||||
ui: noopUi
|
||||
})
|
||||
|
||||
expect(
|
||||
result.some((w) => w.simplified.borderStyle?.includes('promoted'))
|
||||
).toBe(true)
|
||||
expect(result[0].simplified.borderStyle).toBeUndefined()
|
||||
expect(result[0].simplified.label).toBe('Text')
|
||||
})
|
||||
|
||||
it('does not apply promoted border styling to outermost widgets', () => {
|
||||
const promotedWidget = createMockWidget({
|
||||
it('does not apply border styling to regular widgets', () => {
|
||||
const widget = createMockWidget({
|
||||
name: 'text',
|
||||
type: 'combo',
|
||||
nodeId: 'inner-subgraph:1',
|
||||
@@ -257,17 +250,11 @@ describe('computeProcessedWidgets borderStyle', () => {
|
||||
slotName: 'text'
|
||||
})
|
||||
|
||||
usePromotionStore().promote('graph-test', '4', {
|
||||
sourceNodeId: '3',
|
||||
sourceWidgetName: 'text',
|
||||
disambiguatingSourceNodeId: '1'
|
||||
})
|
||||
|
||||
const result = computeProcessedWidgets({
|
||||
nodeData: {
|
||||
id: '4',
|
||||
type: 'SubgraphNode',
|
||||
widgets: [promotedWidget],
|
||||
widgets: [widget],
|
||||
title: 'Test',
|
||||
mode: 0,
|
||||
selected: false,
|
||||
|
||||
@@ -29,7 +29,6 @@ import {
|
||||
stripGraphPrefix,
|
||||
useWidgetValueStore
|
||||
} from '@/stores/widgetValueStore'
|
||||
import { usePromotionStore } from '@/stores/promotionStore'
|
||||
import { useMissingModelStore } from '@/platform/missingModel/missingModelStore'
|
||||
import { useExecutionErrorStore } from '@/stores/executionErrorStore'
|
||||
import type { LGraph } from '@/lib/litegraph/src/litegraph'
|
||||
@@ -167,7 +166,6 @@ export function computeProcessedWidgets({
|
||||
}: ComputeProcessedWidgetsOptions): ProcessedWidget[] {
|
||||
if (!nodeData?.widgets) return []
|
||||
|
||||
const promotionStore = usePromotionStore()
|
||||
const executionErrorStore = useExecutionErrorStore()
|
||||
const missingModelStore = useMissingModelStore()
|
||||
const widgetValueStore = useWidgetValueStore()
|
||||
@@ -249,13 +247,9 @@ export function computeProcessedWidgets({
|
||||
widgetState,
|
||||
identity: { renderKey }
|
||||
} of uniqueWidgets) {
|
||||
const hostNodeId = String(nodeId ?? '')
|
||||
const bareWidgetId = String(
|
||||
stripGraphPrefix(widget.storeNodeId ?? widget.nodeId ?? nodeId ?? '')
|
||||
)
|
||||
const promotionSourceNodeId = widget.storeName
|
||||
? String(bareWidgetId)
|
||||
: undefined
|
||||
|
||||
const vueComponent =
|
||||
getComponent(widget.type) ||
|
||||
@@ -270,17 +264,9 @@ export function computeProcessedWidgets({
|
||||
? { ...mergedOptions, disabled: true }
|
||||
: mergedOptions
|
||||
|
||||
const borderStyle =
|
||||
graphId &&
|
||||
promotionStore.isPromotedByAny(graphId, {
|
||||
sourceNodeId: hostNodeId,
|
||||
sourceWidgetName: widget.storeName ?? widget.name,
|
||||
disambiguatingSourceNodeId: promotionSourceNodeId
|
||||
})
|
||||
? 'ring ring-component-node-widget-promoted'
|
||||
: mergedOptions.advanced
|
||||
? 'ring ring-component-node-widget-advanced'
|
||||
: undefined
|
||||
const borderStyle = mergedOptions.advanced
|
||||
? 'ring ring-component-node-widget-advanced'
|
||||
: undefined
|
||||
|
||||
const linkedUpstream: LinkedUpstreamInfo | undefined =
|
||||
slotMetadata?.linked && slotMetadata.originNodeId
|
||||
|
||||
@@ -3,8 +3,6 @@ import { beforeEach, describe, expect, test, vi } from 'vitest'
|
||||
import { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { ComponentWidgetImpl, DOMWidgetImpl } from '@/scripts/domWidget'
|
||||
|
||||
const isPromotedByAnyMock = vi.hoisted(() => vi.fn())
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock('@/stores/domWidgetStore', () => ({
|
||||
useDomWidgetStore: () => ({
|
||||
@@ -12,12 +10,6 @@ vi.mock('@/stores/domWidgetStore', () => ({
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/stores/promotionStore', () => ({
|
||||
usePromotionStore: () => ({
|
||||
isPromotedByAny: isPromotedByAnyMock
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/utils/formatUtil', () => ({
|
||||
generateUUID: () => 'test-uuid'
|
||||
}))
|
||||
@@ -114,41 +106,12 @@ describe('DOMWidget Y Position Preservation', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('DOMWidget draw promotion behavior', () => {
|
||||
describe('DOMWidget draw behavior', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
test('draws promoted outline for visible promoted widgets', () => {
|
||||
isPromotedByAnyMock.mockReturnValue(true)
|
||||
|
||||
const node = new LGraphNode('test-node')
|
||||
const rootGraph = { id: 'root-graph-id' }
|
||||
node.graph = { rootGraph } as never
|
||||
const onDraw = vi.fn()
|
||||
|
||||
const widget = new DOMWidgetImpl({
|
||||
node,
|
||||
name: 'seed',
|
||||
type: 'text',
|
||||
element: document.createElement('div'),
|
||||
options: { onDraw }
|
||||
})
|
||||
const ctx = createMockContext()
|
||||
|
||||
widget.draw(ctx as CanvasRenderingContext2D, node, 200, 30, 40)
|
||||
|
||||
expect(isPromotedByAnyMock).toHaveBeenCalledWith('root-graph-id', {
|
||||
sourceNodeId: '-1',
|
||||
sourceWidgetName: 'seed'
|
||||
})
|
||||
expect(ctx.strokeRect).toHaveBeenCalledOnce()
|
||||
expect(onDraw).toHaveBeenCalledWith(widget)
|
||||
})
|
||||
|
||||
test('does not draw promoted outline when widget is not promoted', () => {
|
||||
isPromotedByAnyMock.mockReturnValue(false)
|
||||
|
||||
test('does not draw an outline for visible widgets', () => {
|
||||
const node = new LGraphNode('test-node')
|
||||
const rootGraph = { id: 'root-graph-id' }
|
||||
node.graph = { rootGraph } as never
|
||||
@@ -187,7 +150,6 @@ describe('DOMWidget draw promotion behavior', () => {
|
||||
|
||||
widget.draw(ctx as CanvasRenderingContext2D, node, 200, 30, 40)
|
||||
|
||||
expect(isPromotedByAnyMock).not.toHaveBeenCalled()
|
||||
expect(ctx.strokeRect).not.toHaveBeenCalled()
|
||||
expect(onDraw).toHaveBeenCalledWith(widget)
|
||||
})
|
||||
|
||||
@@ -13,7 +13,6 @@ import type {
|
||||
} from '@/lib/litegraph/src/types/widgets'
|
||||
import type { InputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import { useDomWidgetStore } from '@/stores/domWidgetStore'
|
||||
import { usePromotionStore } from '@/stores/promotionStore'
|
||||
import { generateUUID } from '@/utils/formatUtil'
|
||||
|
||||
export interface BaseDOMWidget<
|
||||
@@ -125,7 +124,6 @@ abstract class BaseDOMWidgetImpl<V extends object | string>
|
||||
declare readonly name: string
|
||||
declare readonly options: DOMWidgetOptions<V>
|
||||
declare callback?: (value: V) => void
|
||||
readonly promotionStore = usePromotionStore()
|
||||
|
||||
readonly id: string
|
||||
|
||||
@@ -186,30 +184,6 @@ abstract class BaseDOMWidgetImpl<V extends object | string>
|
||||
this.options.onDraw?.(this)
|
||||
return
|
||||
}
|
||||
|
||||
const graphId = this.node.graph?.rootGraph.id
|
||||
const isPromoted =
|
||||
graphId &&
|
||||
this.promotionStore.isPromotedByAny(graphId, {
|
||||
sourceNodeId: String(this.node.id),
|
||||
sourceWidgetName: this.name
|
||||
})
|
||||
if (!isPromoted) {
|
||||
this.options.onDraw?.(this)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.save()
|
||||
const adjustedMargin = this.margin - 1
|
||||
ctx.beginPath()
|
||||
ctx.strokeStyle = LiteGraph.WIDGET_PROMOTED_OUTLINE_COLOR
|
||||
ctx.strokeRect(
|
||||
adjustedMargin,
|
||||
y + adjustedMargin,
|
||||
widget_width - adjustedMargin * 2,
|
||||
(this.computedHeight ?? widget_height) - 2 * adjustedMargin
|
||||
)
|
||||
ctx.restore()
|
||||
}
|
||||
this.options.onDraw?.(this)
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ import { useDomWidgetStore } from '@/stores/domWidgetStore'
|
||||
import { useExecutionStore } from '@/stores/executionStore'
|
||||
import { useNodeOutputStore } from '@/stores/nodeOutputStore'
|
||||
import { ComfyNodeDefImpl } from '@/stores/nodeDefStore'
|
||||
import { usePromotionStore } from '@/stores/promotionStore'
|
||||
import { usePreviewExposureStore } from '@/stores/previewExposureStore'
|
||||
import { useSubgraphStore } from '@/stores/subgraphStore'
|
||||
import { useFavoritedWidgetsStore } from '@/stores/workspace/favoritedWidgetsStore'
|
||||
import { useRightSidePanelStore } from '@/stores/workspace/rightSidePanelStore'
|
||||
@@ -70,6 +70,7 @@ import {
|
||||
isVideoOutput,
|
||||
migrateWidgetsValues
|
||||
} from '@/utils/litegraphUtil'
|
||||
import { createNodeLocatorId } from '@/types/nodeIdentification'
|
||||
import { getOrderedInputSpecs } from '@/workbench/utils/nodeDefOrderingUtil'
|
||||
|
||||
import { useExtensionService } from './extensionService'
|
||||
@@ -187,11 +188,13 @@ export const useLitegraphService = () => {
|
||||
}
|
||||
|
||||
function getPseudoWidgetPreviewTargets(node: SubgraphNode): LGraphNode[] {
|
||||
const promotionStore = usePromotionStore()
|
||||
const promotions = promotionStore.getPromotionsRef(
|
||||
node.rootGraph.id,
|
||||
node.id
|
||||
)
|
||||
const hostLocator = createNodeLocatorId(node.rootGraph.id, node.id)
|
||||
const promotions = usePreviewExposureStore()
|
||||
.getExposures(node.rootGraph.id, hostLocator)
|
||||
.map((exposure) => ({
|
||||
sourceNodeId: exposure.sourceNodeId,
|
||||
sourceWidgetName: exposure.sourcePreviewName
|
||||
}))
|
||||
const resolved = resolveSubgraphPseudoWidgetCache({
|
||||
cache: subgraphPseudoWidgetCache.get(node) ?? null,
|
||||
promotions,
|
||||
|
||||
Reference in New Issue
Block a user