widgets.js 10.3 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
27
28
29
30
31
32
33
34
export function addValueControlWidget(node, targetWidget, defaultValue = "randomize", values) {
    const valueControl = node.addWidget("combo", "control_after_generate", defaultValue, function (v) { }, {
        values: ["fixed", "increment", "decrement", "randomize"],
        serialize: false, // Don't include this in prompt.
    });
    valueControl.afterQueued = () => {

		var v = valueControl.value;

35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
		if (targetWidget.type == "combo" && v !== "fixed") {
			let current_index = targetWidget.options.values.indexOf(targetWidget.value);
			let current_length = targetWidget.options.values.length;

			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) {
				let value = targetWidget.options.values[current_index];
				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);
65

66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
			//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;
88
			targetWidget.callback(targetWidget.value);
89
		}
90
	}
91
	return valueControl;
92
};
93

94
95
function seedWidget(node, inputName, inputData, app) {
	const seed = ComfyWidgets.INT(node, inputName, inputData, app);
96
	const seedControl = addValueControlWidget(node, seed.widget, "randomize");
pythongosssss's avatar
pythongosssss committed
97

98
99
	seed.widget.linkedWidgets = [seedControl];
	return seed;
pythongosssss's avatar
pythongosssss committed
100
}
101
function addMultilineWidget(node, name, opts, app) {
102
103
104
105
106
107
108
109
	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
110
		},
111
112
		setValue(v) {
			inputEl.value = v;
pythongosssss's avatar
pythongosssss committed
113
114
		},
	});
115
	widget.inputEl = inputEl;
116

pythongosssss's avatar
pythongosssss committed
117
118
119
	return { minWidth: 400, minHeight: 200, widget };
}

120
121
122
123
124
function isSlider(display, app) {
	if (app.ui.settings.getSettingValue("Comfy.DisableSliders")) {
		return "number"
	}

comfyanonymous's avatar
comfyanonymous committed
125
	return (display==="slider") ? "slider" : "number"
Guillaume Faguet's avatar
Guillaume Faguet committed
126
127
}

pythongosssss's avatar
pythongosssss committed
128
129
130
export const ComfyWidgets = {
	"INT:seed": seedWidget,
	"INT:noise_seed": seedWidget,
131
132
	FLOAT(node, inputName, inputData, app) {
		let widgetType = isSlider(inputData[1]["display"], app);
comfyanonymous's avatar
comfyanonymous committed
133
134
135
136
		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);
137
138
		return { widget: node.addWidget(widgetType, inputName, val, 
			function (v) {
139
140
141
142
143
				if (config.round) {
					this.value = Math.round(v/config.round)*config.round;
				} else {
					this.value = v;
				}
144
			}, config) };
pythongosssss's avatar
pythongosssss committed
145
	},
146
147
	INT(node, inputName, inputData, app) {
		let widgetType = isSlider(inputData[1]["display"], app);
comfyanonymous's avatar
comfyanonymous committed
148
		const { val, config } = getNumberDefaults(inputData, 1, 0, true);
149
		Object.assign(config, { precision: 0 });
pythongosssss's avatar
pythongosssss committed
150
151
		return {
			widget: node.addWidget(
Guillaume Faguet's avatar
Guillaume Faguet committed
152
				widgetType,
153
154
155
156
157
158
159
				inputName,
				val,
				function (v) {
					const s = this.options.step / 10;
					this.value = Math.round(v / s) * s;
				},
				config
comfyanonymous's avatar
comfyanonymous committed
160
			),
161
162
		};
	},
comfyanonymous's avatar
comfyanonymous committed
163
	BOOLEAN(node, inputName, inputData) {
164
165
166
167
168
169
170
171
172
173
		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;
		}
174
175
176
177
178
179
		return {
			widget: node.addWidget(
				"toggle",
				inputName,
				defaultVal,
				() => {},
180
				options,
181
182
183
				)
		};
	},
pythongosssss's avatar
pythongosssss committed
184
185
186
187
	STRING(node, inputName, inputData, app) {
		const defaultVal = inputData[1].default || "";
		const multiline = !!inputData[1].multiline;

188
		let res;
pythongosssss's avatar
pythongosssss committed
189
		if (multiline) {
190
			res = addMultilineWidget(node, inputName, { defaultVal, ...inputData[1] }, app);
pythongosssss's avatar
pythongosssss committed
191
		} else {
192
			res = { widget: node.addWidget("text", inputName, defaultVal, () => {}, {}) };
pythongosssss's avatar
pythongosssss committed
193
		}
194
195
196
197
198

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

		return res;
pythongosssss's avatar
pythongosssss committed
199
	},
200
201
202
203
204
205
206
207
	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
208
209
210
211
212
213
214
215
216
217
	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
218
219
220
221
222
223
			let folder_separator = name.lastIndexOf("/");
			let subfolder = "";
			if (folder_separator > -1) {
				subfolder = name.substring(0, folder_separator);
				name = name.substring(folder_separator + 1);
			}
224
			img.src = api.apiURL(`/view?filename=${encodeURIComponent(name)}&type=input&subfolder=${subfolder}${app.getPreviewFormatParam()}`);
225
			node.setSizeForImage?.();
pythongosssss's avatar
pythongosssss committed
226
227
		}

comfyanonymous's avatar
comfyanonymous committed
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
		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
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
		// 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);
			}
		});

276
		async function uploadFile(file, updateNode, pasted = false) {
277
278
279
280
			try {
				// Wrap file in formdata so it includes filename
				const body = new FormData();
				body.append("image", file);
281
				if (pasted) body.append("subfolder", "pasted");
282
				const resp = await api.fetchApi("/upload/image", {
283
284
285
286
287
288
					method: "POST",
					body,
				});

				if (resp.status === 200) {
					const data = await resp.json();
289
290
291
292
293
294
					// 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);
295
296
297
					}

					if (updateNode) {
298
299
						showImage(path);
						imageWidget.value = path;
300
301
302
303
304
305
306
307
308
					}
				} else {
					alert(resp.status + " - " + resp.statusText);
				}
			} catch (error) {
				alert(error);
			}
		}

pythongosssss's avatar
pythongosssss committed
309
310
311
		const fileInput = document.createElement("input");
		Object.assign(fileInput, {
			type: "file",
312
			accept: "image/jpeg,image/png,image/webp",
pythongosssss's avatar
pythongosssss committed
313
314
315
			style: "display: none",
			onchange: async () => {
				if (fileInput.files.length) {
316
					await uploadFile(fileInput.files[0], true);
pythongosssss's avatar
pythongosssss committed
317
318
319
320
321
322
323
324
325
326
327
				}
			},
		});
		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;

328
329
330
		// Add handler to check if an image is being dragged over our node
		node.onDragOver = function (e) {
			if (e.dataTransfer && e.dataTransfer.items) {
331
				const image = [...e.dataTransfer.items].find((f) => f.kind === "file");
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
				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;
		};

352
353
354
355
356
357
358
359
360
361
		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
362
363
		return { widget: uploadWidget };
	},
pythongosssss's avatar
pythongosssss committed
364
};