refactor: remove 34 @ts-expect-error suppressions from widget-related files

This commit is contained in:
DrJKL
2026-01-10 20:20:38 -08:00
parent 56f9e9cd40
commit a1ae4aa7bd
6 changed files with 205 additions and 161 deletions

View File

@@ -1,4 +1,6 @@
import { t } from '@/i18n'
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
import type { INumericWidget } from '@/lib/litegraph/src/types/widgets'
import { useToastStore } from '@/platform/updates/common/toastStore'
import { api } from '../../scripts/api'
@@ -6,15 +8,19 @@ import { app } from '../../scripts/app'
const WEBCAM_READY = Symbol()
interface WebcamNode extends LGraphNode {
[WEBCAM_READY]?: Promise<HTMLVideoElement>
}
app.registerExtension({
name: 'Comfy.WebcamCapture',
getCustomWidgets() {
return {
WEBCAM(node, inputName) {
// @ts-expect-error fixme ts strict error
let res
// @ts-expect-error fixme ts strict error
node[WEBCAM_READY] = new Promise((resolve) => (res = resolve))
WEBCAM(node: WebcamNode, inputName: string) {
let resolveVideo: (video: HTMLVideoElement) => void
node[WEBCAM_READY] = new Promise((resolve) => {
resolveVideo = resolve
})
const container = document.createElement('div')
container.style.background = 'rgba(0,0,0,0.25)'
@@ -31,10 +37,12 @@ app.registerExtension({
})
container.replaceChildren(video)
// @ts-expect-error fixme ts strict error
setTimeout(() => res(video), 500) // Fallback as loadedmetadata doesnt fire sometimes?
// @ts-expect-error fixme ts strict error
video.addEventListener('loadedmetadata', () => res(video), false)
setTimeout(() => resolveVideo(video), 500) // Fallback as loadedmetadata doesnt fire sometimes?
video.addEventListener(
'loadedmetadata',
() => resolveVideo(video),
false
)
video.srcObject = stream
video.play()
} catch (error) {
@@ -44,16 +52,16 @@ app.registerExtension({
label.style.maxHeight = '100%'
label.style.whiteSpace = 'pre-wrap'
const errorMessage =
error instanceof Error ? error.message : String(error)
if (window.isSecureContext) {
label.textContent =
'Unable to load webcam, please ensure access is granted:\n' +
// @ts-expect-error fixme ts strict error
error.message
errorMessage
} else {
label.textContent =
'Unable to load webcam. A secure context is required, if you are not accessing ComfyUI on localhost (127.0.0.1) you will have to enable TLS (https)\n\n' +
// @ts-expect-error fixme ts strict error
error.message
errorMessage
}
container.replaceChildren(label)
@@ -66,32 +74,31 @@ app.registerExtension({
}
}
},
nodeCreated(node) {
nodeCreated(node: WebcamNode) {
if ((node.type, node.constructor.comfyClass !== 'WebcamCapture')) return
// @ts-expect-error fixme ts strict error
let video
// @ts-expect-error fixme ts strict error
const camera = node.widgets.find((w) => w.name === 'image')
// @ts-expect-error fixme ts strict error
const w = node.widgets.find((w) => w.name === 'width')
// @ts-expect-error fixme ts strict error
const h = node.widgets.find((w) => w.name === 'height')
// @ts-expect-error fixme ts strict error
const captureOnQueue = node.widgets.find(
let video: HTMLVideoElement | undefined
const camera = node.widgets?.find((w) => w.name === 'image')
const widthWidget = node.widgets?.find((w) => w.name === 'width') as
| INumericWidget
| undefined
const heightWidget = node.widgets?.find((w) => w.name === 'height') as
| INumericWidget
| undefined
const captureOnQueue = node.widgets?.find(
(w) => w.name === 'capture_on_queue'
)
const canvas = document.createElement('canvas')
const capture = () => {
// @ts-expect-error widget value type narrow down
canvas.width = w.value
// @ts-expect-error widget value type narrow down
canvas.height = h.value
if (!widthWidget || !heightWidget || !video) return
const width = widthWidget.value ?? 640
const height = heightWidget.value ?? 480
canvas.width = width
canvas.height = height
const ctx = canvas.getContext('2d')
// @ts-expect-error widget value type narrow down
ctx.drawImage(video, 0, 0, w.value, h.value)
ctx?.drawImage(video, 0, 0, width, height)
const data = canvas.toDataURL('image/png')
const img = new Image()
@@ -112,48 +119,47 @@ app.registerExtension({
btn.disabled = true
btn.serializeValue = () => undefined
// @ts-expect-error fixme ts strict error
camera.serializeValue = async () => {
// @ts-expect-error fixme ts strict error
if (captureOnQueue.value) {
capture()
} else if (!node.imgs?.length) {
const err = `No webcam image captured`
useToastStore().addAlert(err)
throw new Error(err)
}
if (camera) {
camera.serializeValue = async () => {
if (captureOnQueue?.value) {
capture()
} else if (!node.imgs?.length) {
const err = `No webcam image captured`
useToastStore().addAlert(err)
throw new Error(err)
}
// Upload image to temp storage
// @ts-expect-error fixme ts strict error
const blob = await new Promise<Blob>((r) => canvas.toBlob(r))
const name = `${+new Date()}.png`
const file = new File([blob], name)
const body = new FormData()
body.append('image', file)
body.append('subfolder', 'webcam')
body.append('type', 'temp')
const resp = await api.fetchApi('/upload/image', {
method: 'POST',
body
})
if (resp.status !== 200) {
const err = `Error uploading camera image: ${resp.status} - ${resp.statusText}`
useToastStore().addAlert(err)
throw new Error(err)
// Upload image to temp storage
const blob = await new Promise<Blob>((resolve, reject) => {
canvas.toBlob((b) => (b ? resolve(b) : reject(new Error('No blob'))))
})
const name = `${+new Date()}.png`
const file = new File([blob], name)
const body = new FormData()
body.append('image', file)
body.append('subfolder', 'webcam')
body.append('type', 'temp')
const resp = await api.fetchApi('/upload/image', {
method: 'POST',
body
})
if (resp.status !== 200) {
const err = `Error uploading camera image: ${resp.status} - ${resp.statusText}`
useToastStore().addAlert(err)
throw new Error(err)
}
return `webcam/${name} [temp]`
}
return `webcam/${name} [temp]`
}
// @ts-expect-error fixme ts strict error
node[WEBCAM_READY].then((v) => {
node[WEBCAM_READY]?.then((v) => {
video = v
// If width isn't specified then use video output resolution
// @ts-expect-error fixme ts strict error
if (!w.value) {
// @ts-expect-error fixme ts strict error
w.value = video.videoWidth || 640
// @ts-expect-error fixme ts strict error
h.value = video.videoHeight || 480
if (widthWidget && !widthWidget.value) {
widthWidget.value = video.videoWidth || 640
}
if (heightWidget && !heightWidget.value) {
heightWidget.value = video.videoHeight || 480
}
btn.disabled = false
btn.label = t('g.capture')

View File

@@ -9,9 +9,15 @@ import type {
ISlotType,
LLink
} from '@/lib/litegraph/src/litegraph'
import type { IWidgetLocator } from '@/lib/litegraph/src/interfaces'
import { NodeSlot } from '@/lib/litegraph/src/node/NodeSlot'
import type { CanvasPointerEvent } from '@/lib/litegraph/src/types/events'
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
import type {
IBaseWidget,
IComboWidget,
INumericWidget
} from '@/lib/litegraph/src/types/widgets'
import { isPrimitiveNode } from '@/renderer/utils/nodeTypeGuards'
import type { InputSpec } from '@/schemas/nodeDefSchema'
import { app } from '@/scripts/app'
import {
@@ -22,7 +28,16 @@ import {
import { CONFIG, GET_CONFIG } from '@/services/litegraphService'
import { mergeInputSpec } from '@/utils/nodeDefUtil'
import { applyTextReplacements } from '@/utils/searchAndReplace'
import { isPrimitiveNode } from '@/renderer/utils/nodeTypeGuards'
/**
* Widget locator with CONFIG symbol properties for accessing input spec.
* Used on input/output slots to retrieve widget configuration.
*/
interface IWidgetLocatorWithConfig extends IWidgetLocator {
name: string
[GET_CONFIG]?: () => InputSpec
[CONFIG]?: InputSpec
}
const replacePropertyName = 'Run widget replace on values'
export class PrimitiveNode extends LGraphNode {
@@ -89,14 +104,17 @@ export class PrimitiveNode extends LGraphNode {
override refreshComboInNode() {
const widget = this.widgets?.[0]
if (widget?.type === 'combo') {
// @ts-expect-error fixme ts strict error
widget.options.values = this.outputs[0].widget[GET_CONFIG]()[0]
// @ts-expect-error fixme ts strict error
if (!widget.options.values.includes(widget.value as string)) {
// @ts-expect-error fixme ts strict error
widget.value = widget.options.values[0]
;(widget.callback as Function)(widget.value)
const comboWidget = widget as IComboWidget
const widgetLocator = this.outputs[0].widget as IWidgetLocatorWithConfig
const config = widgetLocator?.[GET_CONFIG]?.()
const rawValues = config?.[0]
if (Array.isArray(rawValues)) {
const newValues = rawValues.map(String)
comboWidget.options.values = newValues
if (!newValues.includes(String(comboWidget.value))) {
comboWidget.value = newValues[0]
comboWidget.callback?.(comboWidget.value)
}
}
}
}
@@ -186,15 +204,16 @@ export class PrimitiveNode extends LGraphNode {
const input = theirNode.inputs[link.target_slot]
if (!input) return
let widget
let widget: IWidgetLocatorWithConfig
if (!input.widget) {
if (!(input.type in ComfyWidgets)) return
widget = { name: input.name, [GET_CONFIG]: () => [input.type, {}] } //fake widget
if (typeof input.type !== 'string' || !(input.type in ComfyWidgets))
return
const inputType = input.type
widget = { name: input.name, [GET_CONFIG]: () => [inputType, {}] }
} else {
widget = input.widget
widget = input.widget as IWidgetLocatorWithConfig
}
// @ts-expect-error fixme ts strict error
const config = widget[GET_CONFIG]?.()
if (!config) return
@@ -208,8 +227,7 @@ export class PrimitiveNode extends LGraphNode {
widget[CONFIG] ?? config,
theirNode,
widget.name,
// @ts-expect-error fixme ts strict error
recreating
recreating ?? false
)
}
@@ -227,12 +245,12 @@ export class PrimitiveNode extends LGraphNode {
// Store current size as addWidget resizes the node
const [oldWidth, oldHeight] = this.size
let widget: IBaseWidget
let widget: IBaseWidget | undefined
if (isValidWidgetType(type)) {
widget = (ComfyWidgets[type](this, 'value', inputData, app) || {}).widget
} else {
// @ts-expect-error InputSpec is not typed correctly
widget = this.addWidget(type, 'value', null, () => {}, {})
// Unknown widget type - use 'custom' as fallback
widget = this.addWidget('custom', 'value', type, () => {}, {})
}
if (node?.widgets && widget) {
@@ -474,7 +492,7 @@ export function mergeIfValid(
output: INodeOutputSlot | INodeInputSlot,
config2: InputSpec,
forceUpdate?: boolean,
recreateWidget?: () => void,
recreateWidget?: () => IBaseWidget | undefined,
config1?: InputSpec
): { customConfig: InputSpec[1] } {
if (!config1) {
@@ -484,25 +502,20 @@ export function mergeIfValid(
const customSpec = mergeInputSpec(config1, config2)
if (customSpec || forceUpdate) {
if (customSpec) {
// @ts-expect-error fixme ts strict error
output.widget[CONFIG] = customSpec
if (customSpec && output.widget) {
const widgetLocator = output.widget as IWidgetLocatorWithConfig
widgetLocator[CONFIG] = customSpec
}
// @ts-expect-error fixme ts strict error
const widget = recreateWidget?.call(this)
// When deleting a node this can be null
if (widget) {
// @ts-expect-error fixme ts strict error
const min = widget.options.min
// @ts-expect-error fixme ts strict error
const max = widget.options.max
// @ts-expect-error fixme ts strict error
if (min != null && widget.value < min) widget.value = min
// @ts-expect-error fixme ts strict error
if (max != null && widget.value > max) widget.value = max
// @ts-expect-error fixme ts strict error
widget.callback(widget.value)
const widget = recreateWidget?.()
if (widget?.type === 'number') {
const numericWidget = widget as INumericWidget
const { min, max } = numericWidget.options
let currentValue = numericWidget.value ?? 0
if (min != null && currentValue < min) currentValue = min
if (max != null && currentValue > max) currentValue = max
numericWidget.value = currentValue
numericWidget.callback?.(currentValue)
}
}
@@ -512,7 +525,6 @@ export function mergeIfValid(
app.registerExtension({
name: 'Comfy.WidgetInputs',
async beforeRegisterNodeDef(nodeType, _nodeData, app) {
// @ts-expect-error adding extra property
nodeType.prototype.convertWidgetToInput = function (this: LGraphNode) {
console.warn(
'Please remove call to convertWidgetToInput. Widget to socket conversion is no longer necessary, as they co-exist now.'

View File

@@ -23,9 +23,7 @@ const isImageFile = (file: File) => file.type.startsWith('image/')
const isVideoFile = (file: File) => file.type.startsWith('video/')
const findFileComboWidget = (node: LGraphNode, inputName: string) =>
node.widgets!.find((w) => w.name === inputName) as IComboWidget & {
value: ExposedValue
}
node.widgets!.find((w) => w.name === inputName) as IComboWidget
export const useImageUploadWidget = () => {
const widgetConstructor: ComfyWidgetConstructor = (
@@ -80,10 +78,13 @@ export const useImageUploadWidget = () => {
output.forEach((path) => addToComboValues(fileComboWidget, path))
// Create a NEW array to ensure Vue reactivity detects the change
const newValue = allow_batch ? [...output] : output[0]
// @ts-expect-error litegraph combo value type does not support arrays yet
fileComboWidget.value = newValue
// Value property is redefined via Object.defineProperty to support batch uploads
const newValue: ExposedValue = allow_batch ? [...output] : output[0]
;(
fileComboWidget as unknown as Omit<IComboWidget, 'value'> & {
value: ExposedValue
}
).value = newValue
fileComboWidget.callback?.(newValue)
}
})
@@ -103,9 +104,9 @@ export const useImageUploadWidget = () => {
// Add our own callback to the combo widget to render an image when it changes
fileComboWidget.callback = function () {
nodeOutputStore.setNodeOutputs(node, fileComboWidget.value, {
isAnimated
})
// Image upload widget value is always a string path, never a number
const value = fileComboWidget.value as string | string[]
nodeOutputStore.setNodeOutputs(node, value, { isAnimated })
node.graph?.setDirtyCanvas(true)
}
@@ -113,9 +114,8 @@ export const useImageUploadWidget = () => {
// The value isn't set immediately so we need to wait a moment
// No change callbacks seem to be fired on initial setting of the value
requestAnimationFrame(() => {
nodeOutputStore.setNodeOutputs(node, fileComboWidget.value, {
isAnimated
})
const value = fileComboWidget.value as string | string[]
nodeOutputStore.setNodeOutputs(node, value, { isAnimated })
showPreview({ block: false })
})

View File

@@ -3,9 +3,28 @@ import { type LGraphNode, isComboWidget } from '@/lib/litegraph/src/litegraph'
import type {
IBaseWidget,
IComboWidget,
INumericWidget,
IStringWidget
} from '@/lib/litegraph/src/types/widgets'
import { useSettingStore } from '@/platform/settings/settingStore'
type ComboValuesType = IComboWidget['options']['values']
/**
* Normalizes combo widget values to an array.
* Handles the case where values may be a dictionary (Record<string, string>)
* or a legacy function type.
*/
function getComboValuesArray(
values: ComboValuesType | undefined,
widget?: IComboWidget,
node?: LGraphNode
): string[] {
if (!values) return []
if (typeof values === 'function') return values(widget, node)
if (Array.isArray(values)) return values
return Object.keys(values)
}
import { dynamicWidgets } from '@/core/graph/widgets/dynamicWidgets'
import { useBooleanWidget } from '@/renderer/extensions/vueNodes/widgets/composables/useBooleanWidget'
import { useChartWidget } from '@/renderer/extensions/vueNodes/widgets/composables/useChartWidget'
@@ -143,9 +162,11 @@ export function addValueControlWidgets(
const isCombo = isComboWidget(targetWidget)
let comboFilter: IStringWidget
if (isCombo && valueControl.options.values) {
// @ts-expect-error Combo widget values may be a dictionary or legacy function type
valueControl.options.values.push('increment-wrap')
if (isCombo) {
const controlValues = valueControl.options.values
if (Array.isArray(controlValues)) {
controlValues.push('increment-wrap')
}
}
if (isCombo && options.addFilterList !== false) {
comboFilter = node.addWidget(
@@ -165,13 +186,18 @@ export function addValueControlWidgets(
}
const applyWidgetControl = () => {
var v = valueControl.value
const v = valueControl.value
if (isCombo && v !== 'fixed') {
let values = targetWidget.options.values ?? []
const comboWidget = targetWidget as IComboWidget
let values = getComboValuesArray(
comboWidget.options.values,
comboWidget,
node
)
const filter = comboFilter?.value
if (filter) {
let check
let check: ((item: string) => boolean) | undefined
if (filter.startsWith('/') && filter.endsWith('/')) {
try {
const regex = new RegExp(filter.substring(1, filter.length - 1))
@@ -188,18 +214,23 @@ export function addValueControlWidgets(
const lower = filter.toLocaleLowerCase()
check = (item: string) => item.toLocaleLowerCase().includes(lower)
}
// @ts-expect-error Combo widget values may be a dictionary or legacy function type
values = values.filter((item: string) => check(item))
if (!values.length && targetWidget.options.values?.length) {
console.warn(
'Filter for node ' + node.id + ' has filtered out all items',
filter
values = values.filter(check)
if (!values.length && comboWidget.options.values) {
const originalValues = getComboValuesArray(
comboWidget.options.values,
comboWidget,
node
)
if (originalValues.length) {
console.warn(
'Filter for node ' + node.id + ' has filtered out all items',
filter
)
}
}
}
// @ts-expect-error targetWidget.value can be number or string
let current_index = values.indexOf(targetWidget.value)
let current_length = values.length
let current_index = values.indexOf(String(comboWidget.value))
const current_length = values.length
switch (v) {
case 'increment':
@@ -215,54 +246,45 @@ export function addValueControlWidgets(
current_index -= 1
break
case 'randomize':
// @ts-expect-error Combo widget values may be a dictionary or legacy function type
current_index = Math.floor(Math.random() * current_length)
break
default:
break
}
current_index = Math.max(0, current_index)
// @ts-expect-error Combo widget values may be a dictionary or legacy function type
current_index = Math.min(current_length - 1, current_index)
if (current_index >= 0) {
// @ts-expect-error Combo widget values may be a dictionary or legacy function type
let value = values[current_index]
targetWidget.value = value
targetWidget.callback?.(value)
const value = values[current_index]
comboWidget.value = value
comboWidget.callback?.(value)
}
} else {
//number
let { min = 0, max = 1, step2 = 1 } = targetWidget.options
// limit to something that javascript can handle
} else if (!isCombo) {
const numericWidget = targetWidget as INumericWidget
let currentValue = numericWidget.value ?? 0
let { min = 0, max = 1, step2 = 1 } = numericWidget.options
max = Math.min(1125899906842624, max)
min = Math.max(-1125899906842624, min)
let range = (max - min) / step2
const range = (max - min) / step2
//adjust values based on valueControl Behaviour
switch (v) {
case 'fixed':
break
case 'increment':
// @ts-expect-error targetWidget.value can be number or string
targetWidget.value += step2
currentValue += step2
break
case 'decrement':
// @ts-expect-error targetWidget.value can be number or string
targetWidget.value -= step2
currentValue -= step2
break
case 'randomize':
targetWidget.value = Math.floor(Math.random() * range) * step2 + min
currentValue = Math.floor(Math.random() * range) * step2 + min
break
default:
break
}
/*check if values are over or under their respective
* ranges and set them to min or max.*/
// @ts-expect-error targetWidget.value can be number or string
if (targetWidget.value < min) targetWidget.value = min
// @ts-expect-error targetWidget.value can be number or string
if (targetWidget.value > max) targetWidget.value = max
targetWidget.callback?.(targetWidget.value)
if (currentValue < min) currentValue = min
if (currentValue > max) currentValue = max
numericWidget.value = currentValue
numericWidget.callback?.(currentValue)
}
}

View File

@@ -113,6 +113,11 @@ declare module '@/lib/litegraph/src/litegraph' {
): ExecutableLGraphNode[]
/** @deprecated groupNode */
convertToNodes?(): LGraphNode[]
/**
* @deprecated Widget to socket conversion is no longer necessary as they co-exist now.
* This method is a no-op stub for backward compatibility with extensions.
*/
convertWidgetToInput?(): boolean
recreate?(): Promise<LGraphNode | null>
refreshComboInNode?(defs: Record<string, ComfyNodeDef>)
/** @deprecated groupNode */

View File

@@ -44,10 +44,9 @@ export function isAudioNode(node: LGraphNode | undefined): boolean {
export function addToComboValues(widget: IComboWidget, value: string) {
if (!widget.options) widget.options = { values: [] }
if (!widget.options.values) widget.options.values = []
// @ts-expect-error Combo widget values may be a dictionary or legacy function type
if (!widget.options.values.includes(value)) {
// @ts-expect-error Combo widget values may be a dictionary or legacy function type
widget.options.values.push(value)
const { values } = widget.options
if (Array.isArray(values) && !values.includes(value)) {
values.push(value)
}
}