diff --git a/src/components/graph/GraphCanvas.vue b/src/components/graph/GraphCanvas.vue index e4db7b6be..4a855623a 100644 --- a/src/components/graph/GraphCanvas.vue +++ b/src/components/graph/GraphCanvas.vue @@ -38,6 +38,7 @@ + @@ -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') diff --git a/src/components/graph/nodes/VueNode.vue b/src/components/graph/nodes/VueNode.vue new file mode 100644 index 000000000..068811d35 --- /dev/null +++ b/src/components/graph/nodes/VueNode.vue @@ -0,0 +1,142 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/components/graph/nodes/VueNodeBody.vue b/src/components/graph/nodes/VueNodeBody.vue new file mode 100644 index 000000000..6d47f8679 --- /dev/null +++ b/src/components/graph/nodes/VueNodeBody.vue @@ -0,0 +1,117 @@ + + + + + + + + + + + {{ widget.name || 'Widget' }} + {{ widget.type || 'unknown' }} + + + Value: {{ formatWidgetValue(widget.value) }} + + + + + + + No widgets + + + + + + + \ No newline at end of file diff --git a/src/components/graph/nodes/VueNodeHeader.vue b/src/components/graph/nodes/VueNodeHeader.vue new file mode 100644 index 000000000..9c6ddd12f --- /dev/null +++ b/src/components/graph/nodes/VueNodeHeader.vue @@ -0,0 +1,101 @@ + + + + + + {{ nodeType }} + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/components/graph/nodes/VueNodeOverlay.vue b/src/components/graph/nodes/VueNodeOverlay.vue new file mode 100644 index 000000000..873f91092 --- /dev/null +++ b/src/components/graph/nodes/VueNodeOverlay.vue @@ -0,0 +1,60 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/components/graph/nodes/VueNodeSlots.vue b/src/components/graph/nodes/VueNodeSlots.vue new file mode 100644 index 000000000..5a39c8bd8 --- /dev/null +++ b/src/components/graph/nodes/VueNodeSlots.vue @@ -0,0 +1,143 @@ + + + + + + + + + + + {{ input.name || `Input ${index}` }} + + + + + {{ input.type }} + + + + + + + + + + {{ output.type }} + + + + + {{ output.name || `Output ${index}` }} + + + + + + + + + + + + \ No newline at end of file diff --git a/src/composables/nodeRendering/useNodeInteractionProxy.ts b/src/composables/nodeRendering/useNodeInteractionProxy.ts new file mode 100644 index 000000000..bac144a2d --- /dev/null +++ b/src/composables/nodeRendering/useNodeInteractionProxy.ts @@ -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 + } +} \ No newline at end of file diff --git a/src/composables/nodeRendering/useNodePositionSync.ts b/src/composables/nodeRendering/useNodePositionSync.ts new file mode 100644 index 000000000..b176b634a --- /dev/null +++ b/src/composables/nodeRendering/useNodePositionSync.ts @@ -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>({}) + 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 = {} + 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 + } +} \ No newline at end of file diff --git a/src/composables/nodeRendering/usePhantomNodes.ts b/src/composables/nodeRendering/usePhantomNodes.ts new file mode 100644 index 000000000..b2c46f126 --- /dev/null +++ b/src/composables/nodeRendering/usePhantomNodes.ts @@ -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 + } +} \ No newline at end of file diff --git a/src/composables/widgets/useBadgedNumberInput.ts b/src/composables/widgets/useBadgedNumberInput.ts index 70069b1a4..aa27f80a8 100644 --- a/src/composables/widgets/useBadgedNumberInput.ts +++ b/src/composables/widgets/useBadgedNumberInput.ts @@ -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 diff --git a/src/composables/widgets/useChatHistoryWidget.ts b/src/composables/widgets/useChatHistoryWidget.ts index 205804c1b..03d964964 100644 --- a/src/composables/widgets/useChatHistoryWidget.ts +++ b/src/composables/widgets/useChatHistoryWidget.ts @@ -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) diff --git a/src/composables/widgets/useColorPickerWidget.ts b/src/composables/widgets/useColorPickerWidget.ts index d6f05770f..416d5272f 100644 --- a/src/composables/widgets/useColorPickerWidget.ts +++ b/src/composables/widgets/useColorPickerWidget.ts @@ -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 } diff --git a/src/composables/widgets/useComboWidget.ts b/src/composables/widgets/useComboWidget.ts index e892c9702..485391b53 100644 --- a/src/composables/widgets/useComboWidget.ts +++ b/src/composables/widgets/useComboWidget.ts @@ -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 } diff --git a/src/composables/widgets/useDropdownComboWidget.ts b/src/composables/widgets/useDropdownComboWidget.ts index 0a03c3ebc..23f5de789 100644 --- a/src/composables/widgets/useDropdownComboWidget.ts +++ b/src/composables/widgets/useDropdownComboWidget.ts @@ -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 diff --git a/src/composables/widgets/useImagePreviewWidget.ts b/src/composables/widgets/useImagePreviewWidget.ts index 98ceffd98..759b4af12 100644 --- a/src/composables/widgets/useImagePreviewWidget.ts +++ b/src/composables/widgets/useImagePreviewWidget.ts @@ -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 } diff --git a/src/composables/widgets/useImageUploadMediaWidget.ts b/src/composables/widgets/useImageUploadMediaWidget.ts index a2ac574d4..8bba061ce 100644 --- a/src/composables/widgets/useImageUploadMediaWidget.ts +++ b/src/composables/widgets/useImageUploadMediaWidget.ts @@ -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([]) @@ -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 => diff --git a/src/composables/widgets/useMediaLoaderWidget.ts b/src/composables/widgets/useMediaLoaderWidget.ts index 92cc9d8cb..4a25a231a 100644 --- a/src/composables/widgets/useMediaLoaderWidget.ts +++ b/src/composables/widgets/useMediaLoaderWidget.ts @@ -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, diff --git a/src/composables/widgets/useProgressTextWidget.ts b/src/composables/widgets/useProgressTextWidget.ts index d95c80012..e19e3d419 100644 --- a/src/composables/widgets/useProgressTextWidget.ts +++ b/src/composables/widgets/useProgressTextWidget.ts @@ -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 } }) diff --git a/src/composables/widgets/useStringWidgetVue.ts b/src/composables/widgets/useStringWidgetVue.ts index bae4e15e2..13392ee36 100644 --- a/src/composables/widgets/useStringWidgetVue.ts +++ b/src/composables/widgets/useStringWidgetVue.ts @@ -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 } diff --git a/src/types/phantomNode.d.ts b/src/types/phantomNode.d.ts new file mode 100644 index 000000000..6461d4898 --- /dev/null +++ b/src/types/phantomNode.d.ts @@ -0,0 +1,7 @@ +// Type extensions for phantom node functionality + +declare module '@comfyorg/litegraph' { + interface LGraphNode { + phantom_mode?: boolean + } +} \ No newline at end of file