widgets.js 11.6 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
}

26
export function addValueControlWidget(node, targetWidget, defaultValue = "randomize", values) {
pythongosssss's avatar
pythongosssss committed
27
28
29
30
31
32
33
34
35
36
37
	const widgets = addValueControlWidgets(node, targetWidget, defaultValue, values, {
		addFilterList: false,
	});
	return widgets[0];
}

export function addValueControlWidgets(node, targetWidget, defaultValue = "randomize", values, options) {
	if (!options) options = {};
	
	const widgets = [];
	const valueControl = node.addWidget("combo", "control_after_generate", defaultValue, function (v) { }, {
38
39
40
        values: ["fixed", "increment", "decrement", "randomize"],
        serialize: false, // Don't include this in prompt.
    });
pythongosssss's avatar
pythongosssss committed
41
	widgets.push(valueControl);
42

pythongosssss's avatar
pythongosssss committed
43
44
45
46
47
48
49
50
51
52
	const isCombo = targetWidget.type === "combo";
	let comboFilter;
	if (isCombo && options.addFilterList !== false) {
		comboFilter = node.addWidget("string", "control_filter_list", "", function (v) {}, {
			serialize: false, // Don't include this in prompt.
		});
		widgets.push(comboFilter);
	}

	valueControl.afterQueued = () => {
53
54
		var v = valueControl.value;

pythongosssss's avatar
pythongosssss committed
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
		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;
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94

			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
95
				let value = values[current_index];
96
97
98
99
100
101
102
103
104
105
				targetWidget.value = value;
				targetWidget.callback(value);
			}
		} else { //number
			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);
106

107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
			//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;
			}
		/*check if values are over or under their respective
		* ranges and set them to min or max.*/
			if (targetWidget.value < min)
				targetWidget.value = min;

			if (targetWidget.value > max)
				targetWidget.value = max;
129
			targetWidget.callback(targetWidget.value);
130
		}
131
	}
pythongosssss's avatar
pythongosssss committed
132
133
	
	return widgets;
134
};
135

136
137
function seedWidget(node, inputName, inputData, app) {
	const seed = ComfyWidgets.INT(node, inputName, inputData, app);
138
	const seedControl = addValueControlWidget(node, seed.widget, "randomize");
pythongosssss's avatar
pythongosssss committed
139

140
141
	seed.widget.linkedWidgets = [seedControl];
	return seed;
pythongosssss's avatar
pythongosssss committed
142
}
143
function addMultilineWidget(node, name, opts, app) {
144
145
146
147
148
149
150
151
	const inputEl = document.createElement("textarea");
	inputEl.className = "comfy-multiline-input";
	inputEl.value = opts.defaultVal;
	inputEl.placeholder = opts.placeholder || "";

	const widget = node.addDOMWidget(name, "customtext", inputEl, {
		getValue() {
			return inputEl.value;
pythongosssss's avatar
pythongosssss committed
152
		},
153
154
		setValue(v) {
			inputEl.value = v;
pythongosssss's avatar
pythongosssss committed
155
156
		},
	});
157
	widget.inputEl = inputEl;
158

pythongosssss's avatar
pythongosssss committed
159
160
161
	return { minWidth: 400, minHeight: 200, widget };
}

162
163
164
165
166
function isSlider(display, app) {
	if (app.ui.settings.getSettingValue("Comfy.DisableSliders")) {
		return "number"
	}

comfyanonymous's avatar
comfyanonymous committed
167
	return (display==="slider") ? "slider" : "number"
Guillaume Faguet's avatar
Guillaume Faguet committed
168
169
}

pythongosssss's avatar
pythongosssss committed
170
171
172
export const ComfyWidgets = {
	"INT:seed": seedWidget,
	"INT:noise_seed": seedWidget,
173
174
	FLOAT(node, inputName, inputData, app) {
		let widgetType = isSlider(inputData[1]["display"], app);
comfyanonymous's avatar
comfyanonymous committed
175
176
177
178
		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);
179
180
		return { widget: node.addWidget(widgetType, inputName, val, 
			function (v) {
181
182
183
184
185
				if (config.round) {
					this.value = Math.round(v/config.round)*config.round;
				} else {
					this.value = v;
				}
186
			}, config) };
pythongosssss's avatar
pythongosssss committed
187
	},
188
189
	INT(node, inputName, inputData, app) {
		let widgetType = isSlider(inputData[1]["display"], app);
comfyanonymous's avatar
comfyanonymous committed
190
		const { val, config } = getNumberDefaults(inputData, 1, 0, true);
191
		Object.assign(config, { precision: 0 });
pythongosssss's avatar
pythongosssss committed
192
193
		return {
			widget: node.addWidget(
Guillaume Faguet's avatar
Guillaume Faguet committed
194
				widgetType,
195
196
197
198
199
200
201
				inputName,
				val,
				function (v) {
					const s = this.options.step / 10;
					this.value = Math.round(v / s) * s;
				},
				config
comfyanonymous's avatar
comfyanonymous committed
202
			),
203
204
		};
	},
comfyanonymous's avatar
comfyanonymous committed
205
	BOOLEAN(node, inputName, inputData) {
206
207
208
209
210
211
212
213
214
215
		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;
		}
216
217
218
219
220
221
		return {
			widget: node.addWidget(
				"toggle",
				inputName,
				defaultVal,
				() => {},
222
				options,
223
224
225
				)
		};
	},
pythongosssss's avatar
pythongosssss committed
226
227
228
229
	STRING(node, inputName, inputData, app) {
		const defaultVal = inputData[1].default || "";
		const multiline = !!inputData[1].multiline;

230
		let res;
pythongosssss's avatar
pythongosssss committed
231
		if (multiline) {
232
			res = addMultilineWidget(node, inputName, { defaultVal, ...inputData[1] }, app);
pythongosssss's avatar
pythongosssss committed
233
		} else {
234
			res = { widget: node.addWidget("text", inputName, defaultVal, () => {}, {}) };
pythongosssss's avatar
pythongosssss committed
235
		}
236
237
238
239
240

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

		return res;
pythongosssss's avatar
pythongosssss committed
241
	},
242
243
244
245
246
247
248
249
	COMBO(node, inputName, inputData) {
		const type = inputData[0];
		let defaultValue = type[0];
		if (inputData[1] && inputData[1].default) {
			defaultValue = inputData[1].default;
		}
		return { widget: node.addWidget("combo", inputName, defaultValue, () => {}, { values: type }) };
	},
pythongosssss's avatar
pythongosssss committed
250
251
252
253
254
255
256
257
258
259
	IMAGEUPLOAD(node, inputName, inputData, app) {
		const imageWidget = node.widgets.find((w) => w.name === "image");
		let uploadWidget;

		function showImage(name) {
			const img = new Image();
			img.onload = () => {
				node.imgs = [img];
				app.graph.setDirtyCanvas(true);
			};
comfyanonymous's avatar
comfyanonymous committed
260
261
262
263
264
265
			let folder_separator = name.lastIndexOf("/");
			let subfolder = "";
			if (folder_separator > -1) {
				subfolder = name.substring(0, folder_separator);
				name = name.substring(folder_separator + 1);
			}
266
			img.src = api.apiURL(`/view?filename=${encodeURIComponent(name)}&type=input&subfolder=${subfolder}${app.getPreviewFormatParam()}`);
267
			node.setSizeForImage?.();
pythongosssss's avatar
pythongosssss committed
268
269
		}

comfyanonymous's avatar
comfyanonymous committed
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
		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
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
		// 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);
			}
		});

318
		async function uploadFile(file, updateNode, pasted = false) {
319
320
321
322
			try {
				// Wrap file in formdata so it includes filename
				const body = new FormData();
				body.append("image", file);
323
				if (pasted) body.append("subfolder", "pasted");
324
				const resp = await api.fetchApi("/upload/image", {
325
326
327
328
329
330
					method: "POST",
					body,
				});

				if (resp.status === 200) {
					const data = await resp.json();
331
332
333
334
335
336
					// 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);
337
338
339
					}

					if (updateNode) {
340
341
						showImage(path);
						imageWidget.value = path;
342
343
344
345
346
347
348
349
350
					}
				} else {
					alert(resp.status + " - " + resp.statusText);
				}
			} catch (error) {
				alert(error);
			}
		}

pythongosssss's avatar
pythongosssss committed
351
352
353
		const fileInput = document.createElement("input");
		Object.assign(fileInput, {
			type: "file",
354
			accept: "image/jpeg,image/png,image/webp",
pythongosssss's avatar
pythongosssss committed
355
356
357
			style: "display: none",
			onchange: async () => {
				if (fileInput.files.length) {
358
					await uploadFile(fileInput.files[0], true);
pythongosssss's avatar
pythongosssss committed
359
360
361
362
363
364
365
366
367
368
369
				}
			},
		});
		document.body.append(fileInput);

		// Create the button widget for selecting the files
		uploadWidget = node.addWidget("button", "choose file to upload", "image", () => {
			fileInput.click();
		});
		uploadWidget.serialize = false;

370
371
372
		// Add handler to check if an image is being dragged over our node
		node.onDragOver = function (e) {
			if (e.dataTransfer && e.dataTransfer.items) {
373
				const image = [...e.dataTransfer.items].find((f) => f.kind === "file");
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
				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;
		};

394
395
396
397
398
399
400
401
402
403
		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
404
405
		return { widget: uploadWidget };
	},
pythongosssss's avatar
pythongosssss committed
406
};