mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-26 09:19:43 +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:
@@ -1,3 +1,5 @@
|
||||
import type { Bounds } from '@/renderer/core/layout/types'
|
||||
|
||||
import type { CanvasColour, Point, RequiredProps, Size } from '../interfaces'
|
||||
import type { CanvasPointer, LGraphCanvas, LGraphNode } from '../litegraph'
|
||||
import type { CanvasPointerEvent } from './events'
|
||||
@@ -88,6 +90,8 @@ export type IWidget =
|
||||
| ISelectButtonWidget
|
||||
| ITextareaWidget
|
||||
| IAssetWidget
|
||||
| IImageCropWidget
|
||||
| IBoundingBoxWidget
|
||||
|
||||
export interface IBooleanWidget extends IBaseWidget<boolean, 'toggle'> {
|
||||
type: 'toggle'
|
||||
@@ -259,6 +263,18 @@ export interface IAssetWidget extends IBaseWidget<
|
||||
value: string
|
||||
}
|
||||
|
||||
/** Image crop widget for cropping image */
|
||||
export interface IImageCropWidget extends IBaseWidget<Bounds, 'imagecrop'> {
|
||||
type: 'imagecrop'
|
||||
value: Bounds
|
||||
}
|
||||
|
||||
/** Bounding box widget for defining regions with numeric inputs */
|
||||
export interface IBoundingBoxWidget extends IBaseWidget<Bounds, 'boundingbox'> {
|
||||
type: 'boundingbox'
|
||||
value: Bounds
|
||||
}
|
||||
|
||||
/**
|
||||
* Valid widget types. TS cannot provide easily extensible type safety for this at present.
|
||||
* Override linkedWidgets[]
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { t } from '@/i18n'
|
||||
import { drawTextInArea } from '@/lib/litegraph/src/draw'
|
||||
import { Rectangle } from '@/lib/litegraph/src/infrastructure/Rectangle'
|
||||
import type { Point } from '@/lib/litegraph/src/interfaces'
|
||||
@@ -227,6 +228,41 @@ export abstract class BaseWidget<
|
||||
if (showText && !this.computedDisabled) ctx.stroke()
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws a placeholder for widgets that only have a Vue implementation.
|
||||
* @param ctx The canvas context
|
||||
* @param options The options for drawing the widget
|
||||
* @param label The label to display (e.g., "ImageCrop", "BoundingBox")
|
||||
*/
|
||||
protected drawVueOnlyWarning(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
{ width }: DrawWidgetOptions,
|
||||
label: string
|
||||
): void {
|
||||
const { y, height } = this
|
||||
|
||||
ctx.save()
|
||||
|
||||
ctx.fillStyle = this.background_color
|
||||
ctx.fillRect(15, y, width - 30, height)
|
||||
|
||||
ctx.strokeStyle = this.outline_color
|
||||
ctx.strokeRect(15, y, width - 30, height)
|
||||
|
||||
ctx.fillStyle = this.text_color
|
||||
ctx.font = '11px monospace'
|
||||
ctx.textAlign = 'center'
|
||||
ctx.textBaseline = 'middle'
|
||||
|
||||
ctx.fillText(
|
||||
`${label}: ${t('widgets.node2only')}`,
|
||||
width / 2,
|
||||
y + height / 2
|
||||
)
|
||||
|
||||
ctx.restore()
|
||||
}
|
||||
|
||||
/**
|
||||
* A shared routine for drawing a label and value as text, truncated
|
||||
* if they exceed the available width.
|
||||
|
||||
22
src/lib/litegraph/src/widgets/BoundingBoxWidget.ts
Normal file
22
src/lib/litegraph/src/widgets/BoundingBoxWidget.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import type { IBoundingBoxWidget } from '../types/widgets'
|
||||
import { BaseWidget } from './BaseWidget'
|
||||
import type { DrawWidgetOptions, WidgetEventOptions } from './BaseWidget'
|
||||
|
||||
/**
|
||||
* Widget for defining bounding box regions.
|
||||
* This widget only has a Vue implementation.
|
||||
*/
|
||||
export class BoundingBoxWidget
|
||||
extends BaseWidget<IBoundingBoxWidget>
|
||||
implements IBoundingBoxWidget
|
||||
{
|
||||
override type = 'boundingbox' as const
|
||||
|
||||
drawWidget(ctx: CanvasRenderingContext2D, options: DrawWidgetOptions): void {
|
||||
this.drawVueOnlyWarning(ctx, options, 'BoundingBox')
|
||||
}
|
||||
|
||||
onClick(_options: WidgetEventOptions): void {
|
||||
// This widget only has a Vue implementation
|
||||
}
|
||||
}
|
||||
22
src/lib/litegraph/src/widgets/ImageCropWidget.ts
Normal file
22
src/lib/litegraph/src/widgets/ImageCropWidget.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import type { IImageCropWidget } from '../types/widgets'
|
||||
import { BaseWidget } from './BaseWidget'
|
||||
import type { DrawWidgetOptions, WidgetEventOptions } from './BaseWidget'
|
||||
|
||||
/**
|
||||
* Widget for displaying an image crop preview.
|
||||
* This widget only has a Vue implementation.
|
||||
*/
|
||||
export class ImageCropWidget
|
||||
extends BaseWidget<IImageCropWidget>
|
||||
implements IImageCropWidget
|
||||
{
|
||||
override type = 'imagecrop' as const
|
||||
|
||||
drawWidget(ctx: CanvasRenderingContext2D, options: DrawWidgetOptions): void {
|
||||
this.drawVueOnlyWarning(ctx, options, 'ImageCrop')
|
||||
}
|
||||
|
||||
onClick(_options: WidgetEventOptions): void {
|
||||
// This widget only has a Vue implementation
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,10 @@
|
||||
import { t } from '@/i18n'
|
||||
|
||||
import type { ITextareaWidget } from '../types/widgets'
|
||||
import { BaseWidget } from './BaseWidget'
|
||||
import type { DrawWidgetOptions, WidgetEventOptions } from './BaseWidget'
|
||||
|
||||
/**
|
||||
* Widget for multi-line text input
|
||||
* This is a widget that only has a Vue widgets implementation
|
||||
* Widget for multi-line text input.
|
||||
* This widget only has a Vue implementation.
|
||||
*/
|
||||
export class TextareaWidget
|
||||
extends BaseWidget<ITextareaWidget>
|
||||
@@ -15,35 +13,10 @@ export class TextareaWidget
|
||||
override type = 'textarea' as const
|
||||
|
||||
drawWidget(ctx: CanvasRenderingContext2D, options: DrawWidgetOptions): void {
|
||||
const { width } = options
|
||||
const { y, height } = this
|
||||
|
||||
const { fillStyle, strokeStyle, textAlign, textBaseline, font } = ctx
|
||||
|
||||
ctx.fillStyle = this.background_color
|
||||
ctx.fillRect(15, y, width - 30, height)
|
||||
|
||||
ctx.strokeStyle = this.outline_color
|
||||
ctx.strokeRect(15, y, width - 30, height)
|
||||
|
||||
ctx.fillStyle = this.text_color
|
||||
ctx.font = '11px monospace'
|
||||
ctx.textAlign = 'center'
|
||||
ctx.textBaseline = 'middle'
|
||||
|
||||
const text = `Textarea: ${t('widgets.node2only')}`
|
||||
ctx.fillText(text, width / 2, y + height / 2)
|
||||
|
||||
Object.assign(ctx, {
|
||||
fillStyle,
|
||||
strokeStyle,
|
||||
textAlign,
|
||||
textBaseline,
|
||||
font
|
||||
})
|
||||
this.drawVueOnlyWarning(ctx, options, 'Textarea')
|
||||
}
|
||||
|
||||
onClick(_options: WidgetEventOptions): void {
|
||||
// This is a widget that only has a Vue widgets implementation
|
||||
// This widget only has a Vue implementation
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import { t } from '@/i18n'
|
||||
|
||||
import type { ITreeSelectWidget } from '../types/widgets'
|
||||
import { BaseWidget } from './BaseWidget'
|
||||
import type { DrawWidgetOptions, WidgetEventOptions } from './BaseWidget'
|
||||
|
||||
/**
|
||||
* Widget for hierarchical tree selection
|
||||
* This is a widget that only has a Vue widgets implementation
|
||||
* Widget for hierarchical tree selection.
|
||||
* This widget only has a Vue implementation.
|
||||
*/
|
||||
export class TreeSelectWidget
|
||||
extends BaseWidget<ITreeSelectWidget>
|
||||
@@ -15,35 +13,10 @@ export class TreeSelectWidget
|
||||
override type = 'treeselect' as const
|
||||
|
||||
drawWidget(ctx: CanvasRenderingContext2D, options: DrawWidgetOptions): void {
|
||||
const { width } = options
|
||||
const { y, height } = this
|
||||
|
||||
const { fillStyle, strokeStyle, textAlign, textBaseline, font } = ctx
|
||||
|
||||
ctx.fillStyle = this.background_color
|
||||
ctx.fillRect(15, y, width - 30, height)
|
||||
|
||||
ctx.strokeStyle = this.outline_color
|
||||
ctx.strokeRect(15, y, width - 30, height)
|
||||
|
||||
ctx.fillStyle = this.text_color
|
||||
ctx.font = '11px monospace'
|
||||
ctx.textAlign = 'center'
|
||||
ctx.textBaseline = 'middle'
|
||||
|
||||
const text = `TreeSelect: ${t('widgets.node2only')}`
|
||||
ctx.fillText(text, width / 2, y + height / 2)
|
||||
|
||||
Object.assign(ctx, {
|
||||
fillStyle,
|
||||
strokeStyle,
|
||||
textAlign,
|
||||
textBaseline,
|
||||
font
|
||||
})
|
||||
this.drawVueOnlyWarning(ctx, options, 'TreeSelect')
|
||||
}
|
||||
|
||||
onClick(_options: WidgetEventOptions): void {
|
||||
// This is a widget that only has a Vue widgets implementation
|
||||
// This widget only has a Vue implementation
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import { toClass } from '@/lib/litegraph/src/utils/type'
|
||||
import { AssetWidget } from './AssetWidget'
|
||||
import { BaseWidget } from './BaseWidget'
|
||||
import { BooleanWidget } from './BooleanWidget'
|
||||
import { BoundingBoxWidget } from './BoundingBoxWidget'
|
||||
import { ButtonWidget } from './ButtonWidget'
|
||||
import { ChartWidget } from './ChartWidget'
|
||||
import { ColorWidget } from './ColorWidget'
|
||||
@@ -18,6 +19,7 @@ import { ComboWidget } from './ComboWidget'
|
||||
import { FileUploadWidget } from './FileUploadWidget'
|
||||
import { GalleriaWidget } from './GalleriaWidget'
|
||||
import { ImageCompareWidget } from './ImageCompareWidget'
|
||||
import { ImageCropWidget } from './ImageCropWidget'
|
||||
import { KnobWidget } from './KnobWidget'
|
||||
import { LegacyWidget } from './LegacyWidget'
|
||||
import { MarkdownWidget } from './MarkdownWidget'
|
||||
@@ -50,6 +52,8 @@ export type WidgetTypeMap = {
|
||||
selectbutton: SelectButtonWidget
|
||||
textarea: TextareaWidget
|
||||
asset: AssetWidget
|
||||
imagecrop: ImageCropWidget
|
||||
boundingbox: BoundingBoxWidget
|
||||
[key: string]: BaseWidget
|
||||
}
|
||||
|
||||
@@ -120,6 +124,10 @@ export function toConcreteWidget<TWidget extends IWidget | IBaseWidget>(
|
||||
return toClass(TextareaWidget, narrowedWidget, node)
|
||||
case 'asset':
|
||||
return toClass(AssetWidget, narrowedWidget, node)
|
||||
case 'imagecrop':
|
||||
return toClass(ImageCropWidget, narrowedWidget, node)
|
||||
case 'boundingbox':
|
||||
return toClass(BoundingBoxWidget, narrowedWidget, node)
|
||||
default: {
|
||||
if (wrapLegacyWidgets) return toClass(LegacyWidget, widget, node)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user