diff --git a/browser_tests/tests/vueNodes/linkInteraction.spec.ts-snapshots/vue-node-dragging-link-chromium-linux.png b/browser_tests/tests/vueNodes/linkInteraction.spec.ts-snapshots/vue-node-dragging-link-chromium-linux.png
index d4c32b4ea..24f465271 100644
Binary files a/browser_tests/tests/vueNodes/linkInteraction.spec.ts-snapshots/vue-node-dragging-link-chromium-linux.png and b/browser_tests/tests/vueNodes/linkInteraction.spec.ts-snapshots/vue-node-dragging-link-chromium-linux.png differ
diff --git a/browser_tests/tests/vueNodes/lod.spec.ts b/browser_tests/tests/vueNodes/lod.spec.ts
new file mode 100644
index 000000000..9011f91b1
--- /dev/null
+++ b/browser_tests/tests/vueNodes/lod.spec.ts
@@ -0,0 +1,44 @@
+import { expect } from '@playwright/test'
+
+import { comfyPageFixture as test } from '../../fixtures/ComfyPage'
+
+test.describe('Vue Nodes - LOD', () => {
+ test.beforeEach(async ({ comfyPage }) => {
+ await comfyPage.setSetting('Comfy.VueNodes.Enabled', true)
+ await comfyPage.setup()
+ await comfyPage.loadWorkflow('default')
+ })
+
+ test('should toggle LOD based on zoom threshold', async ({ comfyPage }) => {
+ await comfyPage.vueNodes.waitForNodes()
+
+ const initialNodeCount = await comfyPage.vueNodes.getNodeCount()
+ expect(initialNodeCount).toBeGreaterThan(0)
+
+ await expect(comfyPage.canvas).toHaveScreenshot('vue-nodes-default.png')
+
+ const vueNodesContainer = comfyPage.vueNodes.nodes
+ const textboxesInNodes = vueNodesContainer.getByRole('textbox')
+ const buttonsInNodes = vueNodesContainer.getByRole('button')
+
+ await expect(textboxesInNodes.first()).toBeVisible()
+ await expect(buttonsInNodes.first()).toBeVisible()
+
+ await comfyPage.zoom(120, 10)
+ await comfyPage.nextFrame()
+
+ await expect(comfyPage.canvas).toHaveScreenshot('vue-nodes-lod-active.png')
+
+ await expect(textboxesInNodes.first()).toBeHidden()
+ await expect(buttonsInNodes.first()).toBeHidden()
+
+ await comfyPage.zoom(-120, 10)
+ await comfyPage.nextFrame()
+
+ await expect(comfyPage.canvas).toHaveScreenshot(
+ 'vue-nodes-lod-inactive.png'
+ )
+ await expect(textboxesInNodes.first()).toBeVisible()
+ await expect(buttonsInNodes.first()).toBeVisible()
+ })
+})
diff --git a/browser_tests/tests/vueNodes/lod.spec.ts-snapshots/vue-nodes-default-chromium-linux.png b/browser_tests/tests/vueNodes/lod.spec.ts-snapshots/vue-nodes-default-chromium-linux.png
new file mode 100644
index 000000000..8e93b88f3
Binary files /dev/null and b/browser_tests/tests/vueNodes/lod.spec.ts-snapshots/vue-nodes-default-chromium-linux.png differ
diff --git a/browser_tests/tests/vueNodes/lod.spec.ts-snapshots/vue-nodes-lod-active-chromium-linux.png b/browser_tests/tests/vueNodes/lod.spec.ts-snapshots/vue-nodes-lod-active-chromium-linux.png
new file mode 100644
index 000000000..5dfa61c19
Binary files /dev/null and b/browser_tests/tests/vueNodes/lod.spec.ts-snapshots/vue-nodes-lod-active-chromium-linux.png differ
diff --git a/browser_tests/tests/vueNodes/lod.spec.ts-snapshots/vue-nodes-lod-inactive-chromium-linux.png b/browser_tests/tests/vueNodes/lod.spec.ts-snapshots/vue-nodes-lod-inactive-chromium-linux.png
new file mode 100644
index 000000000..59802088f
Binary files /dev/null and b/browser_tests/tests/vueNodes/lod.spec.ts-snapshots/vue-nodes-lod-inactive-chromium-linux.png differ
diff --git a/src/assets/css/style.css b/src/assets/css/style.css
index cad8a1b3b..21d526bac 100644
--- a/src/assets/css/style.css
+++ b/src/assets/css/style.css
@@ -929,48 +929,6 @@ audio.comfy-audio.empty-audio-widget {
}
/* End of [Desktop] Electron window specific styles */
-/* Vue Node LOD (Level of Detail) System */
-/* These classes control rendering detail based on zoom level */
-
-/* Minimal LOD (zoom <= 0.4) - Title only for performance */
-.lg-node--lod-minimal {
- min-height: 32px;
- transition: min-height 0.2s ease;
- /* Performance optimizations */
- text-shadow: none;
- backdrop-filter: none;
-}
-
-.lg-node--lod-minimal .lg-node-body {
- display: none !important;
-}
-
-/* Reduced LOD (0.4 < zoom <= 0.8) - Essential widgets, simplified styling */
-.lg-node--lod-reduced {
- transition: opacity 0.1s ease;
- /* Performance optimizations */
- text-shadow: none;
-}
-
-.lg-node--lod-reduced .lg-widget-label,
-.lg-node--lod-reduced .lg-slot-label {
- display: none;
-}
-
-.lg-node--lod-reduced .lg-slot {
- opacity: 0.6;
- font-size: 0.75rem;
-}
-
-.lg-node--lod-reduced .lg-widget {
- margin: 2px 0;
- font-size: 0.875rem;
-}
-
-/* Full LOD (zoom > 0.8) - Complete detail rendering */
-.lg-node--lod-full {
- /* Uses default styling - no overrides needed */
-}
.lg-node {
/* Disable text selection on all nodes */
@@ -996,23 +954,52 @@ audio.comfy-audio.empty-audio-widget {
will-change: transform;
}
-/* Global performance optimizations for LOD */
-.lg-node--lod-minimal,
-.lg-node--lod-reduced {
- /* Remove ALL expensive paint effects */
- box-shadow: none !important;
- filter: none !important;
- backdrop-filter: none !important;
- text-shadow: none !important;
- -webkit-mask-image: none !important;
- mask-image: none !important;
- clip-path: none !important;
+/* START LOD specific styles */
+/* LOD styles - Custom CSS avoids 100+ Tailwind selectors that would slow style recalculation when .isLOD toggles */
+
+.isLOD .lg-node {
+ box-shadow: none;
+ filter: none;
+ backdrop-filter: none;
+ text-shadow: none;
+ -webkit-mask-image: none;
+ mask-image: none;
+ clip-path: none;
+ background-image: none;
+ text-rendering: optimizeSpeed;
+ border-radius: 0;
+ contain: layout style;
+ transition: none;
+
}
-/* Reduce paint complexity for minimal LOD */
-.lg-node--lod-minimal {
- /* Skip complex borders */
- border-radius: 0 !important;
- /* Use solid colors only */
- background-image: none !important;
+.isLOD .lg-node > * {
+ pointer-events: none;
}
+
+.lod-toggle {
+ visibility: visible;
+}
+
+.isLOD .lod-toggle {
+ visibility: hidden;
+}
+
+
+.lod-fallback {
+ display: none;
+}
+
+.isLOD .lod-fallback {
+ display: block;
+}
+
+.isLOD .image-preview img {
+ image-rendering: pixelated;
+}
+
+
+.isLOD .slot-dot {
+ border-radius: 0;
+}
+/* END LOD specific styles */
diff --git a/src/renderer/core/layout/transform/TransformPane.vue b/src/renderer/core/layout/transform/TransformPane.vue
index 29abc1262..43cc0e328 100644
--- a/src/renderer/core/layout/transform/TransformPane.vue
+++ b/src/renderer/core/layout/transform/TransformPane.vue
@@ -1,7 +1,12 @@
@@ -18,6 +23,8 @@ import { TransformStateKey } from '@/renderer/core/layout/injectionKeys'
import { useCanvasTransformSync } from '@/renderer/core/layout/transform/useCanvasTransformSync'
import { useTransformSettling } from '@/renderer/core/layout/transform/useTransformSettling'
import { useTransformState } from '@/renderer/core/layout/transform/useTransformState'
+import { useLOD } from '@/renderer/extensions/vueNodes/lod/useLOD'
+import { cn } from '@/utils/tailwindUtil'
interface TransformPaneProps {
canvas?: LGraphCanvas
@@ -34,6 +41,8 @@ const {
isNodeInViewport
} = useTransformState()
+const { isLOD } = useLOD(camera)
+
const canvasElement = computed(() => props.canvas?.canvas)
const { isTransforming: isInteracting } = useTransformSettling(canvasElement, {
settleDelay: 200,
diff --git a/src/renderer/extensions/vueNodes/components/ImagePreview.vue b/src/renderer/extensions/vueNodes/components/ImagePreview.vue
index 120b24657..d7219c926 100644
--- a/src/renderer/extensions/vueNodes/components/ImagePreview.vue
+++ b/src/renderer/extensions/vueNodes/components/ImagePreview.vue
@@ -94,17 +94,20 @@
-
-
-
- {{ $t('g.errorLoadingImage') }}
-
-
- {{ $t('g.loading') }}...
-
-
- {{ actualDimensions || $t('g.calculatingDimensions') }}
-
+
+
+
+
+ {{ $t('g.errorLoadingImage') }}
+
+
+ {{ $t('g.loading') }}...
+
+
+ {{ actualDimensions || $t('g.calculatingDimensions') }}
+
+
+
@@ -119,6 +122,8 @@ import { downloadFile } from '@/base/common/downloadUtil'
import { useCommandStore } from '@/stores/commandStore'
import { useNodeOutputStore } from '@/stores/imagePreviewStore'
+import LODFallback from './LODFallback.vue'
+
interface ImagePreviewProps {
/** Array of image URLs to display */
readonly imageUrls: readonly string[]
diff --git a/src/renderer/extensions/vueNodes/components/InputSlot.vue b/src/renderer/extensions/vueNodes/components/InputSlot.vue
index ef38c0754..1e8387335 100644
--- a/src/renderer/extensions/vueNodes/components/InputSlot.vue
+++ b/src/renderer/extensions/vueNodes/components/InputSlot.vue
@@ -10,12 +10,15 @@
/>
-
- {{ slotData.localized_name || slotData.name || `Input ${index}` }}
-
+
+
+ {{ slotData.localized_name || slotData.name || `Input ${index}` }}
+
+
+
@@ -38,6 +41,7 @@ import { useSlotElementTracking } from '@/renderer/extensions/vueNodes/composabl
import { useSlotLinkInteraction } from '@/renderer/extensions/vueNodes/composables/useSlotLinkInteraction'
import { cn } from '@/utils/tailwindUtil'
+import LODFallback from './LODFallback.vue'
import SlotConnectionDot from './SlotConnectionDot.vue'
interface InputSlotProps {
diff --git a/src/renderer/extensions/vueNodes/components/LGraphNode.vue b/src/renderer/extensions/vueNodes/components/LGraphNode.vue
index f764a82bf..08ff30d3b 100644
--- a/src/renderer/extensions/vueNodes/components/LGraphNode.vue
+++ b/src/renderer/extensions/vueNodes/components/LGraphNode.vue
@@ -23,7 +23,7 @@
bypassed,
'will-change-transform': isDragging
},
- lodCssClass,
+
shouldHandleNodePointerEvents
? 'pointer-events-auto'
: 'pointer-events-none'
@@ -48,10 +48,9 @@
-
+
@@ -96,28 +93,24 @@
>
@@ -152,7 +145,6 @@ import { useNodePointerInteractions } from '@/renderer/extensions/vueNodes/compo
import { useVueElementTracking } from '@/renderer/extensions/vueNodes/composables/useVueNodeResizeTracking'
import { useNodeExecutionState } from '@/renderer/extensions/vueNodes/execution/useNodeExecutionState'
import { useNodeLayout } from '@/renderer/extensions/vueNodes/layout/useNodeLayout'
-import { LODLevel, useLOD } from '@/renderer/extensions/vueNodes/lod/useLOD'
import { useNodePreviewState } from '@/renderer/extensions/vueNodes/preview/useNodePreviewState'
import { app } from '@/scripts/app'
import { useExecutionStore } from '@/stores/executionStore'
@@ -180,8 +172,7 @@ interface LGraphNodeProps {
const {
nodeData,
error = null,
- readonly = false,
- zoomLevel = 1
+ readonly = false
} = defineProps
()
const { handleNodeCollapse, handleNodeTitleUpdate, handleNodeSelect } =
@@ -218,18 +209,6 @@ const bypassed = computed((): boolean => nodeData.mode === 4)
// Use canvas interactions for proper wheel event handling and pointer event capture control
const { handleWheel, shouldHandleNodePointerEvents } = useCanvasInteractions()
-// LOD (Level of Detail) system based on zoom level
-const {
- lodLevel,
- shouldRenderWidgets,
- shouldRenderSlots,
- shouldRenderContent,
- lodCssClass
-} = useLOD(() => zoomLevel)
-
-// Computed properties for template usage
-const isMinimalLOD = computed(() => lodLevel.value === LODLevel.MINIMAL)
-
// Error boundary implementation
const renderError = ref(null)
const { toastErrorHandler } = useErrorHandling()
@@ -271,28 +250,17 @@ const hasCustomContent = computed(() => {
})
// Computed classes and conditions for better reusability
-const separatorClasses = cn(
- 'bg-sand-100 dark-theme:bg-charcoal-600 h-px mx-0 w-full'
-)
-const progressClasses = cn('h-2 bg-primary-500 transition-all duration-300')
+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'
const { latestPreviewUrl, shouldShowPreviewImg } = useNodePreviewState(
() => nodeData.id,
{
- isMinimalLOD,
isCollapsed
}
)
-// Common condition computations to avoid repetition
-const shouldShowWidgets = computed(
- () => shouldRenderWidgets.value && nodeData.widgets?.length
-)
-
-const shouldShowContent = computed(
- () => shouldRenderContent.value && hasCustomContent.value
-)
-
const borderClass = computed(() => {
if (hasAnyError.value) {
return 'border-error'
diff --git a/src/renderer/extensions/vueNodes/components/LODFallback.vue b/src/renderer/extensions/vueNodes/components/LODFallback.vue
new file mode 100644
index 000000000..9f77a3de9
--- /dev/null
+++ b/src/renderer/extensions/vueNodes/components/LODFallback.vue
@@ -0,0 +1,3 @@
+
+
+
diff --git a/src/renderer/extensions/vueNodes/components/NodeContent.vue b/src/renderer/extensions/vueNodes/components/NodeContent.vue
index 3df38bdf6..9137df463 100644
--- a/src/renderer/extensions/vueNodes/components/NodeContent.vue
+++ b/src/renderer/extensions/vueNodes/components/NodeContent.vue
@@ -21,7 +21,6 @@ import { computed, onErrorCaptured, ref } from 'vue'
import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
import { useErrorHandling } from '@/composables/useErrorHandling'
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
-import type { LODLevel } from '@/renderer/extensions/vueNodes/lod/useLOD'
import ImagePreview from './ImagePreview.vue'
@@ -29,7 +28,6 @@ interface NodeContentProps {
node?: LGraphNode // For backwards compatibility
nodeData?: VueNodeData // New clean data structure
readonly?: boolean
- lodLevel?: LODLevel
imageUrls?: string[]
}
diff --git a/src/renderer/extensions/vueNodes/components/NodeHeader.vue b/src/renderer/extensions/vueNodes/components/NodeHeader.vue
index 40b8a7fe0..67ab22b32 100644
--- a/src/renderer/extensions/vueNodes/components/NodeHeader.vue
+++ b/src/renderer/extensions/vueNodes/components/NodeHeader.vue
@@ -4,41 +4,44 @@