mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-22 15:29:44 +00:00
fix: emit layout change for batch node bounds (#5939)
## Summary Fixes issue where node size changes are not serialized by routing DOM-driven node bounds updates through a single CRDT operation so Vue node geometry stays synchronized with LiteGraph. ## Changes - **What**: Added `BatchUpdateBoundsOperation` to the layout store, applied it via the existing Yjs pipeline, notified link sync to recompute touched nodes, and covered the path with a regression test ## Review Focus Correctness of the new batch operation when multiple nodes update simultaneously, especially remote replay/undo scenarios and link geometry recomputation. ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-5939-fix-emit-layout-change-for-batch-node-bounds-2846d73d365081db8f8cca5bf7b85308) by [Unito](https://www.unito.io) --------- Co-authored-by: github-actions <github-actions@github.com>
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
||||
|
||||
import { calculateIntrinsicSize } from './calculateIntrinsicSize'
|
||||
|
||||
describe('calculateIntrinsicSize', () => {
|
||||
let element: HTMLElement
|
||||
|
||||
beforeEach(() => {
|
||||
// Create a test element
|
||||
element = document.createElement('div')
|
||||
element.style.width = '200px'
|
||||
element.style.height = '100px'
|
||||
document.body.appendChild(element)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
document.body.removeChild(element)
|
||||
})
|
||||
|
||||
it('should calculate intrinsic size and convert to canvas coordinates', () => {
|
||||
// Mock getBoundingClientRect to return specific dimensions
|
||||
const originalGetBoundingClientRect = element.getBoundingClientRect
|
||||
element.getBoundingClientRect = () => ({
|
||||
width: 300,
|
||||
height: 150,
|
||||
top: 0,
|
||||
left: 0,
|
||||
bottom: 150,
|
||||
right: 300,
|
||||
x: 0,
|
||||
y: 0,
|
||||
toJSON: () => ({})
|
||||
})
|
||||
|
||||
const scale = 2
|
||||
const result = calculateIntrinsicSize(element, scale)
|
||||
|
||||
// Should divide by scale to convert from screen to canvas coordinates
|
||||
expect(result).toEqual({
|
||||
width: 150, // 300 / 2
|
||||
height: 75 // 150 / 2
|
||||
})
|
||||
|
||||
element.getBoundingClientRect = originalGetBoundingClientRect
|
||||
})
|
||||
|
||||
it('should restore original size after measuring', () => {
|
||||
const originalWidth = element.style.width
|
||||
const originalHeight = element.style.height
|
||||
|
||||
element.getBoundingClientRect = () => ({
|
||||
width: 300,
|
||||
height: 150,
|
||||
top: 0,
|
||||
left: 0,
|
||||
bottom: 150,
|
||||
right: 300,
|
||||
x: 0,
|
||||
y: 0,
|
||||
toJSON: () => ({})
|
||||
})
|
||||
|
||||
calculateIntrinsicSize(element, 1)
|
||||
|
||||
// Should restore original styles
|
||||
expect(element.style.width).toBe(originalWidth)
|
||||
expect(element.style.height).toBe(originalHeight)
|
||||
})
|
||||
|
||||
it('should handle scale of 1 correctly', () => {
|
||||
element.getBoundingClientRect = () => ({
|
||||
width: 400,
|
||||
height: 200,
|
||||
top: 0,
|
||||
left: 0,
|
||||
bottom: 200,
|
||||
right: 400,
|
||||
x: 0,
|
||||
y: 0,
|
||||
toJSON: () => ({})
|
||||
})
|
||||
|
||||
const result = calculateIntrinsicSize(element, 1)
|
||||
|
||||
expect(result).toEqual({
|
||||
width: 400,
|
||||
height: 200
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle fractional scales', () => {
|
||||
element.getBoundingClientRect = () => ({
|
||||
width: 300,
|
||||
height: 150,
|
||||
top: 0,
|
||||
left: 0,
|
||||
bottom: 150,
|
||||
right: 300,
|
||||
x: 0,
|
||||
y: 0,
|
||||
toJSON: () => ({})
|
||||
})
|
||||
|
||||
const result = calculateIntrinsicSize(element, 0.5)
|
||||
|
||||
expect(result).toEqual({
|
||||
width: 600, // 300 / 0.5
|
||||
height: 300 // 150 / 0.5
|
||||
})
|
||||
})
|
||||
|
||||
it('should temporarily set width and height to auto during measurement', () => {
|
||||
let widthDuringMeasurement = ''
|
||||
let heightDuringMeasurement = ''
|
||||
|
||||
element.getBoundingClientRect = function (this: HTMLElement) {
|
||||
widthDuringMeasurement = this.style.width
|
||||
heightDuringMeasurement = this.style.height
|
||||
return {
|
||||
width: 300,
|
||||
height: 150,
|
||||
top: 0,
|
||||
left: 0,
|
||||
bottom: 150,
|
||||
right: 300,
|
||||
x: 0,
|
||||
y: 0,
|
||||
toJSON: () => ({})
|
||||
}
|
||||
}
|
||||
|
||||
calculateIntrinsicSize(element, 1)
|
||||
|
||||
// During measurement, styles should be set to 'auto'
|
||||
expect(widthDuringMeasurement).toBe('auto')
|
||||
expect(heightDuringMeasurement).toBe('auto')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* Calculate the intrinsic (minimum content-based) size of a node element
|
||||
*
|
||||
* Temporarily sets the element to auto-size to measure its natural content dimensions,
|
||||
* then converts from screen coordinates to canvas coordinates using the camera scale.
|
||||
*
|
||||
* @param element - The node element to measure
|
||||
* @param scale - Camera zoom scale for coordinate conversion
|
||||
* @returns The intrinsic minimum size in canvas coordinates
|
||||
*/
|
||||
export function calculateIntrinsicSize(
|
||||
element: HTMLElement,
|
||||
scale: number
|
||||
): { width: number; height: number } {
|
||||
// Store original size to restore later
|
||||
const originalWidth = element.style.width
|
||||
const originalHeight = element.style.height
|
||||
|
||||
// Temporarily set to auto to measure natural content size
|
||||
element.style.width = 'auto'
|
||||
element.style.height = 'auto'
|
||||
|
||||
const intrinsicRect = element.getBoundingClientRect()
|
||||
|
||||
// Restore original size
|
||||
element.style.width = originalWidth
|
||||
element.style.height = originalHeight
|
||||
|
||||
// Convert from screen coordinates to canvas coordinates
|
||||
return {
|
||||
width: intrinsicRect.width / scale,
|
||||
height: intrinsicRect.height / scale
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user