feat: add CurveEditor component (#8860)

## Summary
Prerequisite for upcoming native color correction nodes (ColorCurves).

Reusable curve editor with monotone cubic Hermite interpolation,
drag-to-add/move/delete control points, and SVG-based rendering.
Includes CurvePoint type, LUT generation utility, and useCurveEditor
composable for interaction logic.

## Screenshots (if applicable)



https://github.com/user-attachments/assets/948352c7-bdf2-40f9-a8f0-35bc2b2f3202

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8860-feat-add-CurveEditor-component-and-d3-shape-dependency-3076d73d3650817f8421f98e349569d0)
by [Unito](https://www.unito.io)
This commit is contained in:
Terry Jia
2026-02-24 00:19:06 -05:00
committed by GitHub
parent aab09b5f99
commit e333ad459e
13 changed files with 716 additions and 1 deletions

View File

@@ -0,0 +1,30 @@
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
import type { ICurveWidget } from '@/lib/litegraph/src/types/widgets'
import type {
CurveInputSpec,
InputSpec as InputSpecV2
} from '@/schemas/nodeDef/nodeDefSchemaV2'
import type { ComfyWidgetConstructorV2 } from '@/scripts/widgets'
export const useCurveWidget = (): ComfyWidgetConstructorV2 => {
return (node: LGraphNode, inputSpec: InputSpecV2): ICurveWidget => {
const spec = inputSpec as CurveInputSpec
const defaultValue = spec.default ?? [
[0, 0],
[1, 1]
]
const rawWidget = node.addWidget(
'curve',
spec.name,
[...defaultValue],
() => {}
)
if (rawWidget.type !== 'curve') {
throw new Error(`Unexpected widget type: ${rawWidget.type}`)
}
return rawWidget as ICurveWidget
}
}

View File

@@ -57,6 +57,9 @@ const WidgetImageCrop = defineAsyncComponent(
const WidgetBoundingBox = defineAsyncComponent(
() => import('@/components/boundingbox/WidgetBoundingBox.vue')
)
const WidgetCurve = defineAsyncComponent(
() => import('@/components/curve/WidgetCurve.vue')
)
export const FOR_TESTING = {
WidgetButton,
@@ -175,6 +178,14 @@ const coreWidgetDefinitions: Array<[string, WidgetDefinition]> = [
aliases: ['BOUNDING_BOX'],
essential: false
}
],
[
'curve',
{
component: WidgetCurve,
aliases: ['CURVE'],
essential: false
}
]
]
@@ -206,7 +217,7 @@ export const shouldRenderAsVue = (widget: Partial<SafeWidgetData>): boolean => {
return !widget.options?.canvasOnly && !!widget.type
}
const EXPANDING_TYPES = ['textarea', 'markdown', 'load3D'] as const
const EXPANDING_TYPES = ['textarea', 'markdown', 'load3D', 'curve'] as const
export function shouldExpand(type: string): boolean {
const canonicalType = getCanonicalType(type)