Spaces:
Running
Running
import type { | |
LGraphCanvas as TLGraphCanvas, | |
LGraphGroup as TLGraphGroup, | |
LGraph as TLGraph, | |
AdjustedMouseEvent, | |
Vector2, | |
} from "typings/litegraph.js"; | |
import type {AdjustedMouseCustomEvent} from "typings/rgthree.js"; | |
import {app} from "scripts/app.js"; | |
import {rgthree} from "./rgthree.js"; | |
import {getOutputNodes} from "./utils.js"; | |
import {SERVICE as CONFIG_SERVICE} from "./services/config_service.js"; | |
const BTN_SIZE = 20; | |
const BTN_MARGIN: Vector2 = [6, 6]; | |
const BTN_SPACING = 8; | |
const BTN_GRID = BTN_SIZE / 8; | |
const TOGGLE_TO_MODE = new Map([ | |
["MUTE", LiteGraph.NEVER], | |
["BYPASS", 4], | |
]); | |
function getToggles() { | |
return [...CONFIG_SERVICE.getFeatureValue("group_header_fast_toggle.toggles", [])].reverse(); | |
} | |
/** | |
* Determines if the user clicked on an fast header icon. | |
*/ | |
function clickedOnToggleButton(e: AdjustedMouseEvent, group: TLGraphGroup): string | null { | |
const toggles = getToggles(); | |
const pos = group.pos; | |
const size = group.size; | |
for (let i = 0; i < toggles.length; i++) { | |
const toggle = toggles[i]; | |
if ( | |
LiteGraph.isInsideRectangle( | |
e.canvasX, | |
e.canvasY, | |
pos[0] + size[0] - (BTN_SIZE + BTN_MARGIN[0]) * (i + 1), | |
pos[1] + BTN_MARGIN[1], | |
BTN_SIZE, | |
BTN_SIZE, | |
) | |
) { | |
return toggle; | |
} | |
} | |
return null; | |
} | |
/** | |
* Registers the GroupHeaderToggles which places a mute and/or bypass icons in groups headers for | |
* quick, single-click ability to mute/bypass. | |
*/ | |
app.registerExtension({ | |
name: "rgthree.GroupHeaderToggles", | |
async setup() { | |
/** | |
* LiteGraph won't call `drawGroups` unless the canvas is dirty. Other nodes will do this, but | |
* in small workflows, we'll want to trigger it dirty so we can be drawn if we're in hover mode. | |
*/ | |
setInterval(() => { | |
if ( | |
CONFIG_SERVICE.getFeatureValue("group_header_fast_toggle.enabled") && | |
CONFIG_SERVICE.getFeatureValue("group_header_fast_toggle.show") !== "always" | |
) { | |
app.canvas.setDirty(true, true); | |
} | |
}, 250); | |
/** | |
* Handles a click on the icon area if the user has the extension enable from settings. | |
* Hooks into the already overriden mouse down processor from rgthree. | |
*/ | |
rgthree.addEventListener("on-process-mouse-down", ((e: AdjustedMouseCustomEvent) => { | |
if (!CONFIG_SERVICE.getFeatureValue("group_header_fast_toggle.enabled")) return; | |
const canvas = app.canvas as TLGraphCanvas; | |
if (canvas.selected_group) { | |
const originalEvent = e.detail.originalEvent; | |
const group = canvas.selected_group; | |
const clickedOnToggle = clickedOnToggleButton(originalEvent, group) || ""; | |
const toggleAction = clickedOnToggle?.toLocaleUpperCase(); | |
if (toggleAction) { | |
if (toggleAction === "QUEUE") { | |
const outputNodes = getOutputNodes(group._nodes); | |
if (!outputNodes?.length) { | |
rgthree.showMessage({ | |
id: "no-output-in-group", | |
type: "warn", | |
timeout: 4000, | |
message: "No output nodes for group!", | |
}); | |
} else { | |
rgthree.queueOutputNodes(outputNodes.map((n) => n.id)); | |
} | |
} else { | |
const toggleMode = TOGGLE_TO_MODE.get(toggleAction); | |
if (toggleMode) { | |
group.recomputeInsideNodes(); | |
const hasAnyActiveNodes = group._nodes.some((n) => n.mode === LiteGraph.ALWAYS); | |
const isAllMuted = | |
!hasAnyActiveNodes && group._nodes.every((n) => n.mode === LiteGraph.NEVER); | |
const isAllBypassed = | |
!hasAnyActiveNodes && !isAllMuted && group._nodes.every((n) => n.mode === 4); | |
let newMode: 0 | 1 | 2 | 3 | 4 = LiteGraph.ALWAYS; | |
if (toggleMode === LiteGraph.NEVER) { | |
newMode = isAllMuted ? LiteGraph.ALWAYS : LiteGraph.NEVER; | |
} else { | |
newMode = isAllBypassed ? LiteGraph.ALWAYS : 4; | |
} | |
for (const node of group._nodes) { | |
node.mode = newMode; | |
} | |
} | |
} | |
// Make it such that we're not then moving the group on drag. | |
canvas.selected_group = null; | |
canvas.dragging_canvas = false; | |
} | |
} | |
}) as EventListener); | |
/** | |
* Overrides LiteGraph's Canvas method for drawingGroups and, after calling the original, checks | |
* that the user has enabled fast toggles and draws them on the top-right of the app.. | |
*/ | |
const drawGroups = LGraphCanvas.prototype.drawGroups; | |
LGraphCanvas.prototype.drawGroups = function ( | |
canvasEl: HTMLCanvasElement, | |
ctx: CanvasRenderingContext2D, | |
) { | |
drawGroups.apply(this, [...arguments] as any); | |
if ( | |
!CONFIG_SERVICE.getFeatureValue("group_header_fast_toggle.enabled") || | |
!rgthree.lastAdjustedMouseEvent | |
) { | |
return; | |
} | |
const graph = app.graph as TLGraph; | |
let groups: TLGraphGroup[]; | |
// Default to hover if not always. | |
if (CONFIG_SERVICE.getFeatureValue("group_header_fast_toggle.show") !== "always") { | |
const hoverGroup = graph.getGroupOnPos( | |
rgthree.lastAdjustedMouseEvent.canvasX, | |
rgthree.lastAdjustedMouseEvent.canvasY, | |
); | |
groups = hoverGroup ? [hoverGroup] : []; | |
} else { | |
groups = graph._groups || []; | |
} | |
if (!groups.length) { | |
return; | |
} | |
const toggles = getToggles(); | |
ctx.save(); | |
for (const group of groups || []) { | |
let anyActive = false; | |
let allMuted = !!group._nodes.length; | |
let allBypassed = allMuted; | |
// Find the current state of the group's nodes. | |
for (const node of group._nodes) { | |
anyActive = anyActive || node.mode === LiteGraph.ALWAYS; | |
allMuted = allMuted && node.mode === LiteGraph.NEVER; | |
allBypassed = allBypassed && node.mode === 4; | |
if (anyActive || (!allMuted && !allBypassed)) { | |
break; | |
} | |
} | |
// Display each toggle. | |
for (let i = 0; i < toggles.length; i++) { | |
const toggle = toggles[i]; | |
const pos = group._pos; | |
const size = group._size; | |
ctx.fillStyle = ctx.strokeStyle = group.color || "#335"; | |
const x = pos[0] + size[0] - BTN_MARGIN[0] - BTN_SIZE - (BTN_SPACING + BTN_SIZE) * i; | |
const y = pos[1] + BTN_MARGIN[1]; | |
const midX = x + BTN_SIZE / 2; | |
const midY = y + BTN_SIZE / 2; | |
if (toggle === "queue") { | |
const outputNodes = getOutputNodes(group._nodes); | |
const oldGlobalAlpha = ctx.globalAlpha; | |
if (!outputNodes?.length) { | |
ctx.globalAlpha = 0.5; | |
} | |
ctx.lineJoin = "round"; | |
ctx.lineCap = "round"; | |
const arrowSizeX = BTN_SIZE * 0.6; | |
const arrowSizeY = BTN_SIZE * 0.7; | |
const arrow = new Path2D( | |
`M ${x + arrowSizeX / 2} ${midY} l 0 -${arrowSizeY / 2} l ${arrowSizeX} ${arrowSizeY / 2} l -${arrowSizeX} ${arrowSizeY / 2} z`, | |
); | |
ctx.stroke(arrow); | |
if (outputNodes?.length) { | |
ctx.fill(arrow); | |
} | |
ctx.globalAlpha = oldGlobalAlpha; | |
} else { | |
const on = toggle === "bypass" ? allBypassed : allMuted; | |
ctx.beginPath(); | |
ctx.lineJoin = "round"; | |
ctx.rect(x, y, BTN_SIZE, BTN_SIZE); | |
ctx.lineWidth = 2; | |
if (toggle === "mute") { | |
ctx.lineJoin = "round"; | |
ctx.lineCap = "round"; | |
if (on) { | |
ctx.stroke( | |
new Path2D(` | |
${eyeFrame(midX, midY)} | |
${eyeLashes(midX, midY)} | |
`), | |
); | |
} else { | |
const radius = BTN_GRID * 1.5; | |
// Eyeball fill | |
ctx.fill( | |
new Path2D(` | |
${eyeFrame(midX, midY)} | |
${eyeFrame(midX, midY, -1)} | |
${circlePath(midX, midY, radius)} | |
${circlePath(midX + BTN_GRID / 2, midY - BTN_GRID / 2, BTN_GRID * 0.375)} | |
`), | |
"evenodd", | |
); | |
// Eye Outline Stroke | |
ctx.stroke(new Path2D(`${eyeFrame(midX, midY)} ${eyeFrame(midX, midY, -1)}`)); | |
// Eye lashes (faded) | |
ctx.globalAlpha = this.editor_alpha * 0.5; | |
ctx.stroke(new Path2D(`${eyeLashes(midX, midY)} ${eyeLashes(midX, midY, -1)}`)); | |
ctx.globalAlpha = this.editor_alpha; | |
} | |
} else { | |
const lineChanges = on | |
? `a ${BTN_GRID * 3}, ${BTN_GRID * 3} 0 1, 1 ${BTN_GRID * 3 * 2},0 | |
l ${BTN_GRID * 2.0} 0` | |
: `l ${BTN_GRID * 8} 0`; | |
ctx.stroke( | |
new Path2D(` | |
M ${x} ${midY} | |
${lineChanges} | |
M ${x + BTN_SIZE} ${midY} l -2 2 | |
M ${x + BTN_SIZE} ${midY} l -2 -2 | |
`), | |
); | |
ctx.fill(new Path2D(`${circlePath(x + BTN_GRID * 3, midY, BTN_GRID * 1.8)}`)); | |
} | |
} | |
} | |
} | |
ctx.restore(); | |
}; | |
}, | |
}); | |
function eyeFrame(midX: number, midY: number, yFlip = 1) { | |
return ` | |
M ${midX - BTN_SIZE / 2} ${midY} | |
c ${BTN_GRID * 1.5} ${yFlip * BTN_GRID * 2.5}, ${BTN_GRID * (8 - 1.5)} ${ | |
yFlip * BTN_GRID * 2.5 | |
}, ${BTN_GRID * 8} 0 | |
`; | |
} | |
function eyeLashes(midX: number, midY: number, yFlip = 1) { | |
return ` | |
M ${midX - BTN_GRID * 3.46} ${midY + yFlip * BTN_GRID * 0.9} l -1.15 ${1.25 * yFlip} | |
M ${midX - BTN_GRID * 2.38} ${midY + yFlip * BTN_GRID * 1.6} l -0.90 ${1.5 * yFlip} | |
M ${midX - BTN_GRID * 1.15} ${midY + yFlip * BTN_GRID * 1.95} l -0.50 ${1.75 * yFlip} | |
M ${midX + BTN_GRID * 0.0} ${midY + yFlip * BTN_GRID * 2.0} l 0.00 ${2.0 * yFlip} | |
M ${midX + BTN_GRID * 1.15} ${midY + yFlip * BTN_GRID * 1.95} l 0.50 ${1.75 * yFlip} | |
M ${midX + BTN_GRID * 2.38} ${midY + yFlip * BTN_GRID * 1.6} l 0.90 ${1.5 * yFlip} | |
M ${midX + BTN_GRID * 3.46} ${midY + yFlip * BTN_GRID * 0.9} l 1.15 ${1.25 * yFlip} | |
`; | |
} | |
function circlePath(cx: number, cy: number, radius: number) { | |
return ` | |
M ${cx} ${cy} | |
m ${radius}, 0 | |
a ${radius},${radius} 0 1, 1 -${radius * 2},0 | |
a ${radius},${radius} 0 1, 1 ${radius * 2},0 | |
`; | |
} | |