import { app } from "../../../../scripts/app.js"; import { resolveComboValue, resolveFormatConfig } from "./format_step.js"; function button_action(widget) { if ( widget.options?.reset == undefined && widget.options?.disable == undefined ) { return "None"; } if ( widget.options.reset != undefined && widget.value != widget.options.reset ) { return "Reset"; } if ( widget.options.disable != undefined && widget.value != widget.options.disable ) { return "Disable"; } if (widget.options.reset != undefined) { return "No Reset"; } return "No Disable"; } function fitText(ctx, text, maxLength) { if (maxLength <= 0) { return ["", 0]; } let fullLength = ctx.measureText(text).width; if (fullLength < maxLength) { return [text, fullLength]; } let cutoff = ((maxLength / fullLength) * text.length) | 0; let shortened = text.slice(0, Math.max(0, cutoff - 2)) + "…"; return [shortened, ctx.measureText(shortened).width]; } export function roundToPrecision(num, precision) { let strnum = Number(num).toFixed(precision); let deci = strnum.indexOf("."); if (deci > 0) { let i = strnum.length - 1; while (i > deci && strnum[i] == "0") { i--; } if (i == deci) { i--; } return strnum.slice(0, i + 1); } return strnum; } function inner_value_change(widget, value, node, pos) { // Ensure value is always treated as a number widget.value = Number(value); if (widget.options?.property && widget.options.property in node.properties) { node.setProperty(widget.options.property, widget.value); } if (widget.callback) { widget.callback(widget.value, app.canvas, node, pos); } } function drawAnnotated(ctx, node, widget_width, y, H) { const litegraph_base = LiteGraph; const show_text = app.canvas.ds.scale >= (app.canvas.low_quality_zoom_threshold ?? 0.5); const margin = 15; ctx.strokeStyle = litegraph_base.WIDGET_OUTLINE_COLOR; ctx.fillStyle = litegraph_base.WIDGET_BGCOLOR; ctx.beginPath(); if (show_text) { ctx.roundRect(margin, y, widget_width - margin * 2, H, [H * 0.5]); } else { ctx.rect(margin, y, widget_width - margin * 2, H); } ctx.fill(); if (show_text) { if (!this.disabled) { ctx.stroke(); } const button = button_action(this); if (button != "None") { ctx.save(); if (button.startsWith("No ")) { ctx.fillStyle = litegraph_base.WIDGET_OUTLINE_COLOR; ctx.strokeStyle = litegraph_base.WIDGET_OUTLINE_COLOR; } else { ctx.fillStyle = litegraph_base.WIDGET_TEXT_COLOR; ctx.strokeStyle = litegraph_base.WIDGET_TEXT_COLOR; } ctx.beginPath(); if (button.endsWith("Reset")) { ctx.arc( widget_width - margin - 26, y + H / 2, 4, (Math.PI * 3) / 2, Math.PI ); ctx.stroke(); ctx.beginPath(); ctx.moveTo(widget_width - margin - 26, y + H / 2 - 1.5); ctx.lineTo(widget_width - margin - 26, y + H / 2 - 6.5); ctx.lineTo(widget_width - margin - 30, y + H / 2 - 3.5); ctx.fill(); } else { ctx.arc( widget_width - margin - 26, y + H / 2, 4, (Math.PI * 2) / 3, (Math.PI * 8) / 3 ); ctx.moveTo( widget_width - margin - 26 - 8 ** 0.5, y + H / 2 + 8 ** 0.5 ); ctx.lineTo( widget_width - margin - 26 + 8 ** 0.5, y + H / 2 - 8 ** 0.5 ); ctx.stroke(); } ctx.restore(); } ctx.fillStyle = litegraph_base.WIDGET_TEXT_COLOR; if (!this.disabled) { ctx.beginPath(); ctx.moveTo(margin + 16, y + 5); ctx.lineTo(margin + 6, y + H * 0.5); ctx.lineTo(margin + 16, y + H - 5); ctx.fill(); ctx.beginPath(); ctx.moveTo(widget_width - margin - 16, y + 5); ctx.lineTo(widget_width - margin - 6, y + H * 0.5); ctx.lineTo(widget_width - margin - 16, y + H - 5); ctx.fill(); } let freeWidth = widget_width - (40 + margin * 2 + 20); let [valueText, valueWidth] = fitText( ctx, this.displayValue?.() ?? this.value ?? "", freeWidth ); freeWidth -= valueWidth; const value_offset = margin * 2 + 20; ctx.textAlign = "left"; ctx.fillStyle = litegraph_base.WIDGET_SECONDARY_TEXT_COLOR; if (freeWidth > 20) { let [name, nameWidth] = fitText( ctx, this.label || this.name, freeWidth ); freeWidth -= nameWidth; ctx.fillText(name, margin * 2 + 5, y + H * 0.7); } let value_offset_right = value_offset; ctx.textAlign = "right"; if (this.options.unit) { ctx.fillStyle = litegraph_base.WIDGET_OUTLINE_COLOR; let [unitText, unitWidth] = fitText( ctx, this.options.unit, freeWidth ); if (unitText == this.options.unit) { ctx.fillText( this.options.unit, widget_width - value_offset_right, y + H * 0.7 ); value_offset_right += unitWidth; freeWidth -= unitWidth; } } ctx.fillStyle = litegraph_base.WIDGET_TEXT_COLOR; ctx.fillText(valueText, widget_width - value_offset_right, y + H * 0.7); ctx.fillStyle = litegraph_base.WIDGET_SECONDARY_TEXT_COLOR; let annotation = ""; if (this.annotation) { annotation = this.annotation(this.value, freeWidth); } else if (this.options.annotation && this.value in this.options.annotation) { annotation = this.options.annotation[this.value]; } if (annotation) { ctx.fillStyle = litegraph_base.WIDGET_OUTLINE_COLOR; let [annoDisplay] = fitText(ctx, annotation, freeWidth); ctx.fillText( annoDisplay, widget_width - 5 - valueWidth - value_offset_right, y + H * 0.7 ); } } } function mouseAnnotated(event, [x, y], node) { const now = typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now(); const duplicateWindowMs = 20; const isDownEvent = event.type === "pointerdown" || event.type === "mousedown"; const isUpEvent = event.type === "pointerup" || event.type === "mouseup"; if (isDownEvent) { if ( typeof this._comfy1hew_last_down_at === "number" && now - this._comfy1hew_last_down_at < duplicateWindowMs ) { return true; } this._comfy1hew_last_down_at = now; } if (isUpEvent) { if ( typeof this._comfy1hew_last_up_at === "number" && now - this._comfy1hew_last_up_at < duplicateWindowMs ) { return true; } this._comfy1hew_last_up_at = now; } const widget_width = this.width || node.size[0]; const old_value = this.value; const margin = 15; let isButton = 0; if (x > margin + 6 && x < margin + 16) { isButton = -1; } else if (x > widget_width - margin - 16 && x < widget_width - margin - 6) { isButton = 1; } else if (x > widget_width - margin - 34 && x < widget_width - margin - 18) { isButton = 2; } let isAnnotation = false; if (this.annotation && !isButton && x > widget_width * 0.5) { isAnnotation = true; } const getStepConfig = () => { // Priority 0: Explicit custom config object (most reliable) if (this._comfy1hew_step_config) { return { step: Number(this._comfy1hew_step_config.step) || 1, mod: Number(this._comfy1hew_step_config.mod) || 0 }; } // Priority 1: Custom step resolver (injected by controller) if (typeof this._comfy1hew_getStep === "function") { const res = this._comfy1hew_getStep(); if (res && typeof res.step === "number") { return { step: res.step, mod: Number(res.mod) || 0 }; } } let step = 1; let mod = 0; // 2. Check widget.step (Standard LiteGraph property) // Allow string "4" to be parsed as number 4 if (this.step !== undefined) { const s = Number(this.step); if (!isNaN(s) && s > 0) { step = s; } } // 3. Fallback to options.step else if (this.options?.step !== undefined) { const s = Number(this.options.step); if (!isNaN(s) && s > 0) { step = s; } } // Check for mod in options if (this.options?.mod !== undefined) { mod = this.options.mod; } // Priority 4: Dynamic lookup from sibling "format" widget (Robust fallback) // This covers cases where custom properties are lost or not initialized, // matching the logic used in load_video.js's callback. if (step === 1) { const n = node || this._comfy1hew_owner_node; if (n && n.widgets) { const formatWidget = n.widgets.find(w => w.name === "format"); if (formatWidget) { const val = formatWidget.value; const fmtName = resolveComboValue(formatWidget, val); const fmtConfig = resolveFormatConfig(fmtName); if (fmtConfig && fmtConfig.step > 1) { step = fmtConfig.step; mod = fmtConfig.mod; } } } } return { step: Number(step), mod: Number(mod) }; }; var allow_scroll = true; const isDragging = typeof event.buttons === "number" ? event.buttons !== 0 : true; if (allow_scroll && event.type == "pointermove" && !isButton && isDragging) { if (event.deltaX && Math.abs(event.deltaX) > 0.5) { const { step, mod } = getStepConfig(); // Apply sensitivity scaling for dragging // If step is large (>1), we might want 1:1, but for step=1, 1:1 is too fast. // Let's use a standard 0.1 scale for finer control, similar to standard UI sliders. const dragScale = 0.1; let nextVal = Number(this.value) + event.deltaX * step * dragScale; // Apply step/mod alignment if step > 1 or mod > 0 if (step > 1 || mod > 0) { if (nextVal <= 0) { nextVal = 0; } else { nextVal = Math.round((nextVal - mod) / step) * step + mod; if (nextVal <= 0) { nextVal = 0; } } } this.value = nextVal; } if (this.options.min != null && this.value < this.options.min) { this.value = this.options.min; } if (this.options.max != null && this.value > this.options.max) { this.value = this.options.max; } // Final alignment check const { step, mod } = getStepConfig(); if (step > 1 || mod > 0) { let alignedVal = Number(this.value); if (alignedVal > 0) { if (alignedVal < mod) { alignedVal = 0; } else { alignedVal = Math.floor((alignedVal - mod) / step) * step + mod; if (alignedVal <= 0) { alignedVal = 0; } } } else if (alignedVal < 0) { alignedVal = 0; } if (this.options.min != null && alignedVal < this.options.min) { alignedVal = this.options.min; } if (this.options.max != null && alignedVal > this.options.max) { alignedVal = this.options.max; } this.value = alignedVal; } } if (event.type == "pointerdown" || event.type == "mousedown") { this._mouse_down_on_button = isButton == 2; if (isButton == 1 || isButton == -1) { this._comfy1hew_arrow_incremented = false; const { step, mod } = getStepConfig(); const val = Number(this.value); let nextVal = val; const s = Number(step) || 1; const m = Number(mod) || 0; if (isButton == 1) { if (s > 1 || m > 0) { // Move to the next legal value in the sequence: step*k + mod if (val < m) { nextVal = m; } else { const k = Math.floor((val - m) / s) + 1; nextVal = s * k + m; } } else { nextVal = val + s; } } else { if (s > 1 || m > 0) { // Move to the previous legal value in the sequence. if (val <= m) { nextVal = 0; } else { const k = Math.ceil((val - m) / s) - 1; nextVal = s * k + m; } } else { nextVal = val - s; } } // Apply limits if (this.options.min != null && nextVal < this.options.min) { nextVal = this.options.min; } if (this.options.max != null && nextVal > this.options.max) { nextVal = this.options.max; } // Alignment check after increment/decrement if (step > 1 || mod > 0) { if (nextVal > 0) { if (nextVal < mod) { nextVal = 0; } else { nextVal = Math.floor((nextVal - mod) / step) * step + mod; if (nextVal <= 0) { nextVal = 0; } } } } this.value = nextVal; this._comfy1hew_arrow_incremented = true; } else if (isAnnotation) { const anno = this.annotation(this.value, 100); if (anno && anno.endsWith("\u21FD")) { const numStr = anno.slice(0, -1); const val = parseFloat(numStr); if (!isNaN(val)) { this.value = val; } } } } else if (event.type == "pointerup" || event.type == "mouseup") { let buttonClicked = false; if (isButton == 2 && (this._mouse_down_on_button || event.click_time < 300)) { const buttonType = button_action(this); if (buttonType == "Reset") { this.value = this.options.reset; buttonClicked = true; } else if (buttonType == "Disable") { this.value = this.options.disable; buttonClicked = true; } } if ( !buttonClicked && event.click_time < 200 && !isButton && !isAnnotation && !this._mouse_down_on_button ) { const d_callback = (v) => { this.value = this.parseValue?.(v) ?? Number(v); inner_value_change(this, this.value, node, [x, y]); }; app.canvas.prompt("Value", this.value, d_callback, event); } } this._mouse_down_on_button = false; this._comfy1hew_arrow_incremented = false; if (old_value != this.value) { setTimeout( function () { inner_value_change(this, this.value, node, [x, y]); }.bind(this), 20 ); } return true; } export function installWidgetSourceOverlay( node, widget, getSourceValue, behavior = "reset" ) { if (!node || !widget || widget._comfy1hewSourceOverlayInstalled === "1") { return; } widget._comfy1hewSourceOverlayInstalled = "1"; widget._comfy1hew_owner_node = node; widget.draw = drawAnnotated; widget.mouse = mouseAnnotated; if (behavior === "disable") { widget.options.disable = 0; delete widget.options.reset; } else { if (widget.options.reset === undefined) { widget.options.reset = widget.options.default ?? widget.value ?? 0; } delete widget.options.disable; } widget.annotation = (value, width) => { const sourceValue = getSourceValue(); if (sourceValue !== null && sourceValue !== undefined) { return roundToPrecision(sourceValue, 2) + "\u21FD"; } return ""; }; }