mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-16 10:27:32 +00:00
Compare commits
5 Commits
fix/load-a
...
perf/reduc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7cabe98c20 | ||
|
|
664ee8fcfc | ||
|
|
8f7f4bcc19 | ||
|
|
b0fd4fe4c1 | ||
|
|
b14d083c5f |
@@ -667,6 +667,9 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
* performant than {@link visible_nodes} for visibility checks.
|
||||
*/
|
||||
private _visible_node_ids: Set<NodeId> = new Set()
|
||||
|
||||
/** Cached per-frame link render context to avoid rebuilding per-link. */
|
||||
private _cachedLinkRenderContext: LinkRenderContext | null = null
|
||||
node_over?: LGraphNode
|
||||
node_capturing_input?: LGraphNode | null
|
||||
highlighted_links: Dictionary<boolean> = {}
|
||||
@@ -4806,6 +4809,9 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
if (!this.canvas || this.canvas.width == 0 || this.canvas.height == 0)
|
||||
return
|
||||
|
||||
// Reset per-frame caches so stale data is never used if a code path is skipped.
|
||||
this._cachedLinkRenderContext = null
|
||||
|
||||
// fps counting
|
||||
const now = LiteGraph.getTime()
|
||||
this.render_time = (now - this.last_draw_time) * 0.001
|
||||
@@ -4816,10 +4822,11 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
// Compute node size before drawing links.
|
||||
if (this.dirty_canvas || force_canvas) {
|
||||
this.computeVisibleNodes(undefined, this.visible_nodes)
|
||||
// Update visible node IDs
|
||||
this._visible_node_ids = new Set(
|
||||
this.visible_nodes.map((node) => node.id)
|
||||
)
|
||||
// Update visible node IDs (reuse existing Set to avoid allocation)
|
||||
this._visible_node_ids.clear()
|
||||
for (const node of this.visible_nodes) {
|
||||
this._visible_node_ids.add(node.id)
|
||||
}
|
||||
|
||||
// Arrange subgraph IO nodes
|
||||
const { subgraph } = this
|
||||
@@ -5014,10 +5021,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
colour,
|
||||
fromDirection,
|
||||
dragDirection,
|
||||
{
|
||||
...this.buildLinkRenderContext(),
|
||||
linkMarkerShape: LinkMarkerShape.None
|
||||
}
|
||||
this._cachedLinkRenderContext ?? this.buildLinkRenderContext()
|
||||
)
|
||||
}
|
||||
if (renderLink instanceof MovingInputLink) this.setDirty(false, true)
|
||||
@@ -5765,6 +5769,9 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
this.renderedPaths.clear()
|
||||
if (this.links_render_mode === LinkRenderType.HIDDEN_LINK) return
|
||||
|
||||
// Cache the link render context once per frame
|
||||
this._cachedLinkRenderContext = this.buildLinkRenderContext()
|
||||
|
||||
// Skip link rendering while waiting for slot positions to sync after reconfigure
|
||||
if (LiteGraph.vueNodesMode && layoutStore.pendingSlotSync) {
|
||||
this._visibleReroutes.clear()
|
||||
@@ -6016,19 +6023,28 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
|
||||
// Get all points this link passes through
|
||||
const reroutes = LLink.getReroutes(graph, link)
|
||||
const points: [Point, ...Point[], Point] = [
|
||||
startPos,
|
||||
...reroutes.map((x) => x.pos),
|
||||
endPos
|
||||
]
|
||||
|
||||
// Bounding box of all points (bezier overshoot on long links will be cut)
|
||||
const pointsX = points.map((x) => x[0])
|
||||
const pointsY = points.map((x) => x[1])
|
||||
link_bounding[0] = Math.min(...pointsX)
|
||||
link_bounding[1] = Math.min(...pointsY)
|
||||
link_bounding[2] = Math.max(...pointsX) - link_bounding[0]
|
||||
link_bounding[3] = Math.max(...pointsY) - link_bounding[1]
|
||||
// Compute bounding box inline to avoid allocating temporary arrays
|
||||
let minX = startPos[0]
|
||||
let minY = startPos[1]
|
||||
let maxX = minX
|
||||
let maxY = minY
|
||||
for (let i = 0; i < reroutes.length; i++) {
|
||||
const pos = reroutes[i].pos
|
||||
if (pos[0] < minX) minX = pos[0]
|
||||
else if (pos[0] > maxX) maxX = pos[0]
|
||||
if (pos[1] < minY) minY = pos[1]
|
||||
else if (pos[1] > maxY) maxY = pos[1]
|
||||
}
|
||||
if (endPos[0] < minX) minX = endPos[0]
|
||||
else if (endPos[0] > maxX) maxX = endPos[0]
|
||||
if (endPos[1] < minY) minY = endPos[1]
|
||||
else if (endPos[1] > maxY) maxY = endPos[1]
|
||||
|
||||
link_bounding[0] = minX
|
||||
link_bounding[1] = minY
|
||||
link_bounding[2] = maxX - minX
|
||||
link_bounding[3] = maxY - minY
|
||||
|
||||
// skip links outside of the visible area of the canvas
|
||||
if (!overlapBounding(link_bounding, margin_area)) return
|
||||
@@ -6096,8 +6112,7 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
// Skip the last segment if it is being dragged
|
||||
if (link._dragging) return
|
||||
|
||||
// Use runtime fallback; TypeScript cannot evaluate this correctly.
|
||||
const segmentStartPos = points.at(-2) ?? startPos
|
||||
const segmentStartPos = reroutes.at(-1)?.pos ?? startPos
|
||||
|
||||
// Render final link segment
|
||||
this.renderLink(
|
||||
@@ -6218,7 +6233,8 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
} = {}
|
||||
): void {
|
||||
if (this.linkRenderer) {
|
||||
const context = this.buildLinkRenderContext()
|
||||
const context =
|
||||
this._cachedLinkRenderContext ?? this.buildLinkRenderContext()
|
||||
this.linkRenderer.renderLinkDirect(
|
||||
ctx,
|
||||
a,
|
||||
|
||||
@@ -653,4 +653,70 @@ describe('LGraphNode', () => {
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('_slotsDirty flag', () => {
|
||||
test('starts dirty', () => {
|
||||
const n = new LGraphNode('Test')
|
||||
expect(n._slotsDirty).toBe(true)
|
||||
})
|
||||
|
||||
test('is cleared by _setConcreteSlots', () => {
|
||||
const n = new LGraphNode('Test')
|
||||
n._setConcreteSlots()
|
||||
expect(n._slotsDirty).toBe(false)
|
||||
})
|
||||
|
||||
test('skips work when clean', () => {
|
||||
const n = new LGraphNode('Test')
|
||||
n.addInput('in', 'number')
|
||||
n._setConcreteSlots()
|
||||
expect(n._slotsDirty).toBe(false)
|
||||
|
||||
const mapSpy = vi.spyOn(n.inputs, 'map')
|
||||
n._setConcreteSlots()
|
||||
expect(mapSpy).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test('is set by addInput', () => {
|
||||
const n = new LGraphNode('Test')
|
||||
n._setConcreteSlots()
|
||||
n.addInput('in', 'number')
|
||||
expect(n._slotsDirty).toBe(true)
|
||||
})
|
||||
|
||||
test('is set by removeInput', () => {
|
||||
const n = new LGraphNode('Test')
|
||||
n.addInput('in', 'number')
|
||||
n._setConcreteSlots()
|
||||
n.removeInput(0)
|
||||
expect(n._slotsDirty).toBe(true)
|
||||
})
|
||||
|
||||
test('is set by addOutput', () => {
|
||||
const n = new LGraphNode('Test')
|
||||
n._setConcreteSlots()
|
||||
n.addOutput('out', 'number')
|
||||
expect(n._slotsDirty).toBe(true)
|
||||
})
|
||||
|
||||
test('is set by removeOutput', () => {
|
||||
const n = new LGraphNode('Test')
|
||||
n.addOutput('out', 'number')
|
||||
n._setConcreteSlots()
|
||||
n.removeOutput(0)
|
||||
expect(n._slotsDirty).toBe(true)
|
||||
})
|
||||
|
||||
test('is set by configure', () => {
|
||||
const n = new LGraphNode('Test')
|
||||
n._setConcreteSlots()
|
||||
n.configure(
|
||||
getMockISerialisedNode({
|
||||
inputs: [{ name: 'a', type: 'number', link: null }],
|
||||
outputs: [{ name: 'b', type: 'number', links: null }]
|
||||
})
|
||||
)
|
||||
expect(n._slotsDirty).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -279,6 +279,8 @@ export class LGraphNode
|
||||
|
||||
private _concreteInputs: NodeInputSlot[] = []
|
||||
private _concreteOutputs: NodeOutputSlot[] = []
|
||||
/** @internal Set when inputs/outputs change; cleared by {@link _setConcreteSlots}. */
|
||||
_slotsDirty: boolean = true
|
||||
|
||||
properties: Dictionary<NodeProperty | undefined> = {}
|
||||
properties_info: INodePropertyInfo[] = []
|
||||
@@ -864,6 +866,7 @@ export class LGraphNode
|
||||
this.inputs = this.inputs.map((input) =>
|
||||
toClass(NodeInputSlot, input, this)
|
||||
)
|
||||
this._slotsDirty = true
|
||||
for (const [i, input] of this.inputs.entries()) {
|
||||
const link =
|
||||
this.graph && input.link != null
|
||||
@@ -1630,6 +1633,7 @@ export class LGraphNode
|
||||
|
||||
this.outputs ||= []
|
||||
this.outputs.push(output)
|
||||
this._slotsDirty = true
|
||||
this.onOutputAdded?.(output)
|
||||
|
||||
if (LiteGraph.auto_load_slot_types)
|
||||
@@ -1650,6 +1654,7 @@ export class LGraphNode
|
||||
}
|
||||
const { outputs } = this
|
||||
outputs.splice(slot, 1)
|
||||
this._slotsDirty = true
|
||||
|
||||
for (let i = slot; i < outputs.length; ++i) {
|
||||
const output = outputs[i]
|
||||
@@ -1687,6 +1692,7 @@ export class LGraphNode
|
||||
|
||||
this.inputs ||= []
|
||||
this.inputs.push(input)
|
||||
this._slotsDirty = true
|
||||
this.expandToFitContent()
|
||||
|
||||
this.onInputAdded?.(input)
|
||||
@@ -1706,6 +1712,7 @@ export class LGraphNode
|
||||
}
|
||||
const { inputs } = this
|
||||
const slot_info = inputs.splice(slot, 1)
|
||||
this._slotsDirty = true
|
||||
|
||||
for (let i = slot; i < inputs.length; ++i) {
|
||||
const input = inputs[i]
|
||||
@@ -4080,33 +4087,36 @@ export class LGraphNode
|
||||
ctx: CanvasRenderingContext2D,
|
||||
{ fromSlot, colorContext, editorAlpha, lowQuality }: DrawSlotsOptions
|
||||
) {
|
||||
for (const slot of [...this._concreteInputs, ...this._concreteOutputs]) {
|
||||
const isValidTarget = fromSlot && slot.isValidTarget(fromSlot)
|
||||
const isMouseOverSlot = this._isMouseOverSlot(slot)
|
||||
for (const slots of [this._concreteInputs, this._concreteOutputs]) {
|
||||
for (let s = 0; s < slots.length; s++) {
|
||||
const slot = slots[s]
|
||||
const isValidTarget = fromSlot && slot.isValidTarget(fromSlot)
|
||||
const isMouseOverSlot = this._isMouseOverSlot(slot)
|
||||
|
||||
// change opacity of incompatible slots when dragging a connection
|
||||
const isValid = !fromSlot || isValidTarget
|
||||
const highlight = isValid && isMouseOverSlot
|
||||
// change opacity of incompatible slots when dragging a connection
|
||||
const isValid = !fromSlot || isValidTarget
|
||||
const highlight = isValid && isMouseOverSlot
|
||||
|
||||
// Show slot if it's not a widget input slot
|
||||
// or if it's a widget input slot and satisfies one of the following:
|
||||
// - the mouse is over the widget
|
||||
// - the slot is valid during link drop
|
||||
// - the slot is connected
|
||||
if (
|
||||
isMouseOverSlot ||
|
||||
isValidTarget ||
|
||||
!slot.isWidgetInputSlot ||
|
||||
this._isMouseOverWidget(this.getWidgetFromSlot(slot)) ||
|
||||
slot.isConnected ||
|
||||
slot.alwaysVisible
|
||||
) {
|
||||
ctx.globalAlpha = isValid ? editorAlpha : 0.4 * editorAlpha
|
||||
slot.draw(ctx, {
|
||||
colorContext,
|
||||
lowQuality,
|
||||
highlight
|
||||
})
|
||||
// Show slot if it's not a widget input slot
|
||||
// or if it's a widget input slot and satisfies one of the following:
|
||||
// - the mouse is over the widget
|
||||
// - the slot is valid during link drop
|
||||
// - the slot is connected
|
||||
if (
|
||||
isMouseOverSlot ||
|
||||
isValidTarget ||
|
||||
!slot.isWidgetInputSlot ||
|
||||
this._isMouseOverWidget(this.getWidgetFromSlot(slot)) ||
|
||||
slot.isConnected ||
|
||||
slot.alwaysVisible
|
||||
) {
|
||||
ctx.globalAlpha = isValid ? editorAlpha : 0.4 * editorAlpha
|
||||
slot.draw(ctx, {
|
||||
colorContext,
|
||||
lowQuality,
|
||||
highlight
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4247,12 +4257,15 @@ export class LGraphNode
|
||||
* have been removed from the ecosystem.
|
||||
*/
|
||||
_setConcreteSlots(): void {
|
||||
if (!this._slotsDirty) return
|
||||
|
||||
this._concreteInputs = this.inputs.map((slot) =>
|
||||
toClass(NodeInputSlot, slot, this)
|
||||
)
|
||||
this._concreteOutputs = this.outputs.map((slot) =>
|
||||
toClass(NodeOutputSlot, slot, this)
|
||||
)
|
||||
this._slotsDirty = false
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -14,4 +14,18 @@ describe('LLink', () => {
|
||||
const link = new LLink(1, 'float', 4, 2, 5, 3)
|
||||
expect(link.serialize()).toMatchSnapshot('Basic')
|
||||
})
|
||||
|
||||
describe('getReroutes', () => {
|
||||
test('returns the same empty array instance for links without reroutes', () => {
|
||||
const network = { reroutes: new Map() }
|
||||
const link1 = new LLink(1, 'float', 4, 2, 5, 3)
|
||||
const link2 = new LLink(2, 'float', 4, 2, 5, 3)
|
||||
|
||||
const result1 = LLink.getReroutes(network, link1)
|
||||
const result2 = LLink.getReroutes(network, link2)
|
||||
|
||||
expect(result1).toHaveLength(0)
|
||||
expect(result1).toBe(result2)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -23,6 +23,8 @@ import type { Serialisable, SerialisableLLink } from './types/serialisation'
|
||||
|
||||
const layoutMutations = useLayoutMutations()
|
||||
|
||||
const EMPTY_REROUTES: Reroute[] = [] as Reroute[]
|
||||
|
||||
export type LinkId = number
|
||||
|
||||
export type SerialisedLLinkArray = [
|
||||
@@ -204,8 +206,11 @@ export class LLink implements LinkSegment, Serialisable<SerialisableLLink> {
|
||||
network: Pick<ReadonlyLinkNetwork, 'reroutes'>,
|
||||
linkSegment: LinkSegment
|
||||
): Reroute[] {
|
||||
if (linkSegment.parentId === undefined) return []
|
||||
return network.reroutes.get(linkSegment.parentId)?.getReroutes() ?? []
|
||||
if (linkSegment.parentId === undefined) return EMPTY_REROUTES
|
||||
return (
|
||||
network.reroutes.get(linkSegment.parentId)?.getReroutes() ??
|
||||
EMPTY_REROUTES
|
||||
)
|
||||
}
|
||||
|
||||
static getFirstReroute(
|
||||
|
||||
Reference in New Issue
Block a user