widgets.js 12.9 KB
Newer Older
1
import { api } from "./api.js"
2
import "./domWidget.js";
3

comfyanonymous's avatar
comfyanonymous committed
4
function getNumberDefaults(inputData, defaultStep, precision, enable_rounding) {
pythongosssss's avatar
pythongosssss committed
5
	let defaultVal = inputData[1]["default"];
6
	let { min, max, step, round} = inputData[1];
pythongosssss's avatar
pythongosssss committed
7
8
9
10
11

	if (defaultVal == undefined) defaultVal = 0;
	if (min == undefined) min = 0;
	if (max == undefined) max = 2048;
	if (step == undefined) step = defaultStep;
12
13
	// precision is the number of decimal places to show.
	// by default, display the the smallest number of decimal places such that changes of size step are visible.
comfyanonymous's avatar
comfyanonymous committed
14
15
	if (precision == undefined) {
		precision = Math.max(-Math.floor(Math.log10(step)),0);
16
	}
17

comfyanonymous's avatar
comfyanonymous committed
18
	if (enable_rounding && (round == undefined || round === true)) {
19
20
21
22
		// by default, round the value to those decimal places shown.
		round = Math.round(1000000*Math.pow(0.1,precision))/1000000;
	}

23
	return { val: defaultVal, config: { min, max, step: 10.0 * step, round, precision } };
pythongosssss's avatar
pythongosssss committed
24
25
}

pythongosssss's avatar
pythongosssss committed
26
27
28
29
30
31
export function addValueControlWidget(node, targetWidget, defaultValue = "randomize", values, widgetName, inputData) {
	let name = inputData[1]?.control_after_generate;
	if(typeof name !== "string") {
		name = widgetName;
	}
	const widgets = addValueControlWidgets(node, targetWidget, defaultValue, {
pythongosssss's avatar
pythongosssss committed
32
		addFilterList: false,
pythongosssss's avatar
pythongosssss committed
33
34
		controlAfterGenerateName: name
	}, inputData);
pythongosssss's avatar
pythongosssss committed
35
36
37
	return widgets[0];
}

pythongosssss's avatar
pythongosssss committed
38
39
export function addValueControlWidgets(node, targetWidget, defaultValue = "randomize", options, inputData) {
	if (!defaultValue) defaultValue = "randomize";
pythongosssss's avatar
pythongosssss committed
40
	if (!options) options = {};
pythongosssss's avatar
pythongosssss committed
41
42
43
44
45
46
47
48
49
50
51
52
53

	const getName = (defaultName, optionName) => {
		let name = defaultName;
		if (options[optionName]) {
			name = options[optionName];
		} else if (typeof inputData?.[1]?.[defaultName] === "string") {
			name = inputData?.[1]?.[defaultName];
		} else if (inputData?.[1]?.control_prefix) {
			name = inputData?.[1]?.control_prefix + " " + name
		}
		return name;
	}

pythongosssss's avatar
pythongosssss committed
54
	const widgets = [];
pythongosssss's avatar
pythongosssss committed
55
56
57
58
59
60
61
62
63
64
	const valueControl = node.addWidget(
		"combo",
		getName("control_after_generate", "controlAfterGenerateName"),
		defaultValue,
		function () {},
		{
			values: ["fixed", "increment", "decrement", "randomize"],
			serialize: false, // Don't include this in prompt.
		}
	);
pythongosssss's avatar
pythongosssss committed
65
	widgets.push(valueControl);
66

pythongosssss's avatar
pythongosssss committed
67
68
69
	const isCombo = targetWidget.type === "combo";
	let comboFilter;
	if (isCombo && options.addFilterList !== false) {
pythongosssss's avatar
pythongosssss committed
70
71
72
73
74
75
76
77
78
		comboFilter = node.addWidget(
			"string",
			getName("control_filter_list", "controlFilterListName"),
			"",
			function () {},
			{
				serialize: false, // Don't include this in prompt.
			}
		);
pythongosssss's avatar
pythongosssss committed
79
80
81
82
		widgets.push(comboFilter);
	}

	valueControl.afterQueued = () => {
83
84
		var v = valueControl.value;

pythongosssss's avatar
pythongosssss committed
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
		if (isCombo && v !== "fixed") {
			let values = targetWidget.options.values;
			const filter = comboFilter?.value;
			if (filter) {
				let check;
				if (filter.startsWith("/") && filter.endsWith("/")) {
					try {
						const regex = new RegExp(filter.substring(1, filter.length - 1));
						check = (item) => regex.test(item);
					} catch (error) {
						console.error("Error constructing RegExp filter for node " + node.id, filter, error);
					}
				}
				if (!check) {
					const lower = filter.toLocaleLowerCase();
					check = (item) => item.toLocaleLowerCase().includes(lower);
				}
				values = values.filter(item => check(item));
				if (!values.length && targetWidget.options.values.length) {
					console.warn("Filter for node " + node.id + " has filtered out all items", filter);
				}
			}
			let current_index = values.indexOf(targetWidget.value);
			let current_length = values.length;
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124

			switch (v) {
				case "increment":
					current_index += 1;
					break;
				case "decrement":
					current_index -= 1;
					break;
				case "randomize":
					current_index = Math.floor(Math.random() * current_length);
				default:
					break;
			}
			current_index = Math.max(0, current_index);
			current_index = Math.min(current_length - 1, current_index);
			if (current_index >= 0) {
pythongosssss's avatar
pythongosssss committed
125
				let value = values[current_index];
126
127
128
				targetWidget.value = value;
				targetWidget.callback(value);
			}
pythongosssss's avatar
pythongosssss committed
129
130
		} else {
			//number
131
132
133
134
135
136
			let min = targetWidget.options.min;
			let max = targetWidget.options.max;
			// limit to something that javascript can handle
			max = Math.min(1125899906842624, max);
			min = Math.max(-1125899906842624, min);
			let range = (max - min) / (targetWidget.options.step / 10);
137

138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
			//adjust values based on valueControl Behaviour
			switch (v) {
				case "fixed":
					break;
				case "increment":
					targetWidget.value += targetWidget.options.step / 10;
					break;
				case "decrement":
					targetWidget.value -= targetWidget.options.step / 10;
					break;
				case "randomize":
					targetWidget.value = Math.floor(Math.random() * range) * (targetWidget.options.step / 10) + min;
				default:
					break;
			}
pythongosssss's avatar
pythongosssss committed
153
154
155
			/*check if values are over or under their respective
			 * ranges and set them to min or max.*/
			if (targetWidget.value < min) targetWidget.value = min;
156
157
158

			if (targetWidget.value > max)
				targetWidget.value = max;
159
			targetWidget.callback(targetWidget.value);
160
		}
pythongosssss's avatar
pythongosssss committed
161
	};
pythongosssss's avatar
pythongosssss committed
162
	return widgets;
163
};
164

pythongosssss's avatar
pythongosssss committed
165
166
167
function seedWidget(node, inputName, inputData, app, widgetName) {
	const seed = createIntWidget(node, inputName, inputData, app, true);
	const seedControl = addValueControlWidget(node, seed.widget, "randomize", undefined, widgetName, inputData);
pythongosssss's avatar
pythongosssss committed
168

169
170
	seed.widget.linkedWidgets = [seedControl];
	return seed;
pythongosssss's avatar
pythongosssss committed
171
}
pythongosssss's avatar
pythongosssss committed
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195

function createIntWidget(node, inputName, inputData, app, isSeedInput) {
	const control = inputData[1]?.control_after_generate;
	if (!isSeedInput && control) {
		return seedWidget(node, inputName, inputData, app, typeof control === "string" ? control : undefined);
	}

	let widgetType = isSlider(inputData[1]["display"], app);
	const { val, config } = getNumberDefaults(inputData, 1, 0, true);
	Object.assign(config, { precision: 0 });
	return {
		widget: node.addWidget(
			widgetType,
			inputName,
			val,
			function (v) {
				const s = this.options.step / 10;
				this.value = Math.round(v / s) * s;
			},
			config
		),
	};
}

196
function addMultilineWidget(node, name, opts, app) {
197
198
199
	const inputEl = document.createElement("textarea");
	inputEl.className = "comfy-multiline-input";
	inputEl.value = opts.defaultVal;
pythongosssss's avatar
pythongosssss committed
200
	inputEl.placeholder = opts.placeholder || name;
201
202
203
204

	const widget = node.addDOMWidget(name, "customtext", inputEl, {
		getValue() {
			return inputEl.value;
pythongosssss's avatar
pythongosssss committed
205
		},
206
207
		setValue(v) {
			inputEl.value = v;
pythongosssss's avatar
pythongosssss committed
208
209
		},
	});
210
	widget.inputEl = inputEl;
211

pythongosssss's avatar
pythongosssss committed
212
213
214
215
	inputEl.addEventListener("input", () => {
		widget.callback?.(widget.value);
	});

pythongosssss's avatar
pythongosssss committed
216
217
218
	return { minWidth: 400, minHeight: 200, widget };
}

219
220
221
222
223
function isSlider(display, app) {
	if (app.ui.settings.getSettingValue("Comfy.DisableSliders")) {
		return "number"
	}

comfyanonymous's avatar
comfyanonymous committed
224
	return (display==="slider") ? "slider" : "number"
Guillaume Faguet's avatar
Guillaume Faguet committed
225
226
}

pythongosssss's avatar
pythongosssss committed
227
228
229
export const ComfyWidgets = {
	"INT:seed": seedWidget,
	"INT:noise_seed": seedWidget,
230
231
	FLOAT(node, inputName, inputData, app) {
		let widgetType = isSlider(inputData[1]["display"], app);
comfyanonymous's avatar
comfyanonymous committed
232
233
234
235
		let precision = app.ui.settings.getSettingValue("Comfy.FloatRoundingPrecision");
		let disable_rounding = app.ui.settings.getSettingValue("Comfy.DisableFloatRounding")
		if (precision == 0) precision = undefined;
		const { val, config } = getNumberDefaults(inputData, 0.5, precision, !disable_rounding);
236
237
		return { widget: node.addWidget(widgetType, inputName, val, 
			function (v) {
238
239
240
241
242
				if (config.round) {
					this.value = Math.round(v/config.round)*config.round;
				} else {
					this.value = v;
				}
243
			}, config) };
pythongosssss's avatar
pythongosssss committed
244
	},
245
	INT(node, inputName, inputData, app) {
pythongosssss's avatar
pythongosssss committed
246
		return createIntWidget(node, inputName, inputData, app);
247
	},
comfyanonymous's avatar
comfyanonymous committed
248
	BOOLEAN(node, inputName, inputData) {
249
250
251
252
253
254
255
256
257
258
		let defaultVal = false;
		let options = {};
		if (inputData[1]) {
			if (inputData[1].default)
				defaultVal = inputData[1].default;
			if (inputData[1].label_on)
				options["on"] = inputData[1].label_on;
			if (inputData[1].label_off)
				options["off"] = inputData[1].label_off;
		}
259
260
261
262
263
264
		return {
			widget: node.addWidget(
				"toggle",
				inputName,
				defaultVal,
				() => {},
265
				options,
266
267
268
				)
		};
	},
pythongosssss's avatar
pythongosssss committed
269
270
271
272
	STRING(node, inputName, inputData, app) {
		const defaultVal = inputData[1].default || "";
		const multiline = !!inputData[1].multiline;

273
		let res;
pythongosssss's avatar
pythongosssss committed
274
		if (multiline) {
275
			res = addMultilineWidget(node, inputName, { defaultVal, ...inputData[1] }, app);
pythongosssss's avatar
pythongosssss committed
276
		} else {
277
			res = { widget: node.addWidget("text", inputName, defaultVal, () => {}, {}) };
pythongosssss's avatar
pythongosssss committed
278
		}
279
280
281
282
283

		if(inputData[1].dynamicPrompts != undefined)
			res.widget.dynamicPrompts = inputData[1].dynamicPrompts;

		return res;
pythongosssss's avatar
pythongosssss committed
284
	},
285
286
287
288
289
290
	COMBO(node, inputName, inputData) {
		const type = inputData[0];
		let defaultValue = type[0];
		if (inputData[1] && inputData[1].default) {
			defaultValue = inputData[1].default;
		}
pythongosssss's avatar
pythongosssss committed
291
292
293
294
295
		const res = { widget: node.addWidget("combo", inputName, defaultValue, () => {}, { values: type }) };
		if (inputData[1]?.control_after_generate) {
			res.widget.linkedWidgets = addValueControlWidgets(node, res.widget, undefined, undefined, inputData);
		}
		return res;
296
	},
pythongosssss's avatar
pythongosssss committed
297
	IMAGEUPLOAD(node, inputName, inputData, app) {
pythongosssss's avatar
pythongosssss committed
298
		const imageWidget = node.widgets.find((w) => w.name === (inputData[1]?.widget ?? "image"));
pythongosssss's avatar
pythongosssss committed
299
300
301
302
303
304
305
306
		let uploadWidget;

		function showImage(name) {
			const img = new Image();
			img.onload = () => {
				node.imgs = [img];
				app.graph.setDirtyCanvas(true);
			};
comfyanonymous's avatar
comfyanonymous committed
307
308
309
310
311
312
			let folder_separator = name.lastIndexOf("/");
			let subfolder = "";
			if (folder_separator > -1) {
				subfolder = name.substring(0, folder_separator);
				name = name.substring(folder_separator + 1);
			}
313
			img.src = api.apiURL(`/view?filename=${encodeURIComponent(name)}&type=input&subfolder=${subfolder}${app.getPreviewFormatParam()}`);
314
			node.setSizeForImage?.();
pythongosssss's avatar
pythongosssss committed
315
316
		}

comfyanonymous's avatar
comfyanonymous committed
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
		var default_value = imageWidget.value;
		Object.defineProperty(imageWidget, "value", {
			set : function(value) {
				this._real_value = value;
			},

			get : function() {
				let value = "";
				if (this._real_value) {
					value = this._real_value;
				} else {
					return default_value;
				}

				if (value.filename) {
					let real_value = value;
					value = "";
					if (real_value.subfolder) {
						value = real_value.subfolder + "/";
					}

					value += real_value.filename;

					if(real_value.type && real_value.type !== "input")
						value += ` [${real_value.type}]`;
				}
				return value;
			}
		});

pythongosssss's avatar
pythongosssss committed
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
		// Add our own callback to the combo widget to render an image when it changes
		const cb = node.callback;
		imageWidget.callback = function () {
			showImage(imageWidget.value);
			if (cb) {
				return cb.apply(this, arguments);
			}
		};

		// On load if we have a value then render the image
		// The value isnt set immediately so we need to wait a moment
		// No change callbacks seem to be fired on initial setting of the value
		requestAnimationFrame(() => {
			if (imageWidget.value) {
				showImage(imageWidget.value);
			}
		});

365
		async function uploadFile(file, updateNode, pasted = false) {
366
367
368
369
			try {
				// Wrap file in formdata so it includes filename
				const body = new FormData();
				body.append("image", file);
370
				if (pasted) body.append("subfolder", "pasted");
371
				const resp = await api.fetchApi("/upload/image", {
372
373
374
375
376
377
					method: "POST",
					body,
				});

				if (resp.status === 200) {
					const data = await resp.json();
378
379
380
381
382
383
					// Add the file to the dropdown list and update the widget value
					let path = data.name;
					if (data.subfolder) path = data.subfolder + "/" + path;

					if (!imageWidget.options.values.includes(path)) {
						imageWidget.options.values.push(path);
384
385
386
					}

					if (updateNode) {
387
388
						showImage(path);
						imageWidget.value = path;
389
390
391
392
393
394
395
396
397
					}
				} else {
					alert(resp.status + " - " + resp.statusText);
				}
			} catch (error) {
				alert(error);
			}
		}

pythongosssss's avatar
pythongosssss committed
398
399
400
		const fileInput = document.createElement("input");
		Object.assign(fileInput, {
			type: "file",
401
			accept: "image/jpeg,image/png,image/webp",
pythongosssss's avatar
pythongosssss committed
402
403
404
			style: "display: none",
			onchange: async () => {
				if (fileInput.files.length) {
405
					await uploadFile(fileInput.files[0], true);
pythongosssss's avatar
pythongosssss committed
406
407
408
409
410
411
				}
			},
		});
		document.body.append(fileInput);

		// Create the button widget for selecting the files
pythongosssss's avatar
pythongosssss committed
412
		uploadWidget = node.addWidget("button", inputName, "image", () => {
pythongosssss's avatar
pythongosssss committed
413
414
			fileInput.click();
		});
pythongosssss's avatar
pythongosssss committed
415
		uploadWidget.label = "choose file to upload";
pythongosssss's avatar
pythongosssss committed
416
417
		uploadWidget.serialize = false;

418
419
420
		// Add handler to check if an image is being dragged over our node
		node.onDragOver = function (e) {
			if (e.dataTransfer && e.dataTransfer.items) {
421
				const image = [...e.dataTransfer.items].find((f) => f.kind === "file");
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
				return !!image;
			}

			return false;
		};

		// On drop upload files
		node.onDragDrop = function (e) {
			console.log("onDragDrop called");
			let handled = false;
			for (const file of e.dataTransfer.files) {
				if (file.type.startsWith("image/")) {
					uploadFile(file, !handled); // Dont await these, any order is fine, only update on first one
					handled = true;
				}
			}

			return handled;
		};

442
443
444
445
446
447
448
449
450
451
		node.pasteFile = function(file) {
			if (file.type.startsWith("image/")) {
				const is_pasted = (file.name === "image.png") &&
								  (file.lastModified - Date.now() < 2000);
				uploadFile(file, true, is_pasted);
				return true;
			}
			return false;
		}

pythongosssss's avatar
pythongosssss committed
452
453
		return { widget: uploadWidget };
	},
pythongosssss's avatar
pythongosssss committed
454
};