diff --git a/web/extensions/core/snapToGrid.js b/web/extensions/core/snapToGrid.js new file mode 100644 index 0000000..20b245e --- /dev/null +++ b/web/extensions/core/snapToGrid.js @@ -0,0 +1,89 @@ +import { app } from "/scripts/app.js"; + +// Shift + drag/resize to snap to grid + +app.registerExtension({ + name: "Comfy.SnapToGrid", + init() { + // Add setting to control grid size + app.ui.settings.addSetting({ + id: "Comfy.SnapToGrid.GridSize", + name: "Grid Size", + type: "number", + attrs: { + min: 1, + max: 500, + }, + tooltip: + "When dragging and resizing nodes while holding shift they will be aligned to the grid, this controls the size of that grid.", + defaultValue: LiteGraph.CANVAS_GRID_SIZE, + onChange(value) { + LiteGraph.CANVAS_GRID_SIZE = +value; + }, + }); + + // After moving a node, if the shift key is down align it to grid + const onNodeMoved = app.canvas.onNodeMoved; + app.canvas.onNodeMoved = function (node) { + const r = onNodeMoved?.apply(this, arguments); + + if (app.shiftDown) { + // Ensure all selected nodes are realigned + for (const id in this.selected_nodes) { + this.selected_nodes[id].alignToGrid(); + } + } + + return r; + }; + + // When a node is added, add a resize handler to it so we can fix align the size with the grid + const onNodeAdded = app.graph.onNodeAdded; + app.graph.onNodeAdded = function (node) { + const onResize = node.onResize; + node.onResize = function () { + if (app.shiftDown) { + const w = LiteGraph.CANVAS_GRID_SIZE * Math.round(node.size[0] / LiteGraph.CANVAS_GRID_SIZE); + const h = LiteGraph.CANVAS_GRID_SIZE * Math.round(node.size[1] / LiteGraph.CANVAS_GRID_SIZE); + node.size[0] = w; + node.size[1] = h; + } + return onResize?.apply(this, arguments); + }; + return onNodeAdded?.apply(this, arguments); + }; + + // Draw a preview of where the node will go if holding shift and the node is selected + const origDrawNode = LGraphCanvas.prototype.drawNode; + LGraphCanvas.prototype.drawNode = function (node, ctx) { + if (app.shiftDown && this.node_dragged && node.id in this.selected_nodes) { + const x = LiteGraph.CANVAS_GRID_SIZE * Math.round(node.pos[0] / LiteGraph.CANVAS_GRID_SIZE); + const y = LiteGraph.CANVAS_GRID_SIZE * Math.round(node.pos[1] / LiteGraph.CANVAS_GRID_SIZE); + + const shiftX = x - node.pos[0]; + let shiftY = y - node.pos[1]; + + let w, h; + if (node.flags.collapsed) { + w = node._collapsed_width; + h = LiteGraph.NODE_TITLE_HEIGHT; + shiftY -= LiteGraph.NODE_TITLE_HEIGHT; + } else { + w = node.size[0]; + h = node.size[1]; + let titleMode = node.constructor.title_mode; + if (titleMode !== LiteGraph.TRANSPARENT_TITLE && titleMode !== LiteGraph.NO_TITLE) { + h += LiteGraph.NODE_TITLE_HEIGHT; + shiftY -= LiteGraph.NODE_TITLE_HEIGHT; + } + } + const f = ctx.fillStyle; + ctx.fillStyle = "rgba(100, 100, 100, 0.5)"; + ctx.fillRect(shiftX, shiftY, w, h); + ctx.fillStyle = f; + } + + return origDrawNode.apply(this, arguments); + }; + }, +}); diff --git a/web/scripts/app.js b/web/scripts/app.js index c216d26..5aef31c 100644 --- a/web/scripts/app.js +++ b/web/scripts/app.js @@ -18,6 +18,7 @@ class ComfyApp { this.ui = new ComfyUI(this); this.extensions = []; this.nodeOutputs = {}; + this.shiftDown = false; } /** @@ -637,11 +638,16 @@ class ComfyApp { #addKeyboardHandler() { window.addEventListener("keydown", (e) => { + this.shiftDown = e.shiftKey; + // Queue prompt using ctrl or command + enter if ((e.ctrlKey || e.metaKey) && (e.key === "Enter" || e.keyCode === 13 || e.keyCode === 10)) { this.queuePrompt(e.shiftKey ? -1 : 0); } }); + window.addEventListener("keyup", (e) => { + this.shiftDown = e.shiftKey; + }); } /** diff --git a/web/scripts/ui.js b/web/scripts/ui.js index c27fbf9..679f10b 100644 --- a/web/scripts/ui.js +++ b/web/scripts/ui.js @@ -198,7 +198,7 @@ class ComfySettingsDialog extends ComfyDialog { localStorage[settingId] = JSON.stringify(value); } - addSetting({ id, name, type, defaultValue, onChange }) { + addSetting({ id, name, type, defaultValue, onChange, attrs = {}, tooltip = "", }) { if (!id) { throw new Error("Settings must have an ID"); } @@ -225,42 +225,72 @@ class ComfySettingsDialog extends ComfyDialog { value = v; }; + let element; + if (typeof type === "function") { - return type(name, setter, value); + element = type(name, setter, value, attrs); + } else { + switch (type) { + case "boolean": + element = $el("div", [ + $el("label", { textContent: name || id }, [ + $el("input", { + type: "checkbox", + checked: !!value, + oninput: (e) => { + setter(e.target.checked); + }, + ...attrs + }), + ]), + ]); + break; + case "number": + element = $el("div", [ + $el("label", { textContent: name || id }, [ + $el("input", { + type, + value, + oninput: (e) => { + setter(e.target.value); + }, + ...attrs + }), + ]), + ]); + break; + default: + console.warn("Unsupported setting type, defaulting to text"); + element = $el("div", [ + $el("label", { textContent: name || id }, [ + $el("input", { + value, + oninput: (e) => { + setter(e.target.value); + }, + ...attrs + }), + ]), + ]); + break; + } } - - switch (type) { - case "boolean": - return $el("div", [ - $el("label", { textContent: name || id }, [ - $el("input", { - type: "checkbox", - checked: !!value, - oninput: (e) => { - setter(e.target.checked); - }, - }), - ]), - ]); - default: - console.warn("Unsupported setting type, defaulting to text"); - return $el("div", [ - $el("label", { textContent: name || id }, [ - $el("input", { - value, - oninput: (e) => { - setter(e.target.value); - }, - }), - ]), - ]); + if(tooltip) { + element.title = tooltip; } + + return element; }, }); } show() { super.show(); + Object.assign(this.textElement.style, { + display: "flex", + flexDirection: "column", + gap: "10px" + }); this.textElement.replaceChildren(...this.settings.map((s) => s.render())); } }