Files
ComfyUI_frontend/src/renderer/extensions/vueNodes/widgets/composables/useBoundingBoxWidget.ts
Terry Jia be8916b4ce feat: Add visual crop preview widget for ImageCrop node - widget ImageCrop (#7825)
## Summary

Another implementation for image crop node, alternative for
https://github.com/Comfy-Org/ComfyUI_frontend/pull/7014
As discussed with @christian-byrne and @DrJKL we could have single
widget - IMAGECROP with 4 ints and UI preview.

However, this solution requires changing the definition of image crop
node in BE (sent
[here](https://github.com/comfyanonymous/ComfyUI/pull/11594)), which
will break the exsiting workflow, also it would not allow connect
separate int node as input, I am not sure it is a good idea.

So I keep two PRs openned for references

## Screenshots


https://github.com/user-attachments/assets/fde6938c-4395-48f6-ac05-6282c5eb8157

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7825-feat-Add-visual-crop-preview-widget-for-ImageCrop-node-widget-ImageCrop-2dc6d73d3650812bb8a2cdff4615032b)
by [Unito](https://www.unito.io)
2026-01-17 17:09:16 -05:00

104 lines
2.6 KiB
TypeScript

import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
import type {
IBaseWidget,
IBoundingBoxWidget,
IImageCropWidget,
INumericWidget
} from '@/lib/litegraph/src/types/widgets'
import type { Bounds } from '@/renderer/core/layout/types'
import type {
BoundingBoxInputSpec,
InputSpec as InputSpecV2
} from '@/schemas/nodeDef/nodeDefSchemaV2'
import type { ComfyWidgetConstructorV2 } from '@/scripts/widgets'
function isBoundingBoxLikeWidget(
widget: IBaseWidget
): widget is IBoundingBoxWidget | IImageCropWidget {
return widget.type === 'boundingbox' || widget.type === 'imagecrop'
}
function isNumericWidget(widget: IBaseWidget): widget is INumericWidget {
return widget.type === 'number'
}
export const useBoundingBoxWidget = (): ComfyWidgetConstructorV2 => {
return (
node: LGraphNode,
inputSpec: InputSpecV2
): IBoundingBoxWidget | IImageCropWidget => {
const spec = inputSpec as BoundingBoxInputSpec
const { name, component } = spec
const defaultValue: Bounds = spec.default ?? {
x: 0,
y: 0,
width: 512,
height: 512
}
const widgetType = component === 'ImageCrop' ? 'imagecrop' : 'boundingbox'
const fields: (keyof Bounds)[] = ['x', 'y', 'width', 'height']
const subWidgets: INumericWidget[] = []
const rawWidget = node.addWidget(
widgetType,
name,
{ ...defaultValue },
null,
{
serialize: true,
canvasOnly: false
}
)
if (!isBoundingBoxLikeWidget(rawWidget)) {
throw new Error(`Unexpected widget type: ${rawWidget.type}`)
}
const widget = rawWidget
widget.callback = () => {
for (let i = 0; i < fields.length; i++) {
const field = fields[i]
const subWidget = subWidgets[i]
if (subWidget) {
subWidget.value = widget.value[field]
}
}
}
for (const field of fields) {
const subWidget = node.addWidget(
'number',
field,
defaultValue[field],
function (this: INumericWidget, v: number) {
this.value = Math.round(v)
widget.value[field] = this.value
widget.callback?.(widget.value)
},
{
min: field === 'width' || field === 'height' ? 1 : 0,
max: 8192,
step: 10,
step2: 1,
precision: 0,
serialize: false,
canvasOnly: true
}
)
if (!isNumericWidget(subWidget)) {
throw new Error(`Unexpected widget type: ${subWidget.type}`)
}
subWidgets.push(subWidget)
}
widget.linkedWidgets = subWidgets
return widget
}
}