-
Notifications
You must be signed in to change notification settings - Fork 634
perf: add gpu hint and transform settle to prevent rasterizing while zooming (scale transform) #7417
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
perf: add gpu hint and transform settle to prevent rasterizing while zooming (scale transform) #7417
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -117,6 +117,73 @@ describe('TransformPane', () => { | |
| }) | ||
| }) | ||
|
|
||
| describe('canvas event listeners', () => { | ||
| it('should add event listeners to canvas on mount', async () => { | ||
| const mockCanvas = createMockCanvas() | ||
| mount(TransformPane, { | ||
| props: { | ||
| canvas: mockCanvas | ||
| } | ||
| }) | ||
|
|
||
| await nextTick() | ||
|
|
||
| expect(mockCanvas.canvas.addEventListener).toHaveBeenCalledWith( | ||
| 'wheel', | ||
| expect.any(Function), | ||
| expect.any(Object) | ||
| ) | ||
| expect(mockCanvas.canvas.addEventListener).not.toHaveBeenCalledWith( | ||
| 'pointerdown', | ||
| expect.any(Function), | ||
| expect.any(Object) | ||
| ) | ||
| expect(mockCanvas.canvas.addEventListener).not.toHaveBeenCalledWith( | ||
| 'pointerup', | ||
| expect.any(Function), | ||
| expect.any(Object) | ||
| ) | ||
| expect(mockCanvas.canvas.addEventListener).not.toHaveBeenCalledWith( | ||
| 'pointercancel', | ||
| expect.any(Function), | ||
| expect.any(Object) | ||
| ) | ||
| }) | ||
|
|
||
| it('should remove event listeners on unmount', async () => { | ||
| const mockCanvas = createMockCanvas() | ||
| const wrapper = mount(TransformPane, { | ||
| props: { | ||
| canvas: mockCanvas | ||
| } | ||
| }) | ||
|
|
||
| await nextTick() | ||
| wrapper.unmount() | ||
|
|
||
| expect(mockCanvas.canvas.removeEventListener).toHaveBeenCalledWith( | ||
| 'wheel', | ||
| expect.any(Function), | ||
| expect.any(Object) | ||
| ) | ||
| expect(mockCanvas.canvas.removeEventListener).not.toHaveBeenCalledWith( | ||
| 'pointerdown', | ||
| expect.any(Function), | ||
| expect.any(Object) | ||
| ) | ||
| expect(mockCanvas.canvas.removeEventListener).not.toHaveBeenCalledWith( | ||
| 'pointerup', | ||
| expect.any(Function), | ||
| expect.any(Object) | ||
| ) | ||
| expect(mockCanvas.canvas.removeEventListener).not.toHaveBeenCalledWith( | ||
| 'pointercancel', | ||
| expect.any(Function), | ||
| expect.any(Object) | ||
| ) | ||
| }) | ||
| }) | ||
|
Comment on lines
+120
to
+185
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Event-listener tests should assert handler identity (and avoid brittle “no pointer listeners” assertions).
Consider tightening to “wheel is registered with capture/passive” and “wheel is removed with the same handler”: it('should add event listeners to canvas on mount', async () => {
const mockCanvas = createMockCanvas()
mount(TransformPane, {
props: {
canvas: mockCanvas
}
})
await nextTick()
- expect(mockCanvas.canvas.addEventListener).toHaveBeenCalledWith(
- 'wheel',
- expect.any(Function),
- expect.any(Object)
- )
- expect(mockCanvas.canvas.addEventListener).not.toHaveBeenCalledWith(
- 'pointerdown',
- expect.any(Function),
- expect.any(Object)
- )
- expect(mockCanvas.canvas.addEventListener).not.toHaveBeenCalledWith(
- 'pointerup',
- expect.any(Function),
- expect.any(Object)
- )
- expect(mockCanvas.canvas.addEventListener).not.toHaveBeenCalledWith(
- 'pointercancel',
- expect.any(Function),
- expect.any(Object)
- )
+ const [type, handler, options] =
+ (mockCanvas.canvas.addEventListener as unknown as { mock: { calls: unknown[][] } })
+ .mock.calls[0]
+
+ expect(type).toBe('wheel')
+ expect(handler).toEqual(expect.any(Function))
+ expect(options).toEqual(expect.objectContaining({ capture: true }))
})
it('should remove event listeners on unmount', async () => {
const mockCanvas = createMockCanvas()
const wrapper = mount(TransformPane, {
props: {
canvas: mockCanvas
}
})
await nextTick()
+ const [, handler, options] =
+ (mockCanvas.canvas.addEventListener as unknown as { mock: { calls: unknown[][] } })
+ .mock.calls[0]
wrapper.unmount()
- expect(mockCanvas.canvas.removeEventListener).toHaveBeenCalledWith(
- 'wheel',
- expect.any(Function),
- expect.any(Object)
- )
- expect(mockCanvas.canvas.removeEventListener).not.toHaveBeenCalledWith(
- 'pointerdown',
- expect.any(Function),
- expect.any(Object)
- )
- expect(mockCanvas.canvas.removeEventListener).not.toHaveBeenCalledWith(
- 'pointerup',
- expect.any(Function),
- expect.any(Object)
- )
- expect(mockCanvas.canvas.removeEventListener).not.toHaveBeenCalledWith(
- 'pointercancel',
- expect.any(Function),
- expect.any(Object)
- )
+ expect(mockCanvas.canvas.removeEventListener).toHaveBeenCalledWith(
+ 'wheel',
+ handler as EventListener,
+ options as AddEventListenerOptions
+ )
})(If you’d rather avoid mocking intricacies altogether, consider using a real 🤖 Prompt for AI Agents |
||
|
|
||
| describe('interaction state management', () => { | ||
| it('should apply interacting class during interactions', async () => { | ||
| const mockCanvas = createMockCanvas() | ||
|
|
@@ -131,7 +198,9 @@ describe('TransformPane', () => { | |
| const transformPane = wrapper.find('[data-testid="transform-pane"]') | ||
|
|
||
| // Initially should not have interacting class | ||
| expect(transformPane.classes()).not.toContain('will-change-transform') | ||
| expect(transformPane.classes()).not.toContain( | ||
| 'transform-pane--interacting' | ||
| ) | ||
|
Comment on lines
+201
to
+203
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial Interaction class test only asserts the initial state; consider validating the “interacting” state is reachable. Right now it only checks the class is absent initially. If feasible, dispatch a 🤖 Prompt for AI Agents |
||
| }) | ||
|
|
||
| it('should handle pointer events for node delegation', async () => { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,84 @@ | ||
| import { useDebounceFn, useEventListener } from '@vueuse/core' | ||
| import { ref } from 'vue' | ||
| import type { MaybeRefOrGetter } from 'vue' | ||
|
|
||
| interface TransformSettlingOptions { | ||
| /** | ||
| * Delay in ms before transform is considered "settled" after last interaction | ||
| * @default 200 | ||
| */ | ||
| settleDelay?: number | ||
| /** | ||
| * Whether to use passive event listeners (better performance but can't preventDefault) | ||
| * @default true | ||
| */ | ||
| passive?: boolean | ||
| } | ||
|
Comment on lines
+5
to
+16
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: cat -n src/renderer/core/layout/transform/useTransformSettling.tsRepository: Comfy-Org/ComfyUI_frontend Length of output: 2905 🌐 Web query:
💡 Result: The VueUse docs show the TypeScript declarations for useEventListener (including its return type). In short: useEventListener returns a cleanup function you can call to unregister the listener, and the library also exports a type alias UseEventListenerReturn = ReturnType. See the Type Declarations in the docs for details and overloads. [1][2] Sources: 🏁 Script executed: rg "useTransformSettling" --type ts --type vue -A 3 -B 1Repository: Comfy-Org/ComfyUI_frontend Length of output: 97 🏁 Script executed: rg "useTransformSettling" -A 3 -B 1Repository: Comfy-Org/ComfyUI_frontend Length of output: 5818 🏁 Script executed: cat -n tests-ui/tests/composables/graph/useTransformSettling.test.tsRepository: Comfy-Org/ComfyUI_frontend Length of output: 6338 Fix documentation mismatch for default The JSDoc Additionally, consider whether returning the 🤖 Prompt for AI Agents |
||
|
|
||
| /** | ||
| * Tracks when canvas zoom transforms are actively changing vs settled. | ||
| * | ||
| * This composable helps optimize rendering quality during zoom transformations. | ||
| * When the user is actively zooming, we can reduce rendering quality | ||
| * for better performance. Once the transform "settles" (stops changing), we can | ||
| * trigger high-quality re-rasterization. | ||
| * | ||
| * The settling concept prevents constant quality switching during interactions | ||
| * by waiting for a period of inactivity before considering the transform complete. | ||
| * | ||
| * Uses VueUse's useEventListener for automatic cleanup and useDebounceFn for | ||
| * efficient settle detection. | ||
| * | ||
| * @example | ||
| * ```ts | ||
| * const { isTransforming } = useTransformSettling(canvasRef, { | ||
| * settleDelay: 200 | ||
| * }) | ||
| * | ||
| * // Use in CSS classes or rendering logic | ||
| * const cssClass = computed(() => ({ | ||
| * 'low-quality': isTransforming.value, | ||
| * 'high-quality': !isTransforming.value | ||
| * })) | ||
| * ``` | ||
| */ | ||
| export function useTransformSettling( | ||
| target: MaybeRefOrGetter<HTMLElement | null | undefined>, | ||
| options: TransformSettlingOptions = {} | ||
| ) { | ||
| const { settleDelay = 256, passive = true } = options | ||
|
|
||
| const isTransforming = ref(false) | ||
|
|
||
| /** | ||
| * Mark transform as active | ||
| */ | ||
| const markTransformActive = () => { | ||
| isTransforming.value = true | ||
| } | ||
|
|
||
| /** | ||
| * Mark transform as settled (debounced) | ||
| */ | ||
| const markTransformSettled = useDebounceFn(() => { | ||
| isTransforming.value = false | ||
| }, settleDelay) | ||
|
|
||
| /** | ||
| * Handle zoom transform event - mark active then queue settle | ||
| */ | ||
| const handleWheel = () => { | ||
| markTransformActive() | ||
| void markTransformSettled() | ||
| } | ||
|
|
||
| // Register wheel event listener with auto-cleanup | ||
| useEventListener(target, 'wheel', handleWheel, { | ||
| capture: true, | ||
| passive | ||
| }) | ||
|
|
||
| return { | ||
| isTransforming | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Over-broad
* { transition: none !important; }risks unintended UI regressions; prefer targeting known transitioning elements.Right now this disables all descendant transitions under nodes during zoom, not just the LiteGraph node transition surfaces. Given you already define transitions on
.lg-node .lg-slot/.lg-node .lg-widget(Line 1324-1329), consider scoping the override to those instead of*.🤖 Prompt for AI Agents