import { app } from "../../../scripts/app.js"; import { $el } from "../../../scripts/ui.js"; let setting, guide_setting, guide_config; const id = "pysssss.SnapToGrid"; const guide_id = id + ".Guide"; const guide_config_default = { lines: { enabled: false, fillStyle: "rgba(255, 0, 0, 0.5)", }, block: { enabled: false, fillStyle: "rgba(0, 0, 255, 0.5)", }, } /** Wraps the provided function call to set/reset shiftDown when setting is enabled. */ function wrapCallInSettingCheck(fn) { if (setting?.value) { const shift = app.shiftDown; app.shiftDown = true; const r = fn(); app.shiftDown = shift; return r; } return fn(); } const ext = { name: id, init() { if (localStorage.getItem(guide_id) === null) { localStorage.setItem(guide_id, JSON.stringify(guide_config_default)); } guide_config = JSON.parse(localStorage.getItem(guide_id)); setting = app.ui.settings.addSetting({ id, name: "🐍 Always snap to grid", defaultValue: false, type: "boolean", onChange(value) { app.canvas.align_to_grid = value; }, }); guide_setting = app.ui.settings.addSetting({ id: id + ".Guide", name: "🐍 Display drag-and-drop guides", type: (name, setter, value) => { return $el("tr", [ $el("td", [ $el("label", { for: id.replaceAll(".", "-"), textContent: name, }), ]), $el("td", [ $el( "label", { textContent: "Lines: ", style: { display: "inline-block", }, }, [ $el("input", { id: id.replaceAll(".", "-") + "-line-text", type: "text", value: guide_config.lines.fillStyle, onchange: (event) => { guide_config.lines.fillStyle = event.target.value; localStorage.setItem(guide_id, JSON.stringify(guide_config)); } }), $el("input", { id: id.replaceAll(".", "-") + "-line-checkbox", type: "checkbox", checked: guide_config.lines.enabled, onchange: (event) => { guide_config.lines.enabled = !!event.target.checked; localStorage.setItem(guide_id, JSON.stringify(guide_config)); }, }), ] ), $el( "label", { textContent: "Block: ", style: { display: "inline-block", }, }, [ $el("input", { id: id.replaceAll(".", "-") + "-block-text", type: "text", value: guide_config.block.fillStyle, onchange: (event) => { guide_config.block.fillStyle = event.target.value; localStorage.setItem(guide_id, JSON.stringify(guide_config)); } }), $el("input", { id: id.replaceAll(".", "-") + '-block-checkbox', type: "checkbox", checked: guide_config.block.enabled, onchange: (event) => { guide_config.block.enabled = !!event.target.checked; localStorage.setItem(guide_id, JSON.stringify(guide_config)); }, }), ] ), ]), ]); } }); // We need to register our hooks after the core snap to grid extension runs // Do this from the graph configure function so we still get onNodeAdded calls const configure = LGraph.prototype.configure; LGraph.prototype.configure = function () { // Override drawNode to draw the drop position const drawNode = LGraphCanvas.prototype.drawNode; LGraphCanvas.prototype.drawNode = function () { wrapCallInSettingCheck(() => drawNode.apply(this, arguments)); }; // Override node added to add a resize handler to force grid alignment const onNodeAdded = app.graph.onNodeAdded; app.graph.onNodeAdded = function (node) { const r = onNodeAdded?.apply(this, arguments); const onResize = node.onResize; node.onResize = function () { wrapCallInSettingCheck(() => onResize?.apply(this, arguments)); }; return r; }; const groupMove = LGraphGroup.prototype.move; LGraphGroup.prototype.move = function(deltax, deltay, ignore_nodes) { wrapCallInSettingCheck(() => groupMove.apply(this, arguments)); } const canvasDrawGroups = LGraphCanvas.prototype.drawGroups; LGraphCanvas.prototype.drawGroups = function (canvas, ctx) { wrapCallInSettingCheck(() => canvasDrawGroups.apply(this, arguments)); } const canvasOnGroupAdd = LGraphCanvas.onGroupAdd; LGraphCanvas.onGroupAdd = function() { wrapCallInSettingCheck(() => canvasOnGroupAdd.apply(this, arguments)); } return configure.apply(this, arguments); }; // Override drag-and-drop behavior to show orthogonal guide lines around selected node(s) and preview of where the node(s) will be placed const origDrawNode = LGraphCanvas.prototype.drawNode LGraphCanvas.prototype.drawNode = function (node, ctx) { const enabled = guide_config.lines.enabled || guide_config.block.enabled; if (enabled && app.shiftDown && this.node_dragged && node.id in this.selected_nodes) { // discretize the canvas into grid let x = LiteGraph.CANVAS_GRID_SIZE * Math.round(node.pos[0] / LiteGraph.CANVAS_GRID_SIZE); let y = LiteGraph.CANVAS_GRID_SIZE * Math.round(node.pos[1] / LiteGraph.CANVAS_GRID_SIZE); // calculate the width and height of the node // (also need to shift the y position of the node, depending on whether the title is visible) x -= node.pos[0]; y -= node.pos[1]; let w, h; if (node.flags.collapsed) { w = node._collapsed_width; h = LiteGraph.NODE_TITLE_HEIGHT; y -= 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; y -= LiteGraph.NODE_TITLE_HEIGHT; } } // save the original fill style const f = ctx.fillStyle; // draw preview for drag-and-drop (rectangle to show where the node will be placed) if (guide_config.block.enabled) { ctx.fillStyle = guide_config.block.fillStyle; ctx.fillRect(x, y, w, h); } // add guide lines around node (arbitrarily long enough to span most workflows) if (guide_config.lines.enabled) { const xd = 10000; const yd = 10000; const thickness = 3; ctx.fillStyle = guide_config.lines.fillStyle; ctx.fillRect(x - xd, y, 2*xd, thickness); ctx.fillRect(x, y - yd, thickness, 2*yd); ctx.fillRect(x - xd, y + h, 2*xd, thickness); ctx.fillRect(x + w, y - yd, thickness, 2*yd); } // restore the original fill style ctx.fillStyle = f; } return origDrawNode.apply(this, arguments); }; }, }; app.registerExtension(ext);