mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-13 17:10:06 +00:00
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)
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -54,6 +54,12 @@ const WidgetAudioUI = defineAsyncComponent(
|
||||
const Load3D = defineAsyncComponent(
|
||||
() => import('@/components/load3d/Load3D.vue')
|
||||
)
|
||||
const WidgetImageCrop = defineAsyncComponent(
|
||||
() => import('@/components/imagecrop/WidgetImageCrop.vue')
|
||||
)
|
||||
const WidgetBoundingBox = defineAsyncComponent(
|
||||
() => import('@/components/boundingbox/WidgetBoundingBox.vue')
|
||||
)
|
||||
|
||||
export const FOR_TESTING = {
|
||||
WidgetAudioUI,
|
||||
@@ -157,7 +163,23 @@ const coreWidgetDefinitions: Array<[string, WidgetDefinition]> = [
|
||||
essential: false
|
||||
}
|
||||
],
|
||||
['load3D', { component: Load3D, aliases: ['LOAD_3D'], essential: false }]
|
||||
['load3D', { component: Load3D, aliases: ['LOAD_3D'], essential: false }],
|
||||
[
|
||||
'imagecrop',
|
||||
{
|
||||
component: WidgetImageCrop,
|
||||
aliases: ['IMAGECROP'],
|
||||
essential: false
|
||||
}
|
||||
],
|
||||
[
|
||||
'boundingbox',
|
||||
{
|
||||
component: WidgetBoundingBox,
|
||||
aliases: ['BOUNDINGBOX'],
|
||||
essential: false
|
||||
}
|
||||
]
|
||||
]
|
||||
|
||||
const getComboWidgetAdditions = (): Map<string, Component> => {
|
||||
|
||||
Reference in New Issue
Block a user