vue node prototype

This commit is contained in:
bymyself
2025-06-10 18:43:05 -07:00
parent e488b2abce
commit 7e153cf10b
20 changed files with 903 additions and 82 deletions

View File

@@ -38,6 +38,7 @@
<SelectionToolbox />
</SelectionOverlay>
<DomWidgets />
<VueNodeOverlay v-if="vueNodeRenderingEnabled" />
</template>
<SubgraphBreadcrumb />
</template>
@@ -53,6 +54,7 @@ import SubgraphBreadcrumb from '@/components/breadcrumb/SubgraphBreadcrumb.vue'
import DomWidgets from '@/components/graph/DomWidgets.vue'
import GraphCanvasMenu from '@/components/graph/GraphCanvasMenu.vue'
import NodeTooltip from '@/components/graph/NodeTooltip.vue'
import VueNodeOverlay from '@/components/graph/nodes/VueNodeOverlay.vue'
import SelectionOverlay from '@/components/graph/SelectionOverlay.vue'
import SelectionToolbox from '@/components/graph/SelectionToolbox.vue'
import TitleEditor from '@/components/graph/TitleEditor.vue'
@@ -110,6 +112,8 @@ const tooltipEnabled = computed(() => settingStore.get('Comfy.EnableTooltips'))
const selectionToolboxEnabled = computed(() =>
settingStore.get('Comfy.Canvas.SelectionToolbox')
)
// Temporarily enable Vue node rendering for testing
const vueNodeRenderingEnabled = computed(() => true)
watchEffect(() => {
nodeDefStore.showDeprecated = settingStore.get('Comfy.Node.ShowDeprecated')

View File

@@ -0,0 +1,142 @@
<template>
<div
class="vue-node pointer-events-auto flex flex-col bg-white dark-theme:bg-gray-800 border border-gray-300 dark-theme:border-gray-600 rounded shadow-lg"
:class="nodeClasses"
:style="nodeStyle"
@mousedown="onMouseDown"
@contextmenu="onContextMenu"
>
<VueNodeHeader
:node="node"
:title="node.title"
:nodeType="node.type"
@title-edit="onTitleEdit"
/>
<VueNodeSlots
v-if="!node.collapsed"
:inputs="node.inputs || []"
:outputs="node.outputs || []"
@slot-click="onSlotInteraction"
/>
<!-- Flexbox container for widgets - no manual height calculations needed -->
<VueNodeBody
v-if="!node.collapsed"
class="flex flex-col gap-2 p-2 flex-grow"
:widgets="nodeWidgets"
:node="node"
@widget-change="onWidgetChange"
/>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import type { LGraphNode } from '@comfyorg/litegraph'
import type { NodeInteractionEvent } from '@/composables/nodeRendering/useNodeInteractionProxy'
import type { NodePosition } from '@/composables/nodeRendering/useNodePositionSync'
import VueNodeHeader from './VueNodeHeader.vue'
import VueNodeSlots from './VueNodeSlots.vue'
import VueNodeBody from './VueNodeBody.vue'
interface VueNodeProps {
node: LGraphNode
position?: NodePosition
selected: boolean
executing: boolean
}
const props = defineProps<VueNodeProps>()
const emit = defineEmits<{
interaction: [event: NodeInteractionEvent]
}>()
// Node styling based on position and state
const nodeStyle = computed(() => ({
position: 'absolute' as const,
left: props.position ? `${props.position.x}px` : '0px',
top: props.position ? `${props.position.y}px` : '0px',
minWidth: props.position ? `${props.position.width}px` : '150px',
// Height is now determined by flexbox content, not manual calculations
zIndex: props.selected ? 10 : 1,
}))
// Node CSS classes based on state
const nodeClasses = computed(() => ({
'vue-node--selected': props.selected,
'vue-node--executing': props.executing,
'vue-node--collapsed': props.node.collapsed,
[`vue-node--${props.node.type?.replace(/[^a-zA-Z0-9]/g, '-')}`]: props.node.type
}))
// Extract widgets from the node
const nodeWidgets = computed(() => {
return props.node.widgets || []
})
// Event handlers
const onMouseDown = (event: MouseEvent) => {
emit('interaction', {
type: 'mousedown',
nodeId: String(props.node.id),
originalEvent: event
})
}
const onContextMenu = (event: MouseEvent) => {
emit('interaction', {
type: 'contextmenu',
nodeId: String(props.node.id),
originalEvent: event
})
}
const onSlotInteraction = (slotIndex: number, event: MouseEvent) => {
emit('interaction', {
type: 'slot-click',
nodeId: String(props.node.id),
originalEvent: event,
slotIndex
})
}
const onTitleEdit = (newTitle: string) => {
props.node.title = newTitle
}
const onWidgetChange = (widgetIndex: number, value: any) => {
if (props.node.widgets?.[widgetIndex]) {
props.node.widgets[widgetIndex].value = value
}
}
</script>
<style scoped>
.vue-node {
transition: box-shadow 0.2s ease;
}
.vue-node--selected {
@apply ring-2 ring-blue-500 shadow-xl;
}
.vue-node--executing {
@apply ring-2 ring-green-500;
animation: pulse 2s infinite;
}
.vue-node--collapsed {
@apply h-8;
}
@keyframes pulse {
0%, 100% {
box-shadow: 0 0 0 0 rgba(34, 197, 94, 0.4);
}
50% {
box-shadow: 0 0 0 10px rgba(34, 197, 94, 0);
}
}
</style>

View File

@@ -0,0 +1,117 @@
<template>
<div class="vue-node-body">
<!-- Render widgets using existing Vue widget system -->
<div
v-for="(widget, index) in widgets"
:key="`widget-${index}`"
class="widget-container mb-2 last:mb-0"
>
<!-- Use existing Vue widget components if available -->
<component
v-if="getWidgetComponent(widget)"
:is="getWidgetComponent(widget)"
:widget="widget"
v-model="widget.value"
@update:model-value="onWidgetChange(index, $event)"
/>
<!-- Fallback for non-Vue widgets -->
<div
v-else
class="legacy-widget p-2 bg-gray-50 dark-theme:bg-gray-700 rounded text-sm"
>
<div class="flex items-center justify-between">
<span class="font-medium">{{ widget.name || 'Widget' }}</span>
<span class="text-gray-500">{{ widget.type || 'unknown' }}</span>
</div>
<div class="mt-1 text-gray-600 dark-theme:text-gray-400">
Value: {{ formatWidgetValue(widget.value) }}
</div>
</div>
</div>
<!-- Message when no widgets -->
<div
v-if="widgets.length === 0"
class="text-center text-gray-500 dark-theme:text-gray-400 text-sm py-2"
>
No widgets
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import type { LGraphNode } from '@comfyorg/litegraph'
import type { BaseWidget } from '@comfyorg/litegraph'
// Import existing Vue widget components
import StringWidget from '@/components/graph/widgets/StringWidget.vue'
import ColorPickerWidget from '@/components/graph/widgets/ColorPickerWidget.vue'
import ImagePreviewWidget from '@/components/graph/widgets/ImagePreviewWidget.vue'
import ImageUploadWidget from '@/components/graph/widgets/ImageUploadWidget.vue'
interface VueNodeBodyProps {
widgets: BaseWidget[]
node: LGraphNode
}
const props = defineProps<VueNodeBodyProps>()
const emit = defineEmits<{
'widget-change': [widgetIndex: number, value: any]
}>()
// Map widget types to Vue components
const widgetComponentMap: Record<string, any> = {
'STRING': StringWidget,
'text': StringWidget,
'COLOR': ColorPickerWidget,
'color': ColorPickerWidget,
'IMAGE': ImagePreviewWidget,
'image': ImagePreviewWidget,
'IMAGEUPLOAD': ImageUploadWidget,
'image_upload': ImageUploadWidget
}
// Get the Vue component for a widget type
const getWidgetComponent = (widget: BaseWidget) => {
const widgetType = widget.type?.toUpperCase()
return widgetType ? widgetComponentMap[widgetType] : null
}
// Format widget value for display
const formatWidgetValue = (value: any) => {
if (value === null || value === undefined) return 'null'
if (typeof value === 'object') return JSON.stringify(value, null, 2)
if (typeof value === 'string' && value.length > 50) {
return value.substring(0, 47) + '...'
}
return String(value)
}
const onWidgetChange = (index: number, value: any) => {
emit('widget-change', index, value)
// Trigger node property change if the widget has a callback
const widget = props.widgets[index]
if (widget?.callback) {
// Note: callback signature may need adjustment based on widget requirements
widget.callback(value, null, props.node, [0, 0], {})
}
}
</script>
<style scoped>
.vue-node-body {
min-height: 2rem;
}
.widget-container {
/* Widget containers use flexbox for natural sizing */
}
.legacy-widget {
/* Styling for non-Vue widgets */
border: 1px dashed #ccc;
}
</style>

View File

@@ -0,0 +1,101 @@
<template>
<div class="vue-node-header flex items-center justify-between p-2 bg-gray-100 dark-theme:bg-gray-700 rounded-t">
<div class="flex items-center gap-2 flex-grow">
<!-- Node type badge -->
<span
v-if="nodeType"
class="text-xs px-2 py-1 bg-blue-100 dark-theme:bg-blue-900 text-blue-800 dark-theme:text-blue-200 rounded"
>
{{ nodeType }}
</span>
<!-- Editable title -->
<EditableText
v-model="editableTitle"
class="font-medium text-sm flex-grow"
@update:model-value="onTitleUpdate"
/>
</div>
<!-- Node controls -->
<div class="flex items-center gap-1">
<!-- Collapse/expand button -->
<button
v-if="node.constructor?.collapsable !== false"
class="w-5 h-5 flex items-center justify-center text-gray-500 hover:text-gray-700 dark-theme:text-gray-400 dark-theme:hover:text-gray-200 rounded hover:bg-gray-200 dark-theme:hover:bg-gray-600"
@click="toggleCollapse"
>
<i class="mdi mdi-chevron-up" :class="{ 'rotate-180': node.collapsed }"></i>
</button>
<!-- Pin button -->
<button
class="w-5 h-5 flex items-center justify-center text-gray-500 hover:text-gray-700 dark-theme:text-gray-400 dark-theme:hover:text-gray-200 rounded hover:bg-gray-200 dark-theme:hover:bg-gray-600"
:class="{ 'text-blue-600 dark-theme:text-blue-400': node.pinned }"
@click="togglePin"
>
<i class="mdi mdi-pin" :class="{ 'mdi-pin-off': !node.pinned }"></i>
</button>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
import type { LGraphNode } from '@comfyorg/litegraph'
import EditableText from '@/components/common/EditableText.vue'
interface VueNodeHeaderProps {
node: LGraphNode
title: string
nodeType?: string
}
const props = defineProps<VueNodeHeaderProps>()
const emit = defineEmits<{
'title-edit': [title: string]
}>()
// Local editable title
const editableTitle = ref(props.title)
// Watch for external title changes
watch(() => props.title, (newTitle) => {
editableTitle.value = newTitle
})
const onTitleUpdate = (newTitle: string) => {
emit('title-edit', newTitle)
}
const toggleCollapse = () => {
// Use node collapse method instead of setting property directly
if (props.node.collapse) {
props.node.collapse()
} else {
// Fallback to manual property setting if method doesn't exist
;(props.node as any).collapsed = !props.node.collapsed
}
// Trigger canvas redraw
props.node.setDirtyCanvas?.(true, true)
}
const togglePin = () => {
// Use pin method if available, otherwise set property
if (props.node.pin) {
props.node.pin()
} else {
// Fallback to manual property setting if method doesn't exist
;(props.node as any).pinned = !props.node.pinned
}
// Trigger canvas redraw
props.node.setDirtyCanvas?.(true, true)
}
</script>
<style scoped>
.rotate-180 {
transform: rotate(180deg);
}
</style>

View File

@@ -0,0 +1,60 @@
<template>
<div
class="vue-node-overlay absolute inset-0 pointer-events-none overflow-hidden"
:style="overlayStyle"
>
<VueNode
v-for="node in visibleNodes"
:key="node.id"
:node="node"
:position="nodePositions[node.id]"
:selected="isNodeSelected(node.id)"
:executing="isNodeExecuting(node.id)"
@interaction="handleNodeInteraction"
/>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useNodePositionSync } from '@/composables/nodeRendering/useNodePositionSync'
import { useNodeInteractionProxy } from '@/composables/nodeRendering/useNodeInteractionProxy'
import { useCanvasStore } from '@/stores/graphStore'
import { useExecutionStore } from '@/stores/executionStore'
import VueNode from './VueNode.vue'
const {
nodePositions,
canvasScale,
canvasOffset,
visibleNodes
} = useNodePositionSync()
const { handleNodeInteraction } = useNodeInteractionProxy()
const canvasStore = useCanvasStore()
const executionStore = useExecutionStore()
// Transform style for the overlay to match canvas transform
const overlayStyle = computed(() => ({
transform: `scale(${canvasScale.value}) translate(${canvasOffset.value.x}px, ${canvasOffset.value.y}px)`,
transformOrigin: '0 0'
}))
// Check if node is selected
const isNodeSelected = (nodeId: string) => {
return canvasStore.selectedItems.has(Number(nodeId))
}
// Check if node is executing
const isNodeExecuting = (nodeId: string) => {
return executionStore.executingNodeId === Number(nodeId)
}
</script>
<style scoped>
.vue-node-overlay {
/* Ensure overlay doesn't interfere with canvas interactions */
z-index: 1;
}
</style>

View File

@@ -0,0 +1,143 @@
<template>
<div class="vue-node-slots">
<!-- Input slots -->
<div v-if="inputs.length > 0" class="inputs mb-2">
<div
v-for="(input, index) in inputs"
:key="`input-${index}`"
class="input-slot flex items-center gap-2 px-2 py-1 hover:bg-gray-50 dark-theme:hover:bg-gray-700"
@click="onSlotClick(index, $event, 'input')"
>
<!-- Input connection point -->
<div
class="slot-connector w-3 h-3 rounded-full border-2 border-gray-400 bg-white dark-theme:bg-gray-800"
:class="getSlotColor(input.type, 'input')"
></div>
<!-- Input label -->
<span class="text-sm text-gray-700 dark-theme:text-gray-300 flex-grow">
{{ input.name || `Input ${index}` }}
</span>
<!-- Input type badge -->
<span
v-if="input.type && input.type !== '*'"
class="text-xs px-1 py-0.5 bg-gray-200 dark-theme:bg-gray-600 text-gray-600 dark-theme:text-gray-400 rounded"
>
{{ input.type }}
</span>
</div>
</div>
<!-- Output slots -->
<div v-if="outputs.length > 0" class="outputs">
<div
v-for="(output, index) in outputs"
:key="`output-${index}`"
class="output-slot flex items-center gap-2 px-2 py-1 hover:bg-gray-50 dark-theme:hover:bg-gray-700"
@click="onSlotClick(index, $event, 'output')"
>
<!-- Output type badge -->
<span
v-if="output.type && output.type !== '*'"
class="text-xs px-1 py-0.5 bg-gray-200 dark-theme:bg-gray-600 text-gray-600 dark-theme:text-gray-400 rounded"
>
{{ output.type }}
</span>
<!-- Output label -->
<span class="text-sm text-gray-700 dark-theme:text-gray-300 flex-grow text-right">
{{ output.name || `Output ${index}` }}
</span>
<!-- Output connection point -->
<div
class="slot-connector w-3 h-3 rounded-full border-2 border-gray-400 bg-white dark-theme:bg-gray-800"
:class="getSlotColor(output.type, 'output')"
></div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import type { INodeInputSlot, INodeOutputSlot } from '@comfyorg/litegraph'
interface VueNodeSlotsProps {
inputs: INodeInputSlot[]
outputs: INodeOutputSlot[]
}
const props = defineProps<VueNodeSlotsProps>()
const emit = defineEmits<{
'slot-click': [slotIndex: number, event: MouseEvent]
}>()
// Color mapping for different slot types
const getSlotColor = (type: string | number | undefined, _direction: 'input' | 'output') => {
if (!type || type === '*') {
return 'border-gray-400'
}
// Convert type to string for lookup
const typeStr = String(type)
// Map common ComfyUI types to colors
const typeColors: Record<string, string> = {
'IMAGE': 'border-green-500 bg-green-100 dark-theme:bg-green-900',
'LATENT': 'border-purple-500 bg-purple-100 dark-theme:bg-purple-900',
'MODEL': 'border-blue-500 bg-blue-100 dark-theme:bg-blue-900',
'CONDITIONING': 'border-yellow-500 bg-yellow-100 dark-theme:bg-yellow-900',
'VAE': 'border-red-500 bg-red-100 dark-theme:bg-red-900',
'CLIP': 'border-orange-500 bg-orange-100 dark-theme:bg-orange-900',
'STRING': 'border-gray-500 bg-gray-100 dark-theme:bg-gray-900',
'INT': 'border-indigo-500 bg-indigo-100 dark-theme:bg-indigo-900',
'FLOAT': 'border-pink-500 bg-pink-100 dark-theme:bg-pink-900'
}
return typeColors[typeStr.toUpperCase()] || 'border-gray-400'
}
const onSlotClick = (index: number, event: MouseEvent, slotType: 'input' | 'output') => {
event.stopPropagation()
// Calculate the actual slot index based on type
// For outputs, we need to add the input count to get the correct index
const slotIndex = slotType === 'output' ? props.inputs.length + index : index
emit('slot-click', slotIndex, event)
}
</script>
<style scoped>
.vue-node-slots {
font-size: 0.875rem;
}
.slot-connector {
transition: all 0.2s ease;
cursor: pointer;
}
.slot-connector:hover {
transform: scale(1.2);
border-width: 3px;
}
.input-slot {
border-left: 3px solid transparent;
}
.output-slot {
border-right: 3px solid transparent;
}
.input-slot:hover {
border-left-color: #3b82f6;
}
.output-slot:hover {
border-right-color: #3b82f6;
}
</style>

View File

@@ -0,0 +1,71 @@
import { computed } from 'vue'
import { useCanvasStore } from '@/stores/graphStore'
export interface NodeInteractionEvent {
type: 'mousedown' | 'contextmenu' | 'slot-click'
nodeId: string
originalEvent: MouseEvent
slotIndex?: number
}
export function useNodeInteractionProxy() {
const canvasStore = useCanvasStore()
// Get canvas reference
const canvas = computed(() => canvasStore.canvas)
const handleNodeInteraction = (event: NodeInteractionEvent) => {
const { type, nodeId, originalEvent } = event
if (!canvas.value?.graph) return
const node = canvas.value.graph.getNodeById(Number(nodeId))
if (!node) return
switch (type) {
case 'mousedown':
// Convert Vue event coordinates back to canvas coordinates
const rect = canvas.value.canvas.getBoundingClientRect()
const canvasX = originalEvent.clientX - rect.left
const canvasY = originalEvent.clientY - rect.top
// Transform to graph coordinates
const graphPos = canvas.value.convertOffsetToCanvas([canvasX, canvasY])
// Note: simulatedEvent not currently used but kept for future expansion
// Trigger node selection and dragging
canvas.value.selectNode(node, originalEvent.ctrlKey || originalEvent.metaKey)
canvas.value.node_dragged = node
// Start drag operation if not holding modifier keys
if (!originalEvent.ctrlKey && !originalEvent.metaKey && !originalEvent.shiftKey) {
canvas.value.dragging_canvas = false
canvas.value.node_dragged = node
canvas.value.drag_start = [originalEvent.clientX, originalEvent.clientY]
}
break
case 'contextmenu':
// Show context menu for the node
originalEvent.preventDefault()
canvas.value.showContextMenu(originalEvent, node)
break
case 'slot-click':
// Handle slot connection interactions
if (event.slotIndex !== undefined) {
const slot = node.inputs?.[event.slotIndex] || node.outputs?.[event.slotIndex]
if (slot) {
canvas.value.processSlotClick(node, event.slotIndex, originalEvent)
}
}
break
}
}
return {
handleNodeInteraction
}
}

View File

@@ -0,0 +1,113 @@
import { ref, computed, readonly, watchEffect } from 'vue'
import type { LGraphNode } from '@comfyorg/litegraph'
import { useChainCallback } from '@/composables/functional/useChainCallback'
import { useCanvasStore } from '@/stores/graphStore'
// Note: useEventListener imported but not currently used - may be used for future enhancements
export interface NodePosition {
id: string
x: number
y: number
width: number
height: number
}
export function useNodePositionSync() {
const canvasStore = useCanvasStore()
const nodePositions = ref<Record<string, NodePosition>>({})
const canvasScale = ref(1)
const canvasOffset = ref({ x: 0, y: 0 })
// Get canvas reference
const canvas = computed(() => canvasStore.canvas)
// Sync canvas transform (scale and offset)
watchEffect(() => {
if (!canvas.value) return
const updateTransform = () => {
if (!canvas.value?.ds) return
canvasScale.value = canvas.value.ds.scale
canvasOffset.value = {
x: canvas.value.ds.offset[0],
y: canvas.value.ds.offset[1]
}
}
// Hook into the canvas draw cycle to update transform
canvas.value.onDrawForeground = useChainCallback(
canvas.value.onDrawForeground,
updateTransform
)
// Initial transform update
updateTransform()
})
// Sync node positions
const syncNodePositions = () => {
if (!canvas.value?.graph) return
const positions: Record<string, NodePosition> = {}
for (const node of canvas.value.graph._nodes) {
positions[node.id] = {
id: String(node.id),
x: node.pos[0],
y: node.pos[1],
width: node.size[0],
height: node.size[1]
}
}
nodePositions.value = positions
}
// Listen for node position changes
watchEffect(() => {
if (!canvas.value) return
// Hook into various node update events
const originalOnNodeMoved = canvas.value.onNodeMoved
canvas.value.onNodeMoved = useChainCallback(
originalOnNodeMoved,
syncNodePositions
)
// Hook into general graph changes
const originalOnGraphChanged = canvas.value.onGraphChanged
canvas.value.onGraphChanged = useChainCallback(
originalOnGraphChanged,
syncNodePositions
)
// Initial sync
syncNodePositions()
})
// Get visible nodes (within viewport bounds)
const visibleNodes = computed(() => {
if (!canvas.value?.graph) return []
const nodes = canvas.value.graph._nodes.filter((node: LGraphNode) => {
// Only return nodes that have phantom_mode enabled
return node.phantom_mode === true
})
// TODO: Add viewport culling for performance
// For now, return all phantom nodes
return nodes
})
// Manual sync function for external triggers
const forceSync = () => {
syncNodePositions()
}
return {
nodePositions: readonly(nodePositions),
canvasScale: readonly(canvasScale),
canvasOffset: readonly(canvasOffset),
visibleNodes: readonly(visibleNodes),
forceSync
}
}

View File

@@ -0,0 +1,142 @@
import { computed } from 'vue'
import type { LGraphNode } from '@comfyorg/litegraph'
import { useCanvasStore } from '@/stores/graphStore'
import { useSettingStore } from '@/stores/settingStore'
export function usePhantomNodes() {
const canvasStore = useCanvasStore()
const settingStore = useSettingStore()
// Get canvas reference
const canvas = computed(() => canvasStore.canvas)
// Check if Vue node rendering is enabled
const vueRenderingEnabled = computed(() => true) // Temporarily enabled for testing
/**
* Enable phantom mode for a specific node
* @param nodeId The ID of the node to make phantom
*/
const enablePhantomMode = (nodeId: string | number) => {
if (!canvas.value?.graph) return false
const node = canvas.value.graph.getNodeById(Number(nodeId))
if (!node) return false
node.phantom_mode = true
// Trigger canvas redraw to hide the node visually
canvas.value.setDirty(true, true)
return true
}
/**
* Disable phantom mode for a specific node
* @param nodeId The ID of the node to make visible again
*/
const disablePhantomMode = (nodeId: string | number) => {
if (!canvas.value?.graph) return false
const node = canvas.value.graph.getNodeById(Number(nodeId))
if (!node) return false
node.phantom_mode = false
// Trigger canvas redraw to show the node visually
canvas.value.setDirty(true, true)
return true
}
/**
* Toggle phantom mode for a specific node
* @param nodeId The ID of the node to toggle
*/
const togglePhantomMode = (nodeId: string | number) => {
if (!canvas.value?.graph) return false
const node = canvas.value.graph.getNodeById(Number(nodeId))
if (!node) return false
const newMode = !node.phantom_mode
node.phantom_mode = newMode
// Trigger canvas redraw
canvas.value.setDirty(true, true)
return newMode
}
/**
* Enable phantom mode for all nodes (global Vue rendering)
*/
const enableAllPhantomMode = () => {
if (!canvas.value?.graph) return 0
let count = 0
for (const node of canvas.value.graph._nodes) {
if (!node.phantom_mode) {
node.phantom_mode = true
count++
}
}
if (count > 0) {
canvas.value.setDirty(true, true)
}
return count
}
/**
* Disable phantom mode for all nodes (back to canvas rendering)
*/
const disableAllPhantomMode = () => {
if (!canvas.value?.graph) return 0
let count = 0
for (const node of canvas.value.graph._nodes) {
if (node.phantom_mode) {
node.phantom_mode = false
count++
}
}
if (count > 0) {
canvas.value.setDirty(true, true)
}
return count
}
/**
* Get all phantom nodes
*/
const getPhantomNodes = (): LGraphNode[] => {
if (!canvas.value?.graph) return []
return canvas.value.graph._nodes.filter((node: LGraphNode) =>
node.phantom_mode === true
)
}
/**
* Check if a node is in phantom mode
* @param nodeId The ID of the node to check
*/
const isPhantomNode = (nodeId: string | number): boolean => {
if (!canvas.value?.graph) return false
const node = canvas.value.graph.getNodeById(Number(nodeId))
return node?.phantom_mode === true
}
return {
vueRenderingEnabled,
enablePhantomMode,
disablePhantomMode,
togglePhantomMode,
enableAllPhantomMode,
disableAllPhantomMode,
getPhantomNodes,
isPhantomNode
}
}

View File

@@ -6,7 +6,6 @@ import type { InputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
import { ComponentWidgetImpl, addWidget } from '@/scripts/domWidget'
import type { ComfyWidgetConstructorV2 } from '@/scripts/widgetTypes'
const PADDING = 8
type BadgeState = 'normal' | 'random' | 'lock' | 'increment' | 'decrement'
@@ -16,7 +15,6 @@ interface BadgedNumberInputOptions {
defaultValue?: number
badgeState?: BadgeState
disabled?: boolean
minHeight?: number
serialize?: boolean
mode?: NumberWidgetMode
}
@@ -43,7 +41,6 @@ export const useBadgedNumberInput = (
const {
defaultValue = 0,
disabled = false,
minHeight = 32,
serialize = true,
mode = 'int'
} = options
@@ -115,10 +112,6 @@ export const useBadgedNumberInput = (
}
},
// Optional: minimum height for the widget
getMinHeight: () => minHeight + PADDING,
// Lock maximum height to prevent oversizing
getMaxHeight: () => 48,
// Optional: whether to serialize this widget's value
serialize

View File

@@ -6,7 +6,6 @@ import type { InputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
import { ComponentWidgetImpl, addWidget } from '@/scripts/domWidget'
import type { ComfyWidgetConstructorV2 } from '@/scripts/widgetTypes'
const PADDING = 16
export const useChatHistoryWidget = (
options: {
@@ -32,7 +31,6 @@ export const useChatHistoryWidget = (
setValue: (value: string | object) => {
widgetValue.value = typeof value === 'string' ? value : String(value)
},
getMinHeight: () => 400 + PADDING
}
})
addWidget(node, widget)

View File

@@ -6,12 +6,9 @@ import type { InputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
import { ComponentWidgetImpl, addWidget } from '@/scripts/domWidget'
import type { ComfyWidgetConstructorV2 } from '@/scripts/widgetTypes'
const PADDING = 8
interface ColorPickerWidgetOptions {
defaultValue?: string
defaultFormat?: 'rgba' | 'hsla' | 'hsva' | 'hex'
minHeight?: number
serialize?: boolean
}
@@ -20,7 +17,6 @@ export const useColorPickerWidget = (
) => {
const {
defaultValue = 'rgba(255, 0, 0, 1)',
minHeight = 48,
serialize = true
} = options
@@ -64,9 +60,6 @@ export const useColorPickerWidget = (
}
},
// Optional: minimum height for the widget
getMinHeight: () => minHeight + PADDING,
// Optional: whether to serialize this widget's value
serialize
}

View File

@@ -30,10 +30,6 @@ const addMultiSelectWidget = (node: LGraphNode, inputSpec: ComboInputSpec) => {
setValue: (value: string[]) => {
widgetValue.value = value
},
// Optional: minimum height for the widget (multiselect needs minimal height)
getMinHeight: () => 24,
// Lock maximum height to prevent oversizing
getMaxHeight: () => 32,
// Optional: whether to serialize this widget's value
serialize: true
}

View File

@@ -49,10 +49,6 @@ export const useDropdownComboWidget = (
widgetValue.value = value
},
// Optional: minimum height for the widget (dropdown needs minimal height)
getMinHeight: () => 32,
// Lock maximum height to prevent oversizing
getMaxHeight: () => 48,
// Optional: whether to serialize this widget's value
serialize: true

View File

@@ -6,7 +6,7 @@ import type { InputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
import { ComponentWidgetImpl, addWidget } from '@/scripts/domWidget'
import { type ComfyWidgetConstructorV2 } from '@/scripts/widgetTypes'
const PADDING = 8
// Removed PADDING constant as it's no longer needed for CSS flexbox layout
export const useImagePreviewWidget = (
options: { defaultValue?: string | string[] } = {}
@@ -35,10 +35,6 @@ export const useImagePreviewWidget = (
widgetValue.value = value
},
// Optional: minimum height for the widget
getMinHeight: () => 320 + PADDING,
getMaxHeight: () => 512 + PADDING,
// Optional: whether to serialize this widget's value
serialize: false
}

View File

@@ -94,38 +94,6 @@ export const useImageUploadMediaWidget = () => {
// Convert the V1 input spec to V2 format for the MediaLoader widget
const inputSpecV2 = transformInputSpecV1ToV2(inputData, { name: inputName })
// Handle widget dimensions based on input options
const getMinHeight = () => {
// Use smaller height for MediaLoader upload widget
let baseHeight = 176
// Handle multiline attribute for expanded height
if (inputOptions.multiline) {
baseHeight = Math.max(
baseHeight,
inputOptions.multiline === true
? 120
: Number(inputOptions.multiline) || 120
)
}
// Handle other height-related attributes
if (inputOptions.min_height) {
baseHeight = Math.max(baseHeight, Number(inputOptions.min_height))
}
return baseHeight + 8 // Add padding
}
const getMaxHeight = () => {
// Lock maximum height to prevent oversizing of upload widget
if (inputOptions.multiline || inputOptions.min_height) {
// Allow more height for special cases
return Math.max(200, getMinHeight())
}
// Lock standard upload widget to ~80px max
return 80
}
// State for MediaLoader widget
const uploadedFiles = ref<string[]>([])
@@ -145,8 +113,6 @@ export const useImageUploadMediaWidget = () => {
setValue: (value: string[]) => {
uploadedFiles.value = value
},
getMinHeight,
getMaxHeight, // Lock maximum height to prevent oversizing
serialize: false,
onFilesSelected: async (files: File[]) => {
const isPastedFile = (file: File): boolean =>

View File

@@ -10,11 +10,9 @@ import {
} from '@/scripts/domWidget'
import type { ComfyWidgetConstructorV2 } from '@/scripts/widgetTypes'
const PADDING = 8
interface MediaLoaderOptions {
defaultValue?: string[]
minHeight?: number
accept?: string
onFilesSelected?: (files: File[]) => void
}
@@ -49,10 +47,6 @@ export const useMediaLoaderWidget = (options: MediaLoaderOptions = {}) => {
widgetValue.value = Array.isArray(value) ? value : []
},
// Optional: minimum height for the widget
// getMinHeight: () => (options.minHeight ?? 64) + PADDING,
getMaxHeight: () => 225 + PADDING,
getMinHeight: () => 176 + PADDING,
// Optional: whether to serialize this widget's value
serialize: true,

View File

@@ -6,13 +6,8 @@ import type { InputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
import { ComponentWidgetImpl, addWidget } from '@/scripts/domWidget'
import type { ComfyWidgetConstructorV2 } from '@/scripts/widgetTypes'
const PADDING = 16
export const useTextPreviewWidget = (
options: {
minHeight?: number
} = {}
) => {
export const useTextPreviewWidget = () => {
const widgetConstructor: ComfyWidgetConstructorV2 = (
node: LGraphNode,
inputSpec: InputSpec
@@ -28,7 +23,6 @@ export const useTextPreviewWidget = (
setValue: (value: string | object) => {
widgetValue.value = typeof value === 'string' ? value : String(value)
},
getMinHeight: () => options.minHeight ?? 42 + PADDING,
serialize: false
}
})

View File

@@ -9,7 +9,7 @@ import {
import { ComponentWidgetImpl, addWidget } from '@/scripts/domWidget'
import { type ComfyWidgetConstructorV2 } from '@/scripts/widgetTypes'
const PADDING = 8
// Removed PADDING constant as it's no longer needed for CSS flexbox layout
export const useStringWidgetVue = (options: { defaultValue?: string } = {}) => {
const widgetConstructor: ComfyWidgetConstructorV2 = (
@@ -40,11 +40,6 @@ export const useStringWidgetVue = (options: { defaultValue?: string } = {}) => {
widgetValue.value = value
},
// Optional: minimum height for the widget
getMinHeight: () => {
return inputSpec.multiline ? 80 + PADDING : 40 + PADDING
},
// Optional: whether to serialize this widget's value
serialize: true
}

7
src/types/phantomNode.d.ts vendored Normal file
View File

@@ -0,0 +1,7 @@
// Type extensions for phantom node functionality
declare module '@comfyorg/litegraph' {
interface LGraphNode {
phantom_mode?: boolean
}
}