mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-03 04:31:58 +00:00
refactor: migrate ES private fields to TypeScript private for Vue Proxy compatibility (#8440)
## Summary Migrates ECMAScript private fields (`#`) to TypeScript private (`private`) across LiteGraph to fix Vue Proxy reactivity incompatibility. ## Problem ES private fields (`#field`) are incompatible with Vue's Proxy-based reactivity system - accessing `#field` through a Proxy throws `TypeError: Cannot read private member from an object whose class did not declare it`. ## Solution - Converted all `#field` to `private _field` across 10 phases - Added `toJSON()` methods to `LGraph`, `NodeSlot`, `NodeInputSlot`, and `NodeOutputSlot` to prevent circular reference errors during serialization (TypeScript private fields are visible to `JSON.stringify` unlike true ES private fields) - Made `DragAndScale.element.data` non-enumerable to break canvas circular reference chain ## Testing - All 4027 unit tests pass - Added 9 new serialization tests to catch future circular reference issues - Browser tests (undo/redo, save workflows) verified working ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-8440-refactor-migrate-ES-private-fields-to-TypeScript-private-for-Vue-Proxy-compatibility-2f76d73d365081a3bd82d429a3e0fcb7) by [Unito](https://www.unito.io) --------- Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
@@ -273,8 +273,8 @@ export class LGraphNode
|
||||
inputs: INodeInputSlot[] = []
|
||||
outputs: INodeOutputSlot[] = []
|
||||
|
||||
#concreteInputs: NodeInputSlot[] = []
|
||||
#concreteOutputs: NodeOutputSlot[] = []
|
||||
private _concreteInputs: NodeInputSlot[] = []
|
||||
private _concreteOutputs: NodeOutputSlot[] = []
|
||||
|
||||
properties: Dictionary<NodeProperty | undefined> = {}
|
||||
properties_info: INodePropertyInfo[] = []
|
||||
@@ -438,24 +438,24 @@ export class LGraphNode
|
||||
}
|
||||
|
||||
/** @inheritdoc {@link renderArea} */
|
||||
#renderArea = new Rectangle()
|
||||
private _renderArea = new Rectangle()
|
||||
/**
|
||||
* Rect describing the node area, including shadows and any protrusions.
|
||||
* Determines if the node is visible. Calculated once at the start of every frame.
|
||||
*/
|
||||
get renderArea(): ReadOnlyRect {
|
||||
return this.#renderArea
|
||||
return this._renderArea
|
||||
}
|
||||
|
||||
/** @inheritdoc {@link boundingRect} */
|
||||
#boundingRect: Rectangle = new Rectangle()
|
||||
private _boundingRect: Rectangle = new Rectangle()
|
||||
/**
|
||||
* Cached node position & area as `x, y, width, height`. Includes changes made by {@link onBounding}, if present.
|
||||
*
|
||||
* Determines the node hitbox and other rendering effects. Calculated once at the start of every frame.
|
||||
*/
|
||||
get boundingRect(): ReadOnlyRectangle {
|
||||
return this.#boundingRect
|
||||
return this._boundingRect
|
||||
}
|
||||
|
||||
/** The offset from {@link pos} to the top-left of {@link boundingRect}. */
|
||||
@@ -753,7 +753,9 @@ export class LGraphNode
|
||||
onPropertyChange?(this: LGraphNode): void
|
||||
updateOutputData?(this: LGraphNode, origin_slot: number): void
|
||||
|
||||
#getErrorStrokeStyle(this: LGraphNode): IDrawBoundingOptions | undefined {
|
||||
private _getErrorStrokeStyle(
|
||||
this: LGraphNode
|
||||
): IDrawBoundingOptions | undefined {
|
||||
if (this.has_errors) {
|
||||
return {
|
||||
padding: 12,
|
||||
@@ -763,7 +765,9 @@ export class LGraphNode
|
||||
}
|
||||
}
|
||||
|
||||
#getSelectedStrokeStyle(this: LGraphNode): IDrawBoundingOptions | undefined {
|
||||
private _getSelectedStrokeStyle(
|
||||
this: LGraphNode
|
||||
): IDrawBoundingOptions | undefined {
|
||||
if (this.selected) {
|
||||
return {
|
||||
padding: this.has_errors ? 20 : undefined
|
||||
@@ -778,8 +782,8 @@ export class LGraphNode
|
||||
this.size = [LiteGraph.NODE_WIDTH, 60]
|
||||
this.pos = [10, 10]
|
||||
this.strokeStyles = {
|
||||
error: this.#getErrorStrokeStyle,
|
||||
selected: this.#getSelectedStrokeStyle
|
||||
error: this._getErrorStrokeStyle,
|
||||
selected: this._getSelectedStrokeStyle
|
||||
}
|
||||
// Initialize property manager with tracked properties
|
||||
this.changeTracker = new LGraphNodeProperties(this)
|
||||
@@ -2067,11 +2071,11 @@ export class LGraphNode
|
||||
* Called automatically at the start of every frame.
|
||||
*/
|
||||
updateArea(ctx?: CanvasRenderingContext2D): void {
|
||||
const bounds = this.#boundingRect
|
||||
const bounds = this._boundingRect
|
||||
this.measure(bounds, ctx)
|
||||
this.onBounding?.(bounds)
|
||||
|
||||
const renderArea = this.#renderArea
|
||||
const renderArea = this._renderArea
|
||||
renderArea.set(bounds)
|
||||
// 4 offset for collapsed node connection points
|
||||
renderArea[0] -= 4
|
||||
@@ -2293,7 +2297,7 @@ export class LGraphNode
|
||||
optsIn?: FindFreeSlotOptions & { returnObj?: TReturn }
|
||||
): INodeInputSlot | -1
|
||||
findInputSlotFree(optsIn?: FindFreeSlotOptions) {
|
||||
return this.#findFreeSlot(this.inputs, optsIn)
|
||||
return this._findFreeSlot(this.inputs, optsIn)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2308,14 +2312,14 @@ export class LGraphNode
|
||||
optsIn?: FindFreeSlotOptions & { returnObj?: TReturn }
|
||||
): INodeOutputSlot | -1
|
||||
findOutputSlotFree(optsIn?: FindFreeSlotOptions) {
|
||||
return this.#findFreeSlot(this.outputs, optsIn)
|
||||
return this._findFreeSlot(this.outputs, optsIn)
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the next free slot
|
||||
* @param slots The slots to search, i.e. this.inputs or this.outputs
|
||||
*/
|
||||
#findFreeSlot<TSlot extends INodeInputSlot | INodeOutputSlot>(
|
||||
private _findFreeSlot<TSlot extends INodeInputSlot | INodeOutputSlot>(
|
||||
slots: TSlot[],
|
||||
options?: FindFreeSlotOptions
|
||||
): TSlot | number {
|
||||
@@ -2357,7 +2361,7 @@ export class LGraphNode
|
||||
preferFreeSlot?: boolean,
|
||||
doNotUseOccupied?: boolean
|
||||
) {
|
||||
return this.#findSlotByType(
|
||||
return this._findSlotByType(
|
||||
this.inputs,
|
||||
type,
|
||||
returnObj,
|
||||
@@ -2387,7 +2391,7 @@ export class LGraphNode
|
||||
preferFreeSlot?: boolean,
|
||||
doNotUseOccupied?: boolean
|
||||
) {
|
||||
return this.#findSlotByType(
|
||||
return this._findSlotByType(
|
||||
this.outputs,
|
||||
type,
|
||||
returnObj,
|
||||
@@ -2433,14 +2437,14 @@ export class LGraphNode
|
||||
doNotUseOccupied?: boolean
|
||||
): number | INodeOutputSlot | INodeInputSlot {
|
||||
return input
|
||||
? this.#findSlotByType(
|
||||
? this._findSlotByType(
|
||||
this.inputs,
|
||||
type,
|
||||
returnObj,
|
||||
preferFreeSlot,
|
||||
doNotUseOccupied
|
||||
)
|
||||
: this.#findSlotByType(
|
||||
: this._findSlotByType(
|
||||
this.outputs,
|
||||
type,
|
||||
returnObj,
|
||||
@@ -2461,7 +2465,7 @@ export class LGraphNode
|
||||
* @see {findInputSlotByType}
|
||||
* @returns If a match is found, the slot if returnObj is true, otherwise the index. If no matches are found, -1
|
||||
*/
|
||||
#findSlotByType<TSlot extends INodeInputSlot | INodeOutputSlot>(
|
||||
private _findSlotByType<TSlot extends INodeInputSlot | INodeOutputSlot>(
|
||||
slots: TSlot[],
|
||||
type: ISlotType,
|
||||
returnObj?: boolean,
|
||||
@@ -3310,8 +3314,8 @@ export class LGraphNode
|
||||
// default vertical slots
|
||||
const offset = LiteGraph.NODE_SLOT_HEIGHT * 0.5
|
||||
const slotIndex = is_input
|
||||
? this.#defaultVerticalInputs.indexOf(this.inputs[slot_number])
|
||||
: this.#defaultVerticalOutputs.indexOf(this.outputs[slot_number])
|
||||
? this._defaultVerticalInputs.indexOf(this.inputs[slot_number])
|
||||
: this._defaultVerticalOutputs.indexOf(this.outputs[slot_number])
|
||||
|
||||
out[0] = is_input ? nodeX + offset : nodeX + this.size[0] + 1 - offset
|
||||
out[1] =
|
||||
@@ -3324,7 +3328,7 @@ export class LGraphNode
|
||||
/**
|
||||
* @internal The inputs that are not positioned with absolute coordinates.
|
||||
*/
|
||||
get #defaultVerticalInputs() {
|
||||
private get _defaultVerticalInputs() {
|
||||
return this.inputs.filter(
|
||||
(slot) => !slot.pos && !(this.widgets?.length && isWidgetInputSlot(slot))
|
||||
)
|
||||
@@ -3333,7 +3337,7 @@ export class LGraphNode
|
||||
/**
|
||||
* @internal The outputs that are not positioned with absolute coordinates.
|
||||
*/
|
||||
get #defaultVerticalOutputs() {
|
||||
private get _defaultVerticalOutputs() {
|
||||
return this.outputs.filter((slot: INodeOutputSlot) => !slot.pos)
|
||||
}
|
||||
|
||||
@@ -3341,7 +3345,7 @@ export class LGraphNode
|
||||
* Get the context needed for slot position calculations
|
||||
* @internal
|
||||
*/
|
||||
#getSlotPositionContext(): SlotPositionContext {
|
||||
private _getSlotPositionContext(): SlotPositionContext {
|
||||
return {
|
||||
nodeX: this.pos[0],
|
||||
nodeY: this.pos[1],
|
||||
@@ -3373,7 +3377,7 @@ export class LGraphNode
|
||||
* @returns Position of the centre of the input slot in graph co-ordinates.
|
||||
*/
|
||||
getInputSlotPos(input: INodeInputSlot): Point {
|
||||
return calculateInputSlotPosFromSlot(this.#getSlotPositionContext(), input)
|
||||
return calculateInputSlotPosFromSlot(this._getSlotPositionContext(), input)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -3916,13 +3920,13 @@ export class LGraphNode
|
||||
*/
|
||||
drawCollapsedSlots(ctx: CanvasRenderingContext2D): void {
|
||||
// Render the first connected slot only.
|
||||
for (const slot of this.#concreteInputs) {
|
||||
for (const slot of this._concreteInputs) {
|
||||
if (slot.link != null) {
|
||||
slot.drawCollapsed(ctx)
|
||||
break
|
||||
}
|
||||
}
|
||||
for (const slot of this.#concreteOutputs) {
|
||||
for (const slot of this._concreteOutputs) {
|
||||
if (slot.links?.length) {
|
||||
slot.drawCollapsed(ctx)
|
||||
break
|
||||
@@ -3934,7 +3938,7 @@ export class LGraphNode
|
||||
return [...this.inputs, ...this.outputs]
|
||||
}
|
||||
|
||||
#measureSlot(
|
||||
private _measureSlot(
|
||||
slot: NodeInputSlot | NodeOutputSlot,
|
||||
slotIndex: number,
|
||||
isInput: boolean
|
||||
@@ -3951,27 +3955,27 @@ export class LGraphNode
|
||||
slot.boundingRect[3] = LiteGraph.NODE_SLOT_HEIGHT
|
||||
}
|
||||
|
||||
#measureSlots(): ReadOnlyRect | null {
|
||||
private _measureSlots(): ReadOnlyRect | null {
|
||||
const slots: (NodeInputSlot | NodeOutputSlot)[] = []
|
||||
|
||||
for (const [slotIndex, slot] of this.#concreteInputs.entries()) {
|
||||
for (const [slotIndex, slot] of this._concreteInputs.entries()) {
|
||||
// Unrecognized nodes (Nodes with error) has inputs but no widgets. Treat
|
||||
// converted inputs as normal inputs.
|
||||
/** Widget input slots are handled in {@link layoutWidgetInputSlots} */
|
||||
if (this.widgets?.length && isWidgetInputSlot(slot)) continue
|
||||
|
||||
this.#measureSlot(slot, slotIndex, true)
|
||||
this._measureSlot(slot, slotIndex, true)
|
||||
slots.push(slot)
|
||||
}
|
||||
for (const [slotIndex, slot] of this.#concreteOutputs.entries()) {
|
||||
this.#measureSlot(slot, slotIndex, false)
|
||||
for (const [slotIndex, slot] of this._concreteOutputs.entries()) {
|
||||
this._measureSlot(slot, slotIndex, false)
|
||||
slots.push(slot)
|
||||
}
|
||||
|
||||
return slots.length ? createBounds(slots, 0) : null
|
||||
}
|
||||
|
||||
#getMouseOverSlot(slot: INodeSlot): INodeSlot | null {
|
||||
private _getMouseOverSlot(slot: INodeSlot): INodeSlot | null {
|
||||
const isInput = isINodeInputSlot(slot)
|
||||
const mouseOverId = this.mouseOver?.[isInput ? 'inputId' : 'outputId'] ?? -1
|
||||
if (mouseOverId === -1) {
|
||||
@@ -3980,11 +3984,11 @@ export class LGraphNode
|
||||
return isInput ? this.inputs[mouseOverId] : this.outputs[mouseOverId]
|
||||
}
|
||||
|
||||
#isMouseOverSlot(slot: INodeSlot): boolean {
|
||||
return this.#getMouseOverSlot(slot) === slot
|
||||
private _isMouseOverSlot(slot: INodeSlot): boolean {
|
||||
return this._getMouseOverSlot(slot) === slot
|
||||
}
|
||||
|
||||
#isMouseOverWidget(widget: IBaseWidget | undefined): boolean {
|
||||
private _isMouseOverWidget(widget: IBaseWidget | undefined): boolean {
|
||||
if (!widget) return false
|
||||
return this.mouseOver?.overWidget === widget
|
||||
}
|
||||
@@ -4016,9 +4020,9 @@ export class LGraphNode
|
||||
ctx: CanvasRenderingContext2D,
|
||||
{ fromSlot, colorContext, editorAlpha, lowQuality }: DrawSlotsOptions
|
||||
) {
|
||||
for (const slot of [...this.#concreteInputs, ...this.#concreteOutputs]) {
|
||||
for (const slot of [...this._concreteInputs, ...this._concreteOutputs]) {
|
||||
const isValidTarget = fromSlot && slot.isValidTarget(fromSlot)
|
||||
const isMouseOverSlot = this.#isMouseOverSlot(slot)
|
||||
const isMouseOverSlot = this._isMouseOverSlot(slot)
|
||||
|
||||
// change opacity of incompatible slots when dragging a connection
|
||||
const isValid = !fromSlot || isValidTarget
|
||||
@@ -4033,7 +4037,7 @@ export class LGraphNode
|
||||
isMouseOverSlot ||
|
||||
isValidTarget ||
|
||||
!slot.isWidgetInputSlot ||
|
||||
this.#isMouseOverWidget(this.getWidgetFromSlot(slot)) ||
|
||||
this._isMouseOverWidget(this.getWidgetFromSlot(slot)) ||
|
||||
slot.isConnected ||
|
||||
slot.alwaysVisible
|
||||
) {
|
||||
@@ -4054,7 +4058,7 @@ export class LGraphNode
|
||||
* - {@link IBaseWidget.y}
|
||||
* @param widgetStartY The y-coordinate of the first widget
|
||||
*/
|
||||
#arrangeWidgets(widgetStartY: number): void {
|
||||
private _arrangeWidgets(widgetStartY: number): void {
|
||||
if (!this.widgets || !this.widgets.length) return
|
||||
|
||||
const bodyHeight = this.bodyHeight
|
||||
@@ -4132,7 +4136,7 @@ export class LGraphNode
|
||||
/**
|
||||
* Arranges the layout of the node's widget input slots.
|
||||
*/
|
||||
#arrangeWidgetInputSlots(): void {
|
||||
private _arrangeWidgetInputSlots(): void {
|
||||
if (!this.widgets) return
|
||||
|
||||
const slotByWidgetName = new Map<
|
||||
@@ -4154,10 +4158,10 @@ export class LGraphNode
|
||||
const slot = slotByWidgetName.get(widget.name)
|
||||
if (!slot) continue
|
||||
|
||||
const actualSlot = this.#concreteInputs[slot.index]
|
||||
const actualSlot = this._concreteInputs[slot.index]
|
||||
const offset = LiteGraph.NODE_SLOT_HEIGHT * 0.5
|
||||
actualSlot.pos = [offset, widget.y + offset]
|
||||
this.#measureSlot(actualSlot, slot.index, true)
|
||||
this._measureSlot(actualSlot, slot.index, true)
|
||||
}
|
||||
} else {
|
||||
// For Vue positioning, just measure the slots without setting pos
|
||||
@@ -4165,7 +4169,7 @@ export class LGraphNode
|
||||
const slot = slotByWidgetName.get(widget.name)
|
||||
if (!slot) continue
|
||||
|
||||
this.#measureSlot(this.#concreteInputs[slot.index], slot.index, true)
|
||||
this._measureSlot(this._concreteInputs[slot.index], slot.index, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4178,10 +4182,10 @@ export class LGraphNode
|
||||
* have been removed from the ecosystem.
|
||||
*/
|
||||
_setConcreteSlots(): void {
|
||||
this.#concreteInputs = this.inputs.map((slot) =>
|
||||
this._concreteInputs = this.inputs.map((slot) =>
|
||||
toClass(NodeInputSlot, slot, this)
|
||||
)
|
||||
this.#concreteOutputs = this.outputs.map((slot) =>
|
||||
this._concreteOutputs = this.outputs.map((slot) =>
|
||||
toClass(NodeOutputSlot, slot, this)
|
||||
)
|
||||
}
|
||||
@@ -4190,12 +4194,12 @@ export class LGraphNode
|
||||
* Arranges node elements in preparation for rendering (slots & widgets).
|
||||
*/
|
||||
arrange(): void {
|
||||
const slotsBounds = this.#measureSlots()
|
||||
const slotsBounds = this._measureSlots()
|
||||
const widgetStartY = slotsBounds
|
||||
? slotsBounds[1] + slotsBounds[3] - this.pos[1]
|
||||
: 0
|
||||
this.#arrangeWidgets(widgetStartY)
|
||||
this.#arrangeWidgetInputSlots()
|
||||
this._arrangeWidgets(widgetStartY)
|
||||
this._arrangeWidgetInputSlots()
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user