mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-17 21:21:06 +00:00
Compare commits
4 Commits
test/stand
...
feat/vue-n
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
06f1a80479 | ||
|
|
45ce156e2a | ||
|
|
a3f8b50bbd | ||
|
|
10424bdd00 |
@@ -1,9 +1,9 @@
|
||||
<!-- Reference:
|
||||
https://github.com/Nuked88/ComfyUI-N-Sidebar/blob/7ae7da4a9761009fb6629bc04c683087a3e168db/app/js/functions/sb_fn.js#L149
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div class="_sb_node_preview">
|
||||
<LGraphNodePreview v-if="shouldRenderVueNodes" :node-def="nodeDef" />
|
||||
<div v-else class="_sb_node_preview">
|
||||
<div class="_sb_table">
|
||||
<div
|
||||
class="node_header text-ellipsis mr-4"
|
||||
@@ -85,6 +85,8 @@ https://github.com/Nuked88/ComfyUI-N-Sidebar/blob/7ae7da4a9761009fb6629bc04c6830
|
||||
import _ from 'es-toolkit/compat'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { useVueFeatureFlags } from '@/composables/useVueFeatureFlags'
|
||||
import LGraphNodePreview from '@/renderer/extensions/vueNodes/components/LGraphNodePreview.vue'
|
||||
import type { ComfyNodeDef as ComfyNodeDefV2 } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import { useWidgetStore } from '@/stores/widgetStore'
|
||||
import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'
|
||||
@@ -94,6 +96,8 @@ const { nodeDef } = defineProps<{
|
||||
nodeDef: ComfyNodeDefV2
|
||||
}>()
|
||||
|
||||
const { shouldRenderVueNodes } = useVueFeatureFlags()
|
||||
|
||||
const colorPaletteStore = useColorPaletteStore()
|
||||
const litegraphColors = computed(
|
||||
() => colorPaletteStore.completedActivePalette.colors.litegraph_base
|
||||
|
||||
@@ -2,137 +2,39 @@
|
||||
<div v-if="renderError" class="node-error p-2 text-red-500 text-sm">
|
||||
{{ $t('Node Render Error') }}
|
||||
</div>
|
||||
<div
|
||||
<NodeBaseTemplate
|
||||
v-else
|
||||
ref="nodeContainerRef"
|
||||
:data-node-id="nodeData.id"
|
||||
:class="
|
||||
cn(
|
||||
'bg-white dark-theme:bg-charcoal-800',
|
||||
'lg-node absolute rounded-2xl',
|
||||
'border border-solid border-sand-100 dark-theme:border-charcoal-600',
|
||||
// hover (only when node should handle events)
|
||||
shouldHandleNodePointerEvents &&
|
||||
'hover:ring-7 ring-gray-500/50 dark-theme:ring-gray-500/20',
|
||||
'outline-transparent -outline-offset-2 outline-2',
|
||||
borderClass,
|
||||
outlineClass,
|
||||
{
|
||||
'animate-pulse': executing,
|
||||
'opacity-50 before:rounded-2xl before:pointer-events-none before:absolute before:bg-bypass/60 before:inset-0':
|
||||
bypassed,
|
||||
'will-change-transform': isDragging
|
||||
},
|
||||
|
||||
shouldHandleNodePointerEvents
|
||||
? 'pointer-events-auto'
|
||||
: 'pointer-events-none'
|
||||
)
|
||||
"
|
||||
:style="[
|
||||
{
|
||||
transform: `translate(${position.x ?? 0}px, ${(position.y ?? 0) - LiteGraph.NODE_TITLE_HEIGHT}px)`,
|
||||
zIndex: zIndex
|
||||
},
|
||||
dragStyle
|
||||
]"
|
||||
@pointerdown="handlePointerDown"
|
||||
@pointermove="handlePointerMove"
|
||||
@pointerup="handlePointerUp"
|
||||
@wheel="handleWheel"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<template v-if="isCollapsed">
|
||||
<SlotConnectionDot multi class="absolute left-0 -translate-x-1/2" />
|
||||
<SlotConnectionDot multi class="absolute right-0 translate-x-1/2" />
|
||||
</template>
|
||||
<!-- Header only updates on title/color changes -->
|
||||
<NodeHeader
|
||||
v-memo="[nodeData.title, isCollapsed]"
|
||||
:node-data="nodeData"
|
||||
:readonly="readonly"
|
||||
:collapsed="isCollapsed"
|
||||
@collapse="handleCollapse"
|
||||
@update:title="handleHeaderTitleUpdate"
|
||||
@enter-subgraph="handleEnterSubgraph"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="isCollapsed && executing && progress !== undefined"
|
||||
:class="
|
||||
cn(
|
||||
'absolute inset-x-4 -bottom-[1px] translate-y-1/2 rounded-full',
|
||||
progressClasses
|
||||
)
|
||||
"
|
||||
:style="{ width: `${Math.min(progress * 100, 100)}%` }"
|
||||
/>
|
||||
|
||||
<template v-if="!isCollapsed">
|
||||
<div class="mb-4 relative">
|
||||
<div :class="separatorClasses" />
|
||||
<!-- Progress bar for executing state -->
|
||||
<div
|
||||
v-if="executing && progress !== undefined"
|
||||
:class="
|
||||
cn(
|
||||
'absolute inset-x-0 top-1/2 -translate-y-1/2',
|
||||
!!(progress < 1) && 'rounded-r-full',
|
||||
progressClasses
|
||||
)
|
||||
"
|
||||
:style="{ width: `${Math.min(progress * 100, 100)}%` }"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Node Body - rendered based on LOD level and collapsed state -->
|
||||
<div
|
||||
class="flex flex-col gap-4 pb-4"
|
||||
:data-testid="`node-body-${nodeData.id}`"
|
||||
>
|
||||
<!-- Slots only rendered at full detail -->
|
||||
<NodeSlots
|
||||
v-memo="[nodeData.inputs?.length, nodeData.outputs?.length]"
|
||||
:node-data="nodeData"
|
||||
:readonly="readonly"
|
||||
/>
|
||||
|
||||
<!-- Widgets rendered at reduced+ detail -->
|
||||
<NodeWidgets
|
||||
v-if="nodeData.widgets?.length"
|
||||
v-memo="[nodeData.widgets?.length]"
|
||||
:node-data="nodeData"
|
||||
:readonly="readonly"
|
||||
/>
|
||||
|
||||
<!-- Custom content at reduced+ detail -->
|
||||
<NodeContent
|
||||
v-if="hasCustomContent"
|
||||
:node-data="nodeData"
|
||||
:readonly="readonly"
|
||||
:image-urls="nodeImageUrls"
|
||||
/>
|
||||
<!-- Live preview image -->
|
||||
<div
|
||||
v-if="shouldShowPreviewImg"
|
||||
v-memo="[latestPreviewUrl]"
|
||||
class="px-4"
|
||||
>
|
||||
<img
|
||||
:src="latestPreviewUrl"
|
||||
alt="preview"
|
||||
class="w-full max-h-64 object-contain"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
:node-data="nodeData"
|
||||
:readonly="readonly"
|
||||
:container-classes="containerClasses"
|
||||
:container-style="containerStyle"
|
||||
:is-collapsed="isCollapsed"
|
||||
:separator-classes="separatorClasses"
|
||||
:progress-classes="progressClasses"
|
||||
:progress-bar-classes="progressBarClasses"
|
||||
:show-progress="showProgress"
|
||||
:progress-value="progress"
|
||||
:progress-style="progressStyle"
|
||||
:progress-bar-style="progressBarStyle"
|
||||
:has-custom-content="hasCustomContent"
|
||||
:image-urls="nodeImageUrls"
|
||||
:show-preview-image="shouldShowPreviewImg"
|
||||
:preview-image-url="latestPreviewUrl"
|
||||
:event-handlers="{
|
||||
onPointerdown: handlePointerDown,
|
||||
onPointermove: handlePointerMove,
|
||||
onPointerup: handlePointerUp,
|
||||
onWheel: handleWheel
|
||||
}"
|
||||
@collapse="handleCollapse"
|
||||
@update:title="handleHeaderTitleUpdate"
|
||||
@enter-subgraph="handleEnterSubgraph"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { computed, inject, onErrorCaptured, onMounted, provide, ref } from 'vue'
|
||||
import { computed, inject, onErrorCaptured, onMounted, ref } from 'vue'
|
||||
|
||||
import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
|
||||
import { useErrorHandling } from '@/composables/useErrorHandling'
|
||||
@@ -155,11 +57,7 @@ import {
|
||||
} from '@/utils/graphTraversalUtil'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
import NodeContent from './NodeContent.vue'
|
||||
import NodeHeader from './NodeHeader.vue'
|
||||
import NodeSlots from './NodeSlots.vue'
|
||||
import NodeWidgets from './NodeWidgets.vue'
|
||||
import SlotConnectionDot from './SlotConnectionDot.vue'
|
||||
import NodeBaseTemplate from './NodeBaseTemplate.vue'
|
||||
|
||||
// Extended props for main node component
|
||||
interface LGraphNodeProps {
|
||||
@@ -240,27 +138,23 @@ onMounted(() => {
|
||||
}
|
||||
})
|
||||
|
||||
// Track collapsed state
|
||||
// Collapsed state
|
||||
const isCollapsed = computed(() => nodeData.flags?.collapsed ?? false)
|
||||
|
||||
// Check if node has custom content (like image outputs)
|
||||
const hasCustomContent = computed(() => {
|
||||
// Show custom content if node has image outputs
|
||||
return nodeImageUrls.value.length > 0
|
||||
// Show progress when executing with defined progress
|
||||
const showProgress = computed(() => {
|
||||
return !!(executing.value && progress.value !== undefined)
|
||||
})
|
||||
|
||||
// Computed classes and conditions for better reusability
|
||||
const separatorClasses =
|
||||
'bg-sand-100 dark-theme:bg-charcoal-600 h-px mx-0 w-full lod-toggle'
|
||||
const progressClasses = 'h-2 bg-primary-500 transition-all duration-300'
|
||||
// Progress styles
|
||||
const progressStyle = computed(() => {
|
||||
if (!showProgress.value || !progress.value) return undefined
|
||||
return { width: `${Math.min(progress.value * 100, 100)}%` }
|
||||
})
|
||||
|
||||
const { latestPreviewUrl, shouldShowPreviewImg } = useNodePreviewState(
|
||||
() => nodeData.id,
|
||||
{
|
||||
isCollapsed
|
||||
}
|
||||
)
|
||||
const progressBarStyle = progressStyle
|
||||
|
||||
// Border class based on state
|
||||
const borderClass = computed(() => {
|
||||
if (hasAnyError.value) {
|
||||
return 'border-error'
|
||||
@@ -271,6 +165,7 @@ const borderClass = computed(() => {
|
||||
return undefined
|
||||
})
|
||||
|
||||
// Outline class based on selection and state
|
||||
const outlineClass = computed(() => {
|
||||
if (!isSelected.value) {
|
||||
return undefined
|
||||
@@ -284,6 +179,64 @@ const outlineClass = computed(() => {
|
||||
return 'outline-black dark-theme:outline-white'
|
||||
})
|
||||
|
||||
// Container classes
|
||||
const containerClasses = computed(() => {
|
||||
return cn(
|
||||
'bg-white dark-theme:bg-charcoal-800',
|
||||
'lg-node absolute rounded-2xl',
|
||||
'border border-solid border-sand-100 dark-theme:border-charcoal-600',
|
||||
// hover (only when node should handle events)
|
||||
shouldHandleNodePointerEvents.value &&
|
||||
'hover:ring-7 ring-gray-500/50 dark-theme:ring-gray-500/20',
|
||||
'outline-transparent -outline-offset-2 outline-2',
|
||||
borderClass.value,
|
||||
outlineClass.value,
|
||||
{
|
||||
'animate-pulse': executing.value,
|
||||
'opacity-50 before:rounded-2xl before:pointer-events-none before:absolute before:bg-bypass/60 before:inset-0':
|
||||
bypassed.value,
|
||||
'will-change-transform': isDragging.value
|
||||
},
|
||||
shouldHandleNodePointerEvents.value
|
||||
? 'pointer-events-auto'
|
||||
: 'pointer-events-none'
|
||||
)
|
||||
})
|
||||
|
||||
const progressBarClasses = computed(() => {
|
||||
return cn(
|
||||
'absolute inset-x-4 -bottom-[1px] translate-y-1/2 rounded-full',
|
||||
progressClasses
|
||||
)
|
||||
})
|
||||
|
||||
// Static classes
|
||||
const separatorClasses =
|
||||
'bg-sand-100 dark-theme:bg-charcoal-600 h-px mx-0 w-full lod-toggle'
|
||||
const progressClasses = 'h-2 bg-primary-500 transition-all duration-300'
|
||||
|
||||
// Container style combining position and drag styles
|
||||
const containerStyle = computed(() => [
|
||||
{
|
||||
transform: `translate(${position.value.x ?? 0}px, ${(position.value.y ?? 0) - LiteGraph.NODE_TITLE_HEIGHT}px)`,
|
||||
zIndex: zIndex.value
|
||||
},
|
||||
dragStyle.value
|
||||
])
|
||||
|
||||
// Check if node has custom content (like image outputs)
|
||||
const hasCustomContent = computed(() => {
|
||||
// Show custom content if node has image outputs
|
||||
return nodeImageUrls.value.length > 0
|
||||
})
|
||||
|
||||
const { latestPreviewUrl, shouldShowPreviewImg } = useNodePreviewState(
|
||||
() => nodeData.id,
|
||||
{
|
||||
isCollapsed
|
||||
}
|
||||
)
|
||||
|
||||
// Event handlers
|
||||
const handleCollapse = () => {
|
||||
handleNodeCollapse(nodeData.id, !isCollapsed.value)
|
||||
@@ -344,7 +297,4 @@ const nodeImageUrls = computed(() => {
|
||||
// Clear URLs if no outputs or no images
|
||||
return []
|
||||
})
|
||||
|
||||
const nodeContainerRef = ref()
|
||||
provide('tooltipContainer', nodeContainerRef)
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
<template>
|
||||
<div class="scale-75">
|
||||
<NodeBaseTemplate
|
||||
:node-data="nodeData"
|
||||
:readonly="true"
|
||||
:container-classes="containerClasses"
|
||||
:is-collapsed="false"
|
||||
:separator-classes="separatorClasses"
|
||||
:has-custom-content="false"
|
||||
:image-urls="[]"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
|
||||
import type { ComfyNodeDef as ComfyNodeDefV2 } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import { useWidgetStore } from '@/stores/widgetStore'
|
||||
|
||||
import NodeBaseTemplate from './NodeBaseTemplate.vue'
|
||||
|
||||
const { nodeDef } = defineProps<{
|
||||
nodeDef: ComfyNodeDefV2
|
||||
}>()
|
||||
|
||||
const widgetStore = useWidgetStore()
|
||||
|
||||
// Convert nodeDef into VueNodeData
|
||||
const nodeData = computed<VueNodeData>(() => {
|
||||
// Convert inputs to widgets (those that have widget constructors)
|
||||
const widgets = Object.entries(nodeDef.inputs || {})
|
||||
.filter(([_, input]) => widgetStore.inputIsWidget(input))
|
||||
.map(([name, input]) => ({
|
||||
name,
|
||||
type: input.widgetType || input.type,
|
||||
value:
|
||||
input.default !== undefined
|
||||
? input.default
|
||||
: input.type === 'COMBO' &&
|
||||
Array.isArray(input.options) &&
|
||||
input.options.length > 0
|
||||
? input.options[0]
|
||||
: undefined,
|
||||
options: {
|
||||
...input,
|
||||
hidden: input.hidden,
|
||||
advanced: input.advanced,
|
||||
values: input.type === 'COMBO' ? input.options : undefined // For combo widgets
|
||||
}
|
||||
}))
|
||||
|
||||
// Filter non-widget inputs for slots
|
||||
const inputs = Object.entries(nodeDef.inputs || {})
|
||||
.filter(([_, input]) => !widgetStore.inputIsWidget(input))
|
||||
.map(([name, input]) => ({
|
||||
name,
|
||||
type: input.type,
|
||||
shape: input.isOptional ? 'HollowCircle' : undefined
|
||||
}))
|
||||
|
||||
return {
|
||||
id: `preview-${nodeDef.name}`,
|
||||
title: nodeDef.display_name || nodeDef.name,
|
||||
type: nodeDef.name,
|
||||
mode: 0, // Normal mode
|
||||
selected: false,
|
||||
executing: false,
|
||||
widgets,
|
||||
inputs,
|
||||
outputs: nodeDef.outputs || [],
|
||||
flags: {
|
||||
collapsed: false
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Static classes for preview mode
|
||||
const containerClasses =
|
||||
'bg-white dark-theme:bg-charcoal-800 lg-node absolute rounded-2xl border border-solid border-sand-100 dark-theme:border-charcoal-600 outline-transparent -outline-offset-2 outline-2 pointer-events-none'
|
||||
const separatorClasses =
|
||||
'bg-sand-100 dark-theme:bg-charcoal-600 h-px mx-0 w-full'
|
||||
</script>
|
||||
158
src/renderer/extensions/vueNodes/components/NodeBaseTemplate.vue
Normal file
158
src/renderer/extensions/vueNodes/components/NodeBaseTemplate.vue
Normal file
@@ -0,0 +1,158 @@
|
||||
<template>
|
||||
<div
|
||||
ref="nodeContainerRef"
|
||||
:data-node-id="nodeData?.id"
|
||||
:class="containerClasses"
|
||||
:style="containerStyle"
|
||||
v-bind="eventHandlers"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<template v-if="isCollapsed">
|
||||
<SlotConnectionDot multi class="absolute left-0 -translate-x-1/2" />
|
||||
<SlotConnectionDot multi class="absolute right-0 translate-x-1/2" />
|
||||
</template>
|
||||
<!-- Header only updates on title/color changes -->
|
||||
<NodeHeader
|
||||
v-memo="[nodeData?.title, isCollapsed]"
|
||||
:node-data="nodeData"
|
||||
:readonly="readonly"
|
||||
:collapsed="isCollapsed"
|
||||
@collapse="emit('collapse')"
|
||||
@update:title="emit('update:title', $event)"
|
||||
@enter-subgraph="emit('enter-subgraph')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="isCollapsed && showProgress"
|
||||
:class="progressBarClasses"
|
||||
:style="progressBarStyle"
|
||||
/>
|
||||
|
||||
<template v-if="!isCollapsed">
|
||||
<div class="mb-4 relative">
|
||||
<div :class="separatorClasses" />
|
||||
<!-- Progress bar for executing state -->
|
||||
<div
|
||||
v-if="showProgress"
|
||||
:class="
|
||||
cn(
|
||||
'absolute inset-x-0 top-1/2 -translate-y-1/2',
|
||||
!!(progressValue && progressValue < 1) && 'rounded-r-full',
|
||||
progressClasses
|
||||
)
|
||||
"
|
||||
:style="progressStyle"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Node Body - rendered based on LOD level and collapsed state -->
|
||||
<div
|
||||
class="flex flex-col gap-4 pb-4"
|
||||
:data-testid="`node-body-${nodeData?.id}`"
|
||||
>
|
||||
<!-- Slots only rendered at full detail -->
|
||||
<NodeSlots
|
||||
v-memo="[nodeData?.inputs?.length, nodeData?.outputs?.length]"
|
||||
:node-data="nodeData"
|
||||
:readonly="readonly"
|
||||
/>
|
||||
|
||||
<!-- Widgets rendered at reduced+ detail -->
|
||||
<NodeWidgets
|
||||
v-if="nodeData?.widgets?.length"
|
||||
v-memo="[nodeData?.widgets?.length]"
|
||||
:node-data="nodeData"
|
||||
:readonly="readonly"
|
||||
/>
|
||||
|
||||
<!-- Custom content at reduced+ detail -->
|
||||
<NodeContent
|
||||
v-if="hasCustomContent"
|
||||
:node-data="nodeData"
|
||||
:readonly="readonly"
|
||||
:image-urls="imageUrls"
|
||||
/>
|
||||
<!-- Live preview image -->
|
||||
<div
|
||||
v-if="showPreviewImage && previewImageUrl"
|
||||
v-memo="[previewImageUrl]"
|
||||
class="px-4"
|
||||
>
|
||||
<img
|
||||
:src="previewImageUrl"
|
||||
alt="preview"
|
||||
class="w-full max-h-64 object-contain"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { provide, ref } from 'vue'
|
||||
|
||||
import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
|
||||
import NodeContent from './NodeContent.vue'
|
||||
import NodeHeader from './NodeHeader.vue'
|
||||
import NodeSlots from './NodeSlots.vue'
|
||||
import NodeWidgets from './NodeWidgets.vue'
|
||||
import SlotConnectionDot from './SlotConnectionDot.vue'
|
||||
|
||||
interface NodeBaseTemplateProps {
|
||||
nodeData?: VueNodeData
|
||||
readonly?: boolean
|
||||
// Presentation state
|
||||
containerClasses?: string
|
||||
containerStyle?: any
|
||||
isCollapsed?: boolean
|
||||
separatorClasses?: string
|
||||
progressClasses?: string
|
||||
progressBarClasses?: string
|
||||
// Progress state
|
||||
showProgress?: boolean
|
||||
progressValue?: number
|
||||
progressStyle?: any
|
||||
progressBarStyle?: any
|
||||
// Content state
|
||||
hasCustomContent?: boolean
|
||||
imageUrls?: string[]
|
||||
showPreviewImage?: boolean
|
||||
previewImageUrl?: string
|
||||
// Event handlers object for v-bind
|
||||
eventHandlers?: Record<string, any>
|
||||
}
|
||||
|
||||
const {
|
||||
nodeData,
|
||||
readonly = false,
|
||||
containerClasses = '',
|
||||
containerStyle = {},
|
||||
isCollapsed = false,
|
||||
separatorClasses = '',
|
||||
progressClasses = '',
|
||||
progressBarClasses = '',
|
||||
showProgress = false,
|
||||
progressValue,
|
||||
progressStyle,
|
||||
progressBarStyle,
|
||||
hasCustomContent = false,
|
||||
imageUrls = [],
|
||||
showPreviewImage = false,
|
||||
previewImageUrl,
|
||||
eventHandlers = {}
|
||||
} = defineProps<NodeBaseTemplateProps>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
collapse: []
|
||||
'update:title': [newTitle: string]
|
||||
'enter-subgraph': []
|
||||
}>()
|
||||
|
||||
// Provide tooltip container for child components
|
||||
const nodeContainerRef = ref<HTMLElement>()
|
||||
provide('tooltipContainer', nodeContainerRef)
|
||||
</script>
|
||||
Reference in New Issue
Block a user