mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-25 08:49:36 +00:00
feat: when restored position has no nodes in viewport, automatically fit to view (#7435)
## Summary Sometimes the saved position is super far away from any of the nodes, which causes general confusion. This PR changes the `loadGraphData` logic to fit-to-view in those scenarios. Fixes https://github.com/Comfy-Org/ComfyUI_frontend/issues/7425 ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-7435-feat-when-restored-position-has-no-nodes-in-viewport-automatically-fit-to-view-2c86d73d36508119bf2ed9d361ec868f) by [Unito](https://www.unito.io) --------- Co-authored-by: github-actions <github-actions@github.com>
This commit is contained in:
@@ -0,0 +1,412 @@
|
|||||||
|
{
|
||||||
|
"id": "2ba0b800-2f13-4f21-b8d6-c6cdb0152cae",
|
||||||
|
"revision": 0,
|
||||||
|
"last_node_id": 16,
|
||||||
|
"last_link_id": 9,
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"type": "CheckpointLoaderSimple",
|
||||||
|
"pos": [
|
||||||
|
60,
|
||||||
|
200
|
||||||
|
],
|
||||||
|
"size": [
|
||||||
|
315,
|
||||||
|
98
|
||||||
|
],
|
||||||
|
"flags": {},
|
||||||
|
"order": 0,
|
||||||
|
"mode": 0,
|
||||||
|
"inputs": [],
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "MODEL",
|
||||||
|
"type": "MODEL",
|
||||||
|
"slot_index": 0,
|
||||||
|
"links": [
|
||||||
|
1
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "CLIP",
|
||||||
|
"type": "CLIP",
|
||||||
|
"slot_index": 1,
|
||||||
|
"links": [
|
||||||
|
3,
|
||||||
|
5
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "VAE",
|
||||||
|
"type": "VAE",
|
||||||
|
"slot_index": 2,
|
||||||
|
"links": [
|
||||||
|
8
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"Node name for S&R": "CheckpointLoaderSimple",
|
||||||
|
"cnr_id": "comfy-core",
|
||||||
|
"ver": "0.3.65",
|
||||||
|
"models": [
|
||||||
|
{
|
||||||
|
"name": "v1-5-pruned-emaonly-fp16.safetensors",
|
||||||
|
"url": "https://huggingface.co/Comfy-Org/stable-diffusion-v1-5-archive/resolve/main/v1-5-pruned-emaonly-fp16.safetensors?download=true",
|
||||||
|
"directory": "checkpoints"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"widgets_values": [
|
||||||
|
"v1-5-pruned-emaonly-fp16.safetensors"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"type": "KSampler",
|
||||||
|
"pos": [
|
||||||
|
870,
|
||||||
|
170
|
||||||
|
],
|
||||||
|
"size": [
|
||||||
|
315,
|
||||||
|
474
|
||||||
|
],
|
||||||
|
"flags": {},
|
||||||
|
"order": 4,
|
||||||
|
"mode": 0,
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"name": "model",
|
||||||
|
"type": "MODEL",
|
||||||
|
"link": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "positive",
|
||||||
|
"type": "CONDITIONING",
|
||||||
|
"link": 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "negative",
|
||||||
|
"type": "CONDITIONING",
|
||||||
|
"link": 6
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "latent_image",
|
||||||
|
"type": "LATENT",
|
||||||
|
"link": 2
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "LATENT",
|
||||||
|
"type": "LATENT",
|
||||||
|
"slot_index": 0,
|
||||||
|
"links": [
|
||||||
|
7
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"Node name for S&R": "KSampler",
|
||||||
|
"cnr_id": "comfy-core",
|
||||||
|
"ver": "0.3.65"
|
||||||
|
},
|
||||||
|
"widgets_values": [
|
||||||
|
685468484323813,
|
||||||
|
"randomize",
|
||||||
|
20,
|
||||||
|
8,
|
||||||
|
"euler",
|
||||||
|
"normal",
|
||||||
|
1
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 8,
|
||||||
|
"type": "VAEDecode",
|
||||||
|
"pos": [
|
||||||
|
975,
|
||||||
|
700
|
||||||
|
],
|
||||||
|
"size": [
|
||||||
|
210,
|
||||||
|
46
|
||||||
|
],
|
||||||
|
"flags": {},
|
||||||
|
"order": 5,
|
||||||
|
"mode": 0,
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"name": "samples",
|
||||||
|
"type": "LATENT",
|
||||||
|
"link": 7
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "vae",
|
||||||
|
"type": "VAE",
|
||||||
|
"link": 8
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "IMAGE",
|
||||||
|
"type": "IMAGE",
|
||||||
|
"slot_index": 0,
|
||||||
|
"links": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"Node name for S&R": "VAEDecode",
|
||||||
|
"cnr_id": "comfy-core",
|
||||||
|
"ver": "0.3.65"
|
||||||
|
},
|
||||||
|
"widgets_values": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 7,
|
||||||
|
"type": "CLIPTextEncode",
|
||||||
|
"pos": [
|
||||||
|
410,
|
||||||
|
410
|
||||||
|
],
|
||||||
|
"size": [
|
||||||
|
425.27801513671875,
|
||||||
|
180.6060791015625
|
||||||
|
],
|
||||||
|
"flags": {},
|
||||||
|
"order": 3,
|
||||||
|
"mode": 0,
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"name": "clip",
|
||||||
|
"type": "CLIP",
|
||||||
|
"link": 5
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "CONDITIONING",
|
||||||
|
"type": "CONDITIONING",
|
||||||
|
"slot_index": 0,
|
||||||
|
"links": [
|
||||||
|
6
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"Node name for S&R": "CLIPTextEncode",
|
||||||
|
"cnr_id": "comfy-core",
|
||||||
|
"ver": "0.3.65"
|
||||||
|
},
|
||||||
|
"widgets_values": [
|
||||||
|
"text, watermark"
|
||||||
|
],
|
||||||
|
"color": "#223",
|
||||||
|
"bgcolor": "#335"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"type": "EmptyLatentImage",
|
||||||
|
"pos": [
|
||||||
|
520,
|
||||||
|
690
|
||||||
|
],
|
||||||
|
"size": [
|
||||||
|
315,
|
||||||
|
106
|
||||||
|
],
|
||||||
|
"flags": {},
|
||||||
|
"order": 1,
|
||||||
|
"mode": 0,
|
||||||
|
"inputs": [],
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "LATENT",
|
||||||
|
"type": "LATENT",
|
||||||
|
"slot_index": 0,
|
||||||
|
"links": [
|
||||||
|
2
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"Node name for S&R": "EmptyLatentImage",
|
||||||
|
"cnr_id": "comfy-core",
|
||||||
|
"ver": "0.3.65"
|
||||||
|
},
|
||||||
|
"widgets_values": [
|
||||||
|
512,
|
||||||
|
512,
|
||||||
|
1
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 6,
|
||||||
|
"type": "CLIPTextEncode",
|
||||||
|
"pos": [
|
||||||
|
411.21649169921875,
|
||||||
|
203.68695068359375
|
||||||
|
],
|
||||||
|
"size": [
|
||||||
|
422.84503173828125,
|
||||||
|
164.31304931640625
|
||||||
|
],
|
||||||
|
"flags": {},
|
||||||
|
"order": 2,
|
||||||
|
"mode": 0,
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"name": "clip",
|
||||||
|
"type": "CLIP",
|
||||||
|
"link": 3
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "CONDITIONING",
|
||||||
|
"type": "CONDITIONING",
|
||||||
|
"slot_index": 0,
|
||||||
|
"links": [
|
||||||
|
4
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"Node name for S&R": "CLIPTextEncode",
|
||||||
|
"cnr_id": "comfy-core",
|
||||||
|
"ver": "0.3.65"
|
||||||
|
},
|
||||||
|
"widgets_values": [
|
||||||
|
"beautiful scenery nature glass bottle landscape, purple galaxy bottle,"
|
||||||
|
],
|
||||||
|
"color": "#232",
|
||||||
|
"bgcolor": "#353"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"links": [
|
||||||
|
[
|
||||||
|
1,
|
||||||
|
4,
|
||||||
|
0,
|
||||||
|
3,
|
||||||
|
0,
|
||||||
|
"MODEL"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2,
|
||||||
|
5,
|
||||||
|
0,
|
||||||
|
3,
|
||||||
|
3,
|
||||||
|
"LATENT"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3,
|
||||||
|
4,
|
||||||
|
1,
|
||||||
|
6,
|
||||||
|
0,
|
||||||
|
"CLIP"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
4,
|
||||||
|
6,
|
||||||
|
0,
|
||||||
|
3,
|
||||||
|
1,
|
||||||
|
"CONDITIONING"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
5,
|
||||||
|
4,
|
||||||
|
1,
|
||||||
|
7,
|
||||||
|
0,
|
||||||
|
"CLIP"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
6,
|
||||||
|
7,
|
||||||
|
0,
|
||||||
|
3,
|
||||||
|
2,
|
||||||
|
"CONDITIONING"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
7,
|
||||||
|
3,
|
||||||
|
0,
|
||||||
|
8,
|
||||||
|
0,
|
||||||
|
"LATENT"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
8,
|
||||||
|
4,
|
||||||
|
2,
|
||||||
|
8,
|
||||||
|
1,
|
||||||
|
"VAE"
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"groups": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"title": "Step 1 - Load model",
|
||||||
|
"bounding": [
|
||||||
|
50,
|
||||||
|
130,
|
||||||
|
335,
|
||||||
|
181.60000610351562
|
||||||
|
],
|
||||||
|
"color": "#3f789e",
|
||||||
|
"font_size": 24,
|
||||||
|
"flags": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"title": "Step 3 - Image size",
|
||||||
|
"bounding": [
|
||||||
|
510,
|
||||||
|
620,
|
||||||
|
335,
|
||||||
|
189.60000610351562
|
||||||
|
],
|
||||||
|
"color": "#3f789e",
|
||||||
|
"font_size": 24,
|
||||||
|
"flags": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"title": "Step 2 - Prompt",
|
||||||
|
"bounding": [
|
||||||
|
400,
|
||||||
|
130,
|
||||||
|
445.27801513671875,
|
||||||
|
467.2060852050781
|
||||||
|
],
|
||||||
|
"color": "#3f789e",
|
||||||
|
"font_size": 24,
|
||||||
|
"flags": {}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"config": {},
|
||||||
|
"extra": {
|
||||||
|
"ds": {
|
||||||
|
"scale": 0.44218252181616574,
|
||||||
|
"offset": [
|
||||||
|
-666.5670907104311,
|
||||||
|
-2227.894644048147
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"frontendVersion": "1.35.3",
|
||||||
|
"VHS_latentpreview": false,
|
||||||
|
"VHS_latentpreviewrate": 0,
|
||||||
|
"VHS_MetadataImage": true,
|
||||||
|
"VHS_KeepIntermediate": true,
|
||||||
|
"workflowRendererVersion": "LG"
|
||||||
|
},
|
||||||
|
"version": 0.4
|
||||||
|
}
|
||||||
20
browser_tests/tests/viewport.spec.ts
Normal file
20
browser_tests/tests/viewport.spec.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { expect } from '@playwright/test'
|
||||||
|
|
||||||
|
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||||
|
|
||||||
|
test.describe('Viewport', () => {
|
||||||
|
test('Fits view to nodes when saved viewport position is offscreen', async ({
|
||||||
|
comfyPage
|
||||||
|
}) => {
|
||||||
|
await comfyPage.loadWorkflow('viewport/default-viewport-saved-offscreen')
|
||||||
|
|
||||||
|
// Wait a few frames for rendering to stabilize
|
||||||
|
for (let i = 0; i < 5; i++) {
|
||||||
|
await comfyPage.nextFrame()
|
||||||
|
}
|
||||||
|
|
||||||
|
await expect(comfyPage.canvas).toHaveScreenshot(
|
||||||
|
'viewport-fits-when-saved-offscreen.png'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 80 KiB |
@@ -64,6 +64,7 @@ import type { ComfyExtension, MissingNodeType } from '@/types/comfy'
|
|||||||
import { type ExtensionManager } from '@/types/extensionTypes'
|
import { type ExtensionManager } from '@/types/extensionTypes'
|
||||||
import type { NodeExecutionId } from '@/types/nodeIdentification'
|
import type { NodeExecutionId } from '@/types/nodeIdentification'
|
||||||
import { graphToPrompt } from '@/utils/executionUtil'
|
import { graphToPrompt } from '@/utils/executionUtil'
|
||||||
|
import { anyItemOverlapsRect } from '@/utils/mathUtil'
|
||||||
import { collectAllNodes, forEachNode } from '@/utils/graphTraversalUtil'
|
import { collectAllNodes, forEachNode } from '@/utils/graphTraversalUtil'
|
||||||
import {
|
import {
|
||||||
getNodeByExecutionId,
|
getNodeByExecutionId,
|
||||||
@@ -1222,13 +1223,19 @@ export class ComfyApp {
|
|||||||
if (graphData.extra?.ds) {
|
if (graphData.extra?.ds) {
|
||||||
this.canvas.ds.offset = graphData.extra.ds.offset
|
this.canvas.ds.offset = graphData.extra.ds.offset
|
||||||
this.canvas.ds.scale = graphData.extra.ds.scale
|
this.canvas.ds.scale = graphData.extra.ds.scale
|
||||||
|
|
||||||
|
// Fit view if no nodes visible in restored viewport
|
||||||
|
this.canvas.ds.computeVisibleArea(this.canvas.viewport)
|
||||||
|
if (
|
||||||
|
!anyItemOverlapsRect(
|
||||||
|
this.rootGraph._nodes,
|
||||||
|
this.canvas.visible_area
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
requestAnimationFrame(() => useLitegraphService().fitView())
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// @note: Set view after the graph has been rendered once. fitView uses
|
requestAnimationFrame(() => useLitegraphService().fitView())
|
||||||
// boundingRect on nodes to calculate the view bounds, which only become
|
|
||||||
// available after the first render.
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
useLitegraphService().fitView()
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import type { ReadOnlyRect } from '@/lib/litegraph/src/interfaces'
|
import type { ReadOnlyRect } from '@/lib/litegraph/src/interfaces'
|
||||||
import type { Bounds } from '@/renderer/core/layout/types'
|
import type { Bounds } from '@/renderer/core/layout/types'
|
||||||
|
|
||||||
|
/** Simple 2D point or size as [x, y] or [width, height] */
|
||||||
|
type Vec2 = readonly [number, number]
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Finds the greatest common divisor (GCD) for two numbers using iterative
|
* Finds the greatest common divisor (GCD) for two numbers using iterative
|
||||||
* Euclidean algorithm. Uses iteration instead of recursion to avoid stack
|
* Euclidean algorithm. Uses iteration instead of recursion to avoid stack
|
||||||
@@ -91,3 +94,28 @@ export function computeUnionBounds(
|
|||||||
height: maxY - minY
|
height: maxY - minY
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if any item with pos/size overlaps a rectangle (AABB test).
|
||||||
|
* @param items Items with pos [x, y] and size [width, height]
|
||||||
|
* @param rect Rectangle as [x, y, width, height]
|
||||||
|
* @returns `true` if any item overlaps the rect
|
||||||
|
*/
|
||||||
|
export function anyItemOverlapsRect(
|
||||||
|
items: Iterable<{ pos: Vec2; size: Vec2 }>,
|
||||||
|
rect: ReadOnlyRect
|
||||||
|
): boolean {
|
||||||
|
const rectRight = rect[0] + rect[2]
|
||||||
|
const rectBottom = rect[1] + rect[3]
|
||||||
|
|
||||||
|
for (const item of items) {
|
||||||
|
const overlaps =
|
||||||
|
item.pos[0] < rectRight &&
|
||||||
|
item.pos[0] + item.size[0] > rect[0] &&
|
||||||
|
item.pos[1] < rectBottom &&
|
||||||
|
item.pos[1] + item.size[1] > rect[1]
|
||||||
|
|
||||||
|
if (overlaps) return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user