mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-19 22:09:37 +00:00
## Summary Fix subgraph node slot connector links appearing misaligned after workflow load, caused by a transform desync between LiteGraph's internal canvas transform and the Vue TransformPane's CSS transform. ## Changes - **What**: Changed `syncNodeSlotLayoutsFromDOM` to use DOM-relative measurement (slot position relative to its parent `[data-node-id]` element) instead of absolute canvas-space conversion via `clientPosToCanvasPos`. This makes the slot offset calculation independent of the global canvas transform, eliminating the frame-lag desync that occurred when `fitView()` updated `lgCanvas.ds` before the Vue CSS transform caught up. - **Cleanup**: Removed the unreachable fallback path that still used `clientPosToCanvasPos` when the parent node element wasn't found (every slot element is necessarily a child of a `[data-node-id]` element — if `closest()` fails the element is detached and measuring is meaningless). This also removed the `conv` parameter from `syncNodeSlotLayoutsFromDOM` and `flushScheduledSlotLayoutSync`, and the `useSharedCanvasPositionConversion` import. - **Test**: Added a Playwright browser test that loads a subgraph workflow with `workflowRendererVersion: "LG"` (triggering the 1.2x scale in `ensureCorrectLayoutScale`) as a template (triggering `fitView`), and verifies all slot connector positions are within bounds of their parent node element. ## Review Focus - The core change is in `useSlotElementTracking.ts` — the new measurement approach uses `getBoundingClientRect()` on both the slot and its parent node element, then divides by `currentScale` to get canvas-space offsets. This is simpler and more robust than the previous approach. - SubgraphNodes were disproportionately affected because they are relatively static and don't often trigger `ResizeObserver`-based re-syncs that would eventually correct stale offsets. ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-9121-fix-resolve-subgraph-node-slot-link-misalignment-during-workflow-load-3106d73d365081eca413c84f2e0571d6) by [Unito](https://www.unito.io) --------- Co-authored-by: GitHub Action <action@github.com> Co-authored-by: Alexander Brown <448862+DrJKL@users.noreply.github.com> Co-authored-by: Amp <amp@ampcode.com> Co-authored-by: Alexander Brown <drjkl@comfy.org>
133 lines
4.2 KiB
TypeScript
133 lines
4.2 KiB
TypeScript
import { readFileSync } from 'fs'
|
|
import { resolve } from 'path'
|
|
|
|
import { expect } from '@playwright/test'
|
|
|
|
import type { ComfyWorkflowJSON } from '@/platform/workflow/validation/schemas/workflowSchema'
|
|
|
|
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
|
|
|
interface SlotMeasurement {
|
|
key: string
|
|
offsetX: number
|
|
offsetY: number
|
|
}
|
|
|
|
interface NodeSlotData {
|
|
nodeId: string
|
|
isSubgraph: boolean
|
|
nodeW: number
|
|
nodeH: number
|
|
slots: SlotMeasurement[]
|
|
}
|
|
|
|
/**
|
|
* Regression test for link misalignment on SubgraphNodes when loading
|
|
* workflows with workflowRendererVersion: "LG".
|
|
*
|
|
* Root cause: ensureCorrectLayoutScale scales nodes by 1.2x for LG workflows,
|
|
* and fitView() updates lgCanvas.ds immediately. The Vue TransformPane's CSS
|
|
* transform lags by a frame, causing clientPosToCanvasPos to produce wrong
|
|
* slot offsets. The fix uses DOM-relative measurement instead.
|
|
*/
|
|
test.describe(
|
|
'Subgraph slot alignment after LG layout scale',
|
|
{ tag: ['@subgraph', '@canvas'] },
|
|
() => {
|
|
test('slot positions stay within node bounds after loading LG workflow', async ({
|
|
comfyPage
|
|
}) => {
|
|
const SLOT_BOUNDS_MARGIN = 20
|
|
await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true)
|
|
|
|
const workflowPath = resolve(
|
|
import.meta.dirname,
|
|
'../assets/subgraphs/basic-subgraph.json'
|
|
)
|
|
const workflow = JSON.parse(
|
|
readFileSync(workflowPath, 'utf-8')
|
|
) as ComfyWorkflowJSON
|
|
workflow.extra = {
|
|
...workflow.extra,
|
|
workflowRendererVersion: 'LG'
|
|
}
|
|
|
|
await comfyPage.page.evaluate(
|
|
(wf) =>
|
|
window.app!.loadGraphData(wf as ComfyWorkflowJSON, true, true, null, {
|
|
openSource: 'template'
|
|
}),
|
|
workflow
|
|
)
|
|
await comfyPage.nextFrame()
|
|
|
|
// Wait for slot elements to appear in DOM
|
|
await comfyPage.page.locator('[data-slot-key]').first().waitFor()
|
|
|
|
const result: NodeSlotData[] = await comfyPage.page.evaluate(() => {
|
|
const nodes = window.app!.graph._nodes
|
|
const slotData: NodeSlotData[] = []
|
|
|
|
for (const node of nodes) {
|
|
const nodeId = String(node.id)
|
|
const nodeEl = document.querySelector(
|
|
`[data-node-id="${nodeId}"]`
|
|
) as HTMLElement | null
|
|
if (!nodeEl) continue
|
|
|
|
const slotEls = nodeEl.querySelectorAll('[data-slot-key]')
|
|
if (slotEls.length === 0) continue
|
|
|
|
const slots: SlotMeasurement[] = []
|
|
|
|
const nodeRect = nodeEl.getBoundingClientRect()
|
|
for (const slotEl of slotEls) {
|
|
const slotRect = slotEl.getBoundingClientRect()
|
|
const slotKey = (slotEl as HTMLElement).dataset.slotKey ?? 'unknown'
|
|
slots.push({
|
|
key: slotKey,
|
|
offsetX: slotRect.left + slotRect.width / 2 - nodeRect.left,
|
|
offsetY: slotRect.top + slotRect.height / 2 - nodeRect.top
|
|
})
|
|
}
|
|
|
|
slotData.push({
|
|
nodeId,
|
|
isSubgraph: !!node.isSubgraphNode?.(),
|
|
nodeW: nodeRect.width,
|
|
nodeH: nodeRect.height,
|
|
slots
|
|
})
|
|
}
|
|
|
|
return slotData
|
|
})
|
|
|
|
const subgraphNodes = result.filter((n) => n.isSubgraph)
|
|
expect(subgraphNodes.length).toBeGreaterThan(0)
|
|
|
|
for (const node of subgraphNodes) {
|
|
for (const slot of node.slots) {
|
|
expect(
|
|
slot.offsetX,
|
|
`Slot ${slot.key} on node ${node.nodeId}: X offset ${slot.offsetX} outside node width ${node.nodeW}`
|
|
).toBeGreaterThanOrEqual(-SLOT_BOUNDS_MARGIN)
|
|
expect(
|
|
slot.offsetX,
|
|
`Slot ${slot.key} on node ${node.nodeId}: X offset ${slot.offsetX} outside node width ${node.nodeW}`
|
|
).toBeLessThanOrEqual(node.nodeW + SLOT_BOUNDS_MARGIN)
|
|
|
|
expect(
|
|
slot.offsetY,
|
|
`Slot ${slot.key} on node ${node.nodeId}: Y offset ${slot.offsetY} outside node height ${node.nodeH}`
|
|
).toBeGreaterThanOrEqual(-SLOT_BOUNDS_MARGIN)
|
|
expect(
|
|
slot.offsetY,
|
|
`Slot ${slot.key} on node ${node.nodeId}: Y offset ${slot.offsetY} outside node height ${node.nodeH}`
|
|
).toBeLessThanOrEqual(node.nodeH + SLOT_BOUNDS_MARGIN)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
)
|