diff --git a/browser_tests/tests/lodThreshold.spec.ts b/browser_tests/tests/lodThreshold.spec.ts new file mode 100644 index 000000000..025347e4d --- /dev/null +++ b/browser_tests/tests/lodThreshold.spec.ts @@ -0,0 +1,197 @@ +import { expect } from '@playwright/test' + +import { comfyPageFixture as test } from '../fixtures/ComfyPage' + +test.describe('LOD Threshold', () => { + test('Should switch to low quality mode at correct zoom threshold', async ({ + comfyPage + }) => { + // Load a workflow with some nodes to render + await comfyPage.loadWorkflow('default') + + // Get initial LOD state and settings + const initialState = await comfyPage.page.evaluate(() => { + const canvas = window['app'].canvas + return { + lowQuality: canvas.low_quality, + scale: canvas.ds.scale, + minFontSize: canvas.min_font_size_for_lod + } + }) + + // Should start at normal zoom (not low quality) + expect(initialState.lowQuality).toBe(false) + expect(initialState.scale).toBeCloseTo(1, 1) + + // Calculate expected threshold (8px / 14px ≈ 0.571) + const expectedThreshold = initialState.minFontSize / 14 + // Can't access private _lowQualityZoomThreshold directly + + // Zoom out just above threshold (should still be high quality) + await comfyPage.zoom(120, 5) // Zoom out 5 steps + await comfyPage.nextFrame() + + const aboveThresholdState = await comfyPage.page.evaluate(() => { + const canvas = window['app'].canvas + return { + lowQuality: canvas.low_quality, + scale: canvas.ds.scale + } + }) + + // If still above threshold, should be high quality + if (aboveThresholdState.scale > expectedThreshold) { + expect(aboveThresholdState.lowQuality).toBe(false) + } + + // Zoom out more to trigger LOD (below threshold) + await comfyPage.zoom(120, 5) // Zoom out 5 more steps + await comfyPage.nextFrame() + + // Check that LOD is now active + const zoomedOutState = await comfyPage.page.evaluate(() => { + const canvas = window['app'].canvas + return { + lowQuality: canvas.low_quality, + scale: canvas.ds.scale + } + }) + + expect(zoomedOutState.scale).toBeLessThan(expectedThreshold) + expect(zoomedOutState.lowQuality).toBe(true) + + // Zoom back in to disable LOD (above threshold) + await comfyPage.zoom(-120, 15) // Zoom in 15 steps + await comfyPage.nextFrame() + + // Check that LOD is now inactive + const zoomedInState = await comfyPage.page.evaluate(() => { + const canvas = window['app'].canvas + return { + lowQuality: canvas.low_quality, + scale: canvas.ds.scale + } + }) + + expect(zoomedInState.scale).toBeGreaterThan(expectedThreshold) + expect(zoomedInState.lowQuality).toBe(false) + }) + + test('Should update threshold when font size setting changes', async ({ + comfyPage + }) => { + await comfyPage.loadWorkflow('default') + + // Change the font size setting to 14px (more aggressive LOD) + await comfyPage.setSetting('LiteGraph.Canvas.MinFontSizeForLOD', 14) + + // Check that font size updated + const newState = await comfyPage.page.evaluate(() => { + const canvas = window['app'].canvas + return { + minFontSize: canvas.min_font_size_for_lod + } + }) + + expect(newState.minFontSize).toBe(14) + // Expected threshold would be 14px / 14px = 1.0 + + // At default zoom, LOD should still be inactive (scale is exactly 1.0, not less than) + const lodState = await comfyPage.page.evaluate(() => { + return window['app'].canvas.low_quality + }) + expect(lodState).toBe(false) + + // Zoom out slightly to trigger LOD + await comfyPage.zoom(120, 1) // Zoom out 1 step + await comfyPage.nextFrame() + + const afterZoom = await comfyPage.page.evaluate(() => { + const canvas = window['app'].canvas + return { + lowQuality: canvas.low_quality, + scale: canvas.ds.scale + } + }) + + expect(afterZoom.scale).toBeLessThan(1.0) + expect(afterZoom.lowQuality).toBe(true) + }) + + test('Should disable LOD when font size is set to 0', async ({ + comfyPage + }) => { + await comfyPage.loadWorkflow('default') + + // Disable LOD by setting font size to 0 + await comfyPage.setSetting('LiteGraph.Canvas.MinFontSizeForLOD', 0) + + // Zoom out significantly + await comfyPage.zoom(120, 20) // Zoom out 20 steps + await comfyPage.nextFrame() + + // LOD should remain disabled even at very low zoom + const state = await comfyPage.page.evaluate(() => { + const canvas = window['app'].canvas + return { + lowQuality: canvas.low_quality, + scale: canvas.ds.scale, + minFontSize: canvas.min_font_size_for_lod + } + }) + + expect(state.minFontSize).toBe(0) // LOD disabled + expect(state.lowQuality).toBe(false) + expect(state.scale).toBeLessThan(0.2) // Very zoomed out + }) + + test('Should show visual difference between LOD on and off', async ({ + comfyPage + }) => { + // Load a workflow with text-heavy nodes for clear visual difference + await comfyPage.loadWorkflow('default') + + // Set zoom level clearly below the threshold to ensure LOD activates + const targetZoom = 0.4 // Well below default threshold of ~0.571 + + // Zoom to target level + await comfyPage.page.evaluate((zoom) => { + window['app'].canvas.ds.scale = zoom + window['app'].canvas.setDirty(true, true) + }, targetZoom) + await comfyPage.nextFrame() + + // Take snapshot with LOD active (default 8px setting) + await expect(comfyPage.canvas).toHaveScreenshot( + 'lod-comparison-low-quality.png' + ) + + const lowQualityState = await comfyPage.page.evaluate(() => { + const canvas = window['app'].canvas + return { + lowQuality: canvas.low_quality, + scale: canvas.ds.scale + } + }) + expect(lowQualityState.lowQuality).toBe(true) + + // Disable LOD to see high quality at same zoom + await comfyPage.setSetting('LiteGraph.Canvas.MinFontSizeForLOD', 0) + await comfyPage.nextFrame() + + // Take snapshot with LOD disabled (full quality at same zoom) + await expect(comfyPage.canvas).toHaveScreenshot( + 'lod-comparison-high-quality.png' + ) + + const highQualityState = await comfyPage.page.evaluate(() => { + const canvas = window['app'].canvas + return { + lowQuality: canvas.low_quality, + scale: canvas.ds.scale + } + }) + expect(highQualityState.lowQuality).toBe(false) + expect(highQualityState.scale).toBeCloseTo(targetZoom, 2) + }) +}) diff --git a/browser_tests/tests/lodThreshold.spec.ts-snapshots/lod-comparison-high-quality-chromium-linux.png b/browser_tests/tests/lodThreshold.spec.ts-snapshots/lod-comparison-high-quality-chromium-linux.png new file mode 100644 index 000000000..d34400f3f Binary files /dev/null and b/browser_tests/tests/lodThreshold.spec.ts-snapshots/lod-comparison-high-quality-chromium-linux.png differ diff --git a/browser_tests/tests/lodThreshold.spec.ts-snapshots/lod-comparison-low-quality-chromium-linux.png b/browser_tests/tests/lodThreshold.spec.ts-snapshots/lod-comparison-low-quality-chromium-linux.png new file mode 100644 index 000000000..a3212c734 Binary files /dev/null and b/browser_tests/tests/lodThreshold.spec.ts-snapshots/lod-comparison-low-quality-chromium-linux.png differ diff --git a/src/composables/useLitegraphSettings.ts b/src/composables/useLitegraphSettings.ts index 50488e3ac..fd340bb09 100644 --- a/src/composables/useLitegraphSettings.ts +++ b/src/composables/useLitegraphSettings.ts @@ -63,12 +63,11 @@ export const useLitegraphSettings = () => { }) watchEffect(() => { - const lowQualityRenderingZoomThreshold = settingStore.get( - 'LiteGraph.Canvas.LowQualityRenderingZoomThreshold' + const minFontSizeForLOD = settingStore.get( + 'LiteGraph.Canvas.MinFontSizeForLOD' ) if (canvasStore.canvas) { - canvasStore.canvas.low_quality_zoom_threshold = - lowQualityRenderingZoomThreshold + canvasStore.canvas.min_font_size_for_lod = minFontSizeForLOD canvasStore.canvas.setDirty(/* fg */ true, /* bg */ true) } }) diff --git a/src/constants/coreSettings.ts b/src/constants/coreSettings.ts index 280ec0df9..45093da5d 100644 --- a/src/constants/coreSettings.ts +++ b/src/constants/coreSettings.ts @@ -776,19 +776,36 @@ export const CORE_SETTINGS: SettingParams[] = [ type: 'boolean', versionAdded: '1.8.8' }, + { id: 'LiteGraph.Canvas.LowQualityRenderingZoomThreshold', - name: 'Low quality rendering zoom threshold', + type: 'hidden', + deprecated: true, + name: 'Low quality rendering zoom threshold (deprecated)', tooltip: 'Zoom level threshold for performance mode. Lower values (0.1) = quality at all zoom levels. Higher values (1.0) = performance mode even when zoomed in. Performance mode simplifies rendering by hiding text labels, shadows, and details.', - type: 'slider', attrs: { min: 0.1, max: 1, step: 0.01 }, defaultValue: 0.6, - versionAdded: '1.9.1' + versionAdded: '1.9.1', + versionModified: '1.26.7' + }, + { + id: 'LiteGraph.Canvas.MinFontSizeForLOD', + name: 'Zoom Node Level of Detail - font size threshold', + tooltip: + 'Controls when the nodes switch to low quality LOD rendering. Uses font size in pixels to determine when to switch. Set to 0 to disable. Values 1-24 set the minimum font size threshold for LOD - higher values (24px) = switch nodes to simplified rendering sooner when zooming out, lower values (1px) = maintain full node quality longer.', + type: 'slider', + attrs: { + min: 0, + max: 24, + step: 1 + }, + defaultValue: 8, + versionAdded: '1.26.7' }, { id: 'Comfy.Canvas.NavigationMode', diff --git a/src/lib/litegraph/src/LGraphCanvas.ts b/src/lib/litegraph/src/LGraphCanvas.ts index 33c05aed5..a32b8c354 100644 --- a/src/lib/litegraph/src/LGraphCanvas.ts +++ b/src/lib/litegraph/src/LGraphCanvas.ts @@ -441,11 +441,38 @@ export class LGraphCanvas LiteGraph.ROUND_RADIUS = value } + // Cached LOD threshold values for performance + private _lowQualityZoomThreshold: number = 0 + private _isLowQuality: boolean = false + /** - * Render low quality when zoomed out. + * Updates the low quality zoom threshold based on current settings. + * Called when min_font_size_for_lod or DPR changes. + */ + private updateLowQualityThreshold(): void { + if (this._min_font_size_for_lod === 0) { + // LOD disabled + this._lowQualityZoomThreshold = 0 + this._isLowQuality = false + return + } + + const baseFontSize = LiteGraph.NODE_TEXT_SIZE // 14px + const dprAdjustment = Math.sqrt(window.devicePixelRatio || 1) //Using sqrt here because higher DPR monitors do not linearily scale the readability of the font, instead they increase the font by some heurisitc, and to approximate we use sqrt to say bascially a DPR of 2 increases the readibility by 40%, 3 by 70% + + // Calculate the zoom level where text becomes unreadable + this._lowQualityZoomThreshold = + this._min_font_size_for_lod / (baseFontSize * dprAdjustment) + + // Update current state based on current zoom + this._isLowQuality = this.ds.scale < this._lowQualityZoomThreshold + } + + /** + * Render low quality when zoomed out based on minimum readable font size. */ get low_quality(): boolean { - return this.ds.scale < this.low_quality_zoom_threshold + return this._isLowQuality } options: { @@ -516,8 +543,21 @@ export class LGraphCanvas /** Shape of the markers shown at the midpoint of links. Default: Circle */ linkMarkerShape: LinkMarkerShape = LinkMarkerShape.Circle links_render_mode: number - /** Zoom threshold for low quality rendering. Zoom below this threshold will render low quality. */ - low_quality_zoom_threshold: number = 0.6 + /** Minimum font size in pixels before switching to low quality rendering. + * This intializes first and if we cant get the value from the settings we default to 8px + */ + private _min_font_size_for_lod: number = 8 + + get min_font_size_for_lod(): number { + return this._min_font_size_for_lod + } + + set min_font_size_for_lod(value: number) { + if (this._min_font_size_for_lod !== value) { + this._min_font_size_for_lod = value + this.updateLowQualityThreshold() + } + } /** mouse in canvas coordinates, where 0,0 is the top-left corner of the blue rectangle */ readonly mouse: Point /** mouse in graph coordinates, where 0,0 is the top-left corner of the blue rectangle */ @@ -700,6 +740,14 @@ export class LGraphCanvas this.ds = new DragAndScale(canvas) this.pointer = new CanvasPointer(canvas) + // Set up zoom change handler for efficient LOD updates + this.ds.onChanged = (scale: number, _offset: Point) => { + // Only check LOD threshold if it's enabled + if (this._lowQualityZoomThreshold > 0) { + this._isLowQuality = scale < this._lowQualityZoomThreshold + } + } + this.linkConnector.events.addEventListener('link-created', () => this.#dirty() ) @@ -885,6 +933,8 @@ export class LGraphCanvas } this.autoresize = options.autoresize + + this.updateLowQualityThreshold() } static onGroupAdd( diff --git a/src/locales/en/settings.json b/src/locales/en/settings.json index e2c759c5c..3f8d206db 100644 --- a/src/locales/en/settings.json +++ b/src/locales/en/settings.json @@ -388,9 +388,9 @@ "Topbar (2nd-row)": "Topbar (2nd-row)" } }, - "LiteGraph_Canvas_LowQualityRenderingZoomThreshold": { - "name": "Low quality rendering zoom threshold", - "tooltip": "Zoom level threshold for performance mode. Lower values (0.1) = quality at all zoom levels. Higher values (1.0) = performance mode even when zoomed in. Performance mode simplifies rendering by hiding text labels, shadows, and details." + "LiteGraph_Canvas_MinFontSizeForLOD": { + "name": "Zoom Node Level of Detail - font size threshold", + "tooltip": "Controls when the nodes switch to low quality LOD rendering. Uses font size in pixels to determine when to switch. Set to 0 to disable. Values 1-24 set the minimum font size threshold for LOD - higher values (24px) = switch nodes to simplified rendering sooner when zooming out, lower values (1px) = maintain full node quality longer." }, "LiteGraph_Canvas_MaximumFps": { "name": "Maximum FPS", diff --git a/src/schemas/apiSchema.ts b/src/schemas/apiSchema.ts index 1f30d31ec..145ee57a0 100644 --- a/src/schemas/apiSchema.ts +++ b/src/schemas/apiSchema.ts @@ -460,11 +460,12 @@ const zSettings = z.object({ 'Comfy.Workflow.AutoSaveDelay': z.number(), 'Comfy.Workflow.AutoSave': z.enum(['off', 'after delay']), 'Comfy.RerouteBeta': z.boolean(), - 'LiteGraph.Canvas.LowQualityRenderingZoomThreshold': z.number(), + 'LiteGraph.Canvas.MinFontSizeForLOD': z.number(), 'Comfy.Canvas.SelectionToolbox': z.boolean(), 'LiteGraph.Node.TooltipDelay': z.number(), 'LiteGraph.ContextMenu.Scaling': z.boolean(), 'LiteGraph.Reroute.SplineOffset': z.number(), + 'LiteGraph.Canvas.LowQualityRenderingZoomThreshold': z.number(), 'Comfy.Toast.DisableReconnectingToast': z.boolean(), 'Comfy.Workflow.Persist': z.boolean(), 'Comfy.TutorialCompleted': z.boolean(), diff --git a/src/stores/settingStore.ts b/src/stores/settingStore.ts index 4417816a3..3887688ac 100644 --- a/src/stores/settingStore.ts +++ b/src/stores/settingStore.ts @@ -188,6 +188,48 @@ export const useSettingStore = defineStore('setting', () => { ) } settingValues.value = await api.getSettings() + + // Migrate old zoom threshold setting to new font size setting + await migrateZoomThresholdToFontSize() + } + + /** + * Migrate the old zoom threshold setting to the new font size setting. + * Preserves the exact zoom threshold behavior by converting it to equivalent font size. + */ + async function migrateZoomThresholdToFontSize() { + const oldKey = 'LiteGraph.Canvas.LowQualityRenderingZoomThreshold' + const newKey = 'LiteGraph.Canvas.MinFontSizeForLOD' + + // Only migrate if old setting exists and new setting doesn't + if ( + settingValues.value[oldKey] !== undefined && + settingValues.value[newKey] === undefined + ) { + const oldValue = settingValues.value[oldKey] as number + + // Convert zoom threshold to equivalent font size to preserve exact behavior + // The threshold formula is: threshold = font_size / (14 * sqrt(DPR)) + // For DPR=1: threshold = font_size / 14 + // Therefore: font_size = threshold * 14 + // + // Examples: + // - Old 0.6 threshold → 0.6 * 14 = 8.4px → rounds to 8px (preserves ~60% zoom threshold) + // - Old 0.5 threshold → 0.5 * 14 = 7px (preserves 50% zoom threshold) + // - Old 1.0 threshold → 1.0 * 14 = 14px (preserves 100% zoom threshold) + const mappedFontSize = Math.round(oldValue * 14) + const clampedFontSize = Math.max(1, Math.min(24, mappedFontSize)) + + // Set the new value + settingValues.value[newKey] = clampedFontSize + + // Remove the old setting to prevent confusion + delete settingValues.value[oldKey] + + // Store the migrated setting + await api.storeSetting(newKey, clampedFontSize) + await api.storeSetting(oldKey, undefined) + } } return {