Skip to content

Vue Node Body #4387

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 22 commits into
base: vue-nodes-transform-pane
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions src/components/graph/GraphCanvas.vue
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@
:zoom-level="canvasStore.canvas?.ds?.scale || 1"
:data-node-id="nodeData.id"
@node-click="handleNodeSelect"
@update:collapsed="handleNodeCollapse"
@update:title="handleNodeTitleUpdate"
/>
</TransformPane>

Expand Down Expand Up @@ -458,6 +460,29 @@ const handleNodeSelect = (event: PointerEvent, nodeData: VueNodeData) => {
canvasStore.updateSelectedItems()
}

// Handle node collapse state changes
const handleNodeCollapse = (nodeId: string, collapsed: boolean) => {
if (!nodeManager) return

const node = nodeManager.getNode(nodeId)
if (!node) return

// Sync collapsed state back to LiteGraph node
node.flags = node.flags || {}
node.flags.collapsed = collapsed
}

// Handle node title updates
const handleNodeTitleUpdate = (nodeId: string, newTitle: string) => {
if (!nodeManager) return

const node = nodeManager.getNode(nodeId)
if (!node) return

// Update the node title in LiteGraph for persistence
node.title = newTitle
}

watchEffect(() => {
nodeDefStore.showDeprecated = settingStore.get('Comfy.Node.ShowDeprecated')
})
Expand Down
24 changes: 18 additions & 6 deletions src/components/graph/vueNodes/LGraphNode.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<div
v-else
:class="[
'lg-node absolute border-2 rounded',
'lg-node absolute border-2 rounded-lg',
'contain-layout contain-style contain-paint',
selected ? 'border-blue-500 ring-2 ring-blue-300' : 'border-gray-600',
executing ? 'animate-pulse' : '',
Expand All @@ -24,15 +24,17 @@
>
<!-- Header only updates on title/color changes -->
<NodeHeader
v-memo="[nodeData.title, lodLevel]"
v-memo="[nodeData.title, lodLevel, isCollapsed]"
:node-data="nodeData"
:readonly="readonly"
:lod-level="lodLevel"
:collapsed="isCollapsed"
@collapse="handleCollapse"
@update:title="handleTitleUpdate"
/>

<!-- Node Body - rendered based on LOD level -->
<div v-if="!isMinimalLOD" class="flex flex-col gap-2 p-2">
<!-- Node Body - rendered based on LOD level and collapsed state -->
<div v-if="!isMinimalLOD && !isCollapsed" class="flex flex-col gap-2 p-2">
<!-- Slots only rendered at full detail -->
<NodeSlots
v-if="shouldRenderSlots"
Expand Down Expand Up @@ -114,7 +116,8 @@ const emit = defineEmits<{
slotIndex: number,
isInput: boolean
]
collapse: []
'update:collapsed': [nodeId: string, collapsed: boolean]
'update:title': [nodeId: string, newTitle: string]
}>()

// LOD (Level of Detail) system based on zoom level
Expand Down Expand Up @@ -143,6 +146,9 @@ onErrorCaptured((error) => {
// Track dragging state for will-change optimization
const isDragging = ref(false)

// Track collapsed state
const isCollapsed = ref(props.nodeData.flags?.collapsed ?? false)

// Check if node has custom content
const hasCustomContent = computed(() => {
// Currently all content is handled through widgets
Expand All @@ -160,7 +166,9 @@ const handlePointerDown = (event: PointerEvent) => {
}

const handleCollapse = () => {
emit('collapse')
isCollapsed.value = !isCollapsed.value
// Emit event so parent can sync with LiteGraph if needed
emit('update:collapsed', props.nodeData.id, isCollapsed.value)
}

const handleSlotClick = (
Expand All @@ -175,6 +183,10 @@ const handleSlotClick = (
emit('slot-click', event, props.nodeData, slotIndex, isInput)
}

const handleTitleUpdate = (newTitle: string) => {
emit('update:title', props.nodeData.id, newTitle)
}

// Expose methods for parent to control dragging state
defineExpose({
setDragging(dragging: boolean) {
Expand Down
69 changes: 41 additions & 28 deletions src/components/graph/vueNodes/NodeHeader.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,39 +4,33 @@
</div>
<div
v-else
class="lg-node-header flex items-center justify-between px-3 py-2 rounded-t cursor-move"
class="lg-node-header flex items-center justify-between p-2 rounded-t-lg cursor-move -mt-[30px]"
:style="{
backgroundColor: headerColor,
color: textColor
}"
@dblclick="handleDoubleClick"
>
<!-- Collapse/Expand Button -->
<button
v-show="!readonly"
class="bg-transparent border-transparent flex items-center"
title="Toggle collapse"
@click.stop="handleCollapse"
>
<i
:class="collapsed ? 'pi pi-chevron-right' : 'pi pi-chevron-down'"
class="text-xs leading-none relative top-[1px]"
></i>
</button>

<!-- Node Title -->
<span class="text-sm font-medium truncate flex-1">
{{ nodeInfo?.title || 'Untitled' }}
</span>

<!-- Node Controls -->
<div class="flex items-center gap-1 ml-2">
<!-- Collapse/Expand Button -->
<button
v-if="!readonly"
class="lg-node-header__control p-0.5 rounded hover:bg-white/20 dark-theme:hover:bg-black/20 transition-colors opacity-60 hover:opacity-100"
title="Toggle collapse"
@click.stop="handleCollapse"
>
<svg
class="w-3 h-3 transition-transform"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<polyline points="6 9 12 15 18 9"></polyline>
</svg>
</button>

<!-- Additional controls can be added here -->
<div class="text-sm font-medium truncate flex-1">
<EditableText
:model-value="displayTitle"
:is-editing="isEditing"
@edit="handleTitleEdit"
/>
</div>
</div>
</template>
Expand All @@ -45,6 +39,7 @@
import type { LGraphNode } from '@comfyorg/litegraph'
import { computed, onErrorCaptured, ref } from 'vue'

import EditableText from '@/components/common/EditableText.vue'
import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
import type { LODLevel } from '@/composables/graph/useLOD'
import { useErrorHandling } from '@/composables/useErrorHandling'
Expand All @@ -54,13 +49,14 @@ interface NodeHeaderProps {
nodeData?: VueNodeData // New clean data structure
readonly?: boolean
lodLevel?: LODLevel
collapsed?: boolean
}

const props = defineProps<NodeHeaderProps>()

const emit = defineEmits<{
collapse: []
'title-edit': []
'update:title': [newTitle: string]
}>()

// Error boundary implementation
Expand All @@ -73,8 +69,14 @@ onErrorCaptured((error) => {
return false
})

// Editing state
const isEditing = ref(false)

const nodeInfo = computed(() => props.nodeData || props.node)

// Local state for title to provide immediate feedback
const displayTitle = ref(nodeInfo.value?.title || 'Untitled')

// Compute header color based on node color property or type
const headerColor = computed(() => {
const info = nodeInfo.value
Expand Down Expand Up @@ -110,7 +112,18 @@ const handleCollapse = () => {

const handleDoubleClick = () => {
if (!props.readonly) {
emit('title-edit')
isEditing.value = true
}
}

const handleTitleEdit = (newTitle: string) => {
isEditing.value = false
const trimmedTitle = newTitle.trim()
if (trimmedTitle && trimmedTitle !== displayTitle.value) {
// Update local state immediately for instant feedback
displayTitle.value = trimmedTitle
// Emit for litegraph sync
emit('update:title', trimmedTitle)
}
}
</script>
6 changes: 5 additions & 1 deletion src/composables/graph/useGraphNodeManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ export interface VueNodeData {
widgets?: SafeWidgetData[]
inputs?: unknown[]
outputs?: unknown[]
flags?: {
collapsed?: boolean
}
}

export interface SpatialMetrics {
Expand Down Expand Up @@ -201,7 +204,8 @@ export const useGraphNodeManager = (graph: LGraph): GraphNodeManager => {
executing: false, // Will be updated separately based on execution state
widgets: safeWidgets,
inputs: node.inputs ? [...node.inputs] : undefined,
outputs: node.outputs ? [...node.outputs] : undefined
outputs: node.outputs ? [...node.outputs] : undefined,
flags: node.flags ? { ...node.flags } : undefined
}
}

Expand Down
16 changes: 8 additions & 8 deletions src/locales/en/commands.json
Original file line number Diff line number Diff line change
Expand Up @@ -231,19 +231,19 @@
"label": "Toggle Focus Mode"
},
"Workspace_ToggleSidebarTab_model-library": {
"label": "Toggle Model Library Sidebar",
"tooltip": "Model Library"
"label": "sideToolbar.modelLibrary",
"tooltip": "sideToolbar.modelLibrary"
},
"Workspace_ToggleSidebarTab_node-library": {
"label": "Toggle Node Library Sidebar",
"tooltip": "Node Library"
"label": "sideToolbar.nodeLibrary",
"tooltip": "sideToolbar.nodeLibrary"
},
"Workspace_ToggleSidebarTab_queue": {
"label": "Toggle Queue Sidebar",
"tooltip": "Queue"
"label": "sideToolbar.queue",
"tooltip": "sideToolbar.queue"
},
"Workspace_ToggleSidebarTab_workflows": {
"label": "Toggle Workflows Sidebar",
"tooltip": "Workflows"
"label": "sideToolbar.workflows",
"tooltip": "sideToolbar.workflows"
}
}
11 changes: 6 additions & 5 deletions src/locales/en/main.json
Original file line number Diff line number Diff line change
Expand Up @@ -878,10 +878,10 @@
"Toggle Terminal Bottom Panel": "Toggle Terminal Bottom Panel",
"Toggle Logs Bottom Panel": "Toggle Logs Bottom Panel",
"Toggle Focus Mode": "Toggle Focus Mode",
"Toggle Model Library Sidebar": "Toggle Model Library Sidebar",
"Toggle Node Library Sidebar": "Toggle Node Library Sidebar",
"Toggle Queue Sidebar": "Toggle Queue Sidebar",
"Toggle Workflows Sidebar": "Toggle Workflows Sidebar"
"sideToolbar_modelLibrary": "sideToolbar.modelLibrary",
"sideToolbar_nodeLibrary": "sideToolbar.nodeLibrary",
"sideToolbar_queue": "sideToolbar.queue",
"sideToolbar_workflows": "sideToolbar.workflows"
},
"desktopMenu": {
"reinstall": "Reinstall",
Expand Down Expand Up @@ -939,7 +939,8 @@
"Light": "Light",
"User": "User",
"Credits": "Credits",
"API Nodes": "API Nodes"
"API Nodes": "API Nodes",
"Vue Nodes": "Vue Nodes"
},
"serverConfigItems": {
"listen": {
Expand Down
8 changes: 8 additions & 0 deletions src/locales/en/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,14 @@
"Comfy_Validation_Workflows": {
"name": "Validate workflows"
},
"Comfy_VueNodes_Enabled": {
"name": "Enable Vue node rendering",
"tooltip": "Render nodes as Vue components instead of canvas elements. Experimental feature."
},
"Comfy_VueNodes_Widgets": {
"name": "Enable Vue widgets",
"tooltip": "Render widgets as Vue components within Vue nodes."
},
"Comfy_WidgetControlMode": {
"name": "Widget control mode",
"tooltip": "Controls when widget values are updated (randomize/increment/decrement), either before the prompt is queued or after.",
Expand Down
11 changes: 6 additions & 5 deletions src/locales/es/main.json
Original file line number Diff line number Diff line change
Expand Up @@ -775,20 +775,20 @@
"Toggle Bottom Panel": "Alternar panel inferior",
"Toggle Focus Mode": "Alternar modo de enfoque",
"Toggle Logs Bottom Panel": "Alternar panel inferior de registros",
"Toggle Model Library Sidebar": "Alternar barra lateral de biblioteca de modelos",
"Toggle Node Library Sidebar": "Alternar barra lateral de biblioteca de nodos",
"Toggle Queue Sidebar": "Alternar barra lateral de cola",
"Toggle Search Box": "Alternar caja de búsqueda",
"Toggle Terminal Bottom Panel": "Alternar panel inferior de terminal",
"Toggle Theme (Dark/Light)": "Alternar tema (Oscuro/Claro)",
"Toggle Workflows Sidebar": "Alternar barra lateral de flujos de trabajo",
"Toggle the Custom Nodes Manager": "Alternar el Administrador de Nodos Personalizados",
"Toggle the Custom Nodes Manager Progress Bar": "Alternar la Barra de Progreso del Administrador de Nodos Personalizados",
"Undo": "Deshacer",
"Ungroup selected group nodes": "Desagrupar nodos de grupo seleccionados",
"Workflow": "Flujo de trabajo",
"Zoom In": "Acercar",
"Zoom Out": "Alejar"
"Zoom Out": "Alejar",
"sideToolbar_modelLibrary": "sideToolbar.bibliotecaDeModelos",
"sideToolbar_nodeLibrary": "sideToolbar.bibliotecaDeNodos",
"sideToolbar_queue": "sideToolbar.cola",
"sideToolbar_workflows": "sideToolbar.flujosDeTrabajo"
},
"missingModelsDialog": {
"doNotAskAgain": "No mostrar esto de nuevo",
Expand Down Expand Up @@ -1103,6 +1103,7 @@
"UV": "UV",
"User": "Usuario",
"Validation": "Validación",
"Vue Nodes": "Nodos Vue",
"Window": "Ventana",
"Workflow": "Flujo de Trabajo"
},
Expand Down
8 changes: 8 additions & 0 deletions src/locales/es/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,14 @@
"Comfy_Validation_Workflows": {
"name": "Validar flujos de trabajo"
},
"Comfy_VueNodes_Enabled": {
"name": "Habilitar renderizado de nodos Vue",
"tooltip": "Renderiza los nodos como componentes Vue en lugar de elementos canvas. Función experimental."
},
"Comfy_VueNodes_Widgets": {
"name": "Habilitar widgets de Vue",
"tooltip": "Renderiza los widgets como componentes de Vue dentro de los nodos de Vue."
},
"Comfy_WidgetControlMode": {
"name": "Modo de control del widget",
"options": {
Expand Down
Loading