From 9743db3801aafcdfe45a7386bdbef0c46d528aba Mon Sep 17 00:00:00 2001 From: ussoewwin <136552381+ussoewwin@users.noreply.github.com> Date: Tue, 13 Jan 2026 09:31:07 +0900 Subject: [PATCH] Add missing zimageturbo_lora_dynamic_v4.js file (Issue #47) --- js/zimageturbo_lora_dynamic_v4.js | 204 ++++++++++++++++++++++++++++++ 1 file changed, 204 insertions(+) create mode 100644 js/zimageturbo_lora_dynamic_v4.js diff --git a/js/zimageturbo_lora_dynamic_v4.js b/js/zimageturbo_lora_dynamic_v4.js new file mode 100644 index 0000000..4afff31 --- /dev/null +++ b/js/zimageturbo_lora_dynamic_v4.js @@ -0,0 +1,204 @@ +import { app } from "../../scripts/app.js"; + +console.log("★★★ zimageturbo_lora_dynamic_v4.js: Z-Image-Turbo LoRA Stack V4 ★★★"); + +const HIDDEN_TAG = "tschide"; + +app.registerExtension({ + name: "nunchaku.zimageturbo_lora_dynamic_v4", + + async beforeRegisterNodeDef(nodeType, nodeData, app) { + if (nodeData.name === "NunchakuZImageTurboLoraStackV4") { + nodeType["@visibleLoraCount"] = { type: "number", default: 1, min: 1, max: 10, step: 1 }; + } + }, + + nodeCreated(node) { + if (node.comfyClass !== "NunchakuZImageTurboLoraStackV4") return; + + if (!node.properties) node.properties = {}; + if (node.properties["visibleLoraCount"] === undefined) node.properties["visibleLoraCount"] = 1; + + // Immediately hide lora_count widget if it exists + const initialLoraCountWidget = node.widgets?.find(w => w.name === "lora_count"); + if (initialLoraCountWidget) { + if (!initialLoraCountWidget.origType) { + initialLoraCountWidget.origType = initialLoraCountWidget.type; + initialLoraCountWidget.origComputeSize = initialLoraCountWidget.computeSize; + } + initialLoraCountWidget.type = HIDDEN_TAG; + initialLoraCountWidget.computeSize = () => [0, -4]; + } + + node.cachedWidgets = {}; + let cacheReady = false; + + const initCache = () => { + if (cacheReady) return; + const all = [...node.widgets]; + + // Cache lora_count widget (required for Python backend, but hidden in UI) + const loraCountWidget = all.find(w => w.name === "lora_count"); + if (loraCountWidget) { + node.cachedLoraCount = loraCountWidget; + // Store original properties for restoration if needed + if (!loraCountWidget.origType) { + loraCountWidget.origType = loraCountWidget.type; + loraCountWidget.origComputeSize = loraCountWidget.computeSize; + } + // Hide V4's lora_count widget using HIDDEN_TAG and computeSize + loraCountWidget.type = HIDDEN_TAG; + loraCountWidget.computeSize = () => [0, -4]; + } + + // Cache toggle_all widget + const toggleAllWidget = all.find(w => w.name === "toggle_all"); + if (toggleAllWidget) { + node.cachedToggleAll = toggleAllWidget; + } + + for (let i = 1; i <= 10; i++) { + const wEnabled = all.find(w => w.name === `enabled_${i}`); + const wName = all.find(w => w.name === `lora_name_${i}`); + const wStrength = all.find(w => w.name === `lora_strength_${i}`); + if (wEnabled && wName && wStrength) { + node.cachedWidgets[i] = [wEnabled, wName, wStrength]; + wEnabled.type = "toggle"; + wName.type = "combo"; + wStrength.type = "number"; + if (wEnabled.computeSize) delete wEnabled.computeSize; + if (wName.computeSize) delete wName.computeSize; + if (wStrength.computeSize) delete wStrength.computeSize; + } + } + cacheReady = true; + }; + + const ensureControlWidget = () => { + const name = "🔢 LoRA Count"; + + // Remove old button widgets + for (let i = node.widgets.length - 1; i >= 0; i--) { + const w = node.widgets[i]; + if (w.name === "🔢 Set LoRA Count" || w.type === "button") { + node.widgets.splice(i, 1); + } + } + + let w = node.widgets.find(x => x.name === name); + if (!w) { + const values = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"]; + w = node.addWidget("combo", name, "1", (v) => { + const num = parseInt(v); + if (!isNaN(num)) { + node.properties["visibleLoraCount"] = num; + if (node.cachedLoraCount) { + node.cachedLoraCount.value = num; + } + node.updateLoraSlots(); + } + }, { values }); + } + w.value = node.properties["visibleLoraCount"].toString(); + if (node.cachedLoraCount) { + node.cachedLoraCount.value = node.properties["visibleLoraCount"]; + } + return w; + }; + + const ensureToggleAllWidget = () => { + if (!node.cachedToggleAll) return null; + if (!node.cachedToggleAll.origCallback) { + node.cachedToggleAll.origCallback = node.cachedToggleAll.callback; + } + node.cachedToggleAll.callback = (value) => { + if (node.cachedToggleAll.origCallback) { + node.cachedToggleAll.origCallback(value); + } + const count = parseInt(node.properties["visibleLoraCount"] || 1); + for (let i = 1; i <= count; i++) { + const pair = node.cachedWidgets[i]; + if (pair && pair[0]) { + pair[0].value = value; + } + } + }; + return node.cachedToggleAll; + }; + + // Plain logic restored + node.updateLoraSlots = function () { + if (!cacheReady) initCache(); + + const count = parseInt(this.properties["visibleLoraCount"] || 1); + const controlWidget = ensureControlWidget(); + + this.widgets = [controlWidget]; + + if (node.cachedLoraCount) { + node.cachedLoraCount.type = HIDDEN_TAG; + node.cachedLoraCount.computeSize = () => [0, -4]; + node.cachedLoraCount.value = count; + this.widgets.push(node.cachedLoraCount); + } + + const toggleAllWidget = ensureToggleAllWidget(); + if (toggleAllWidget) { + this.widgets.push(toggleAllWidget); + } + + for (let i = 1; i <= count; i++) { + const pair = this.cachedWidgets[i]; + if (pair && pair.length >= 3) { + const wEnabled = pair[0]; + const wName = pair[1]; + const wStrength = pair[2]; + + // Restore standard widgets + wEnabled.type = "toggle"; + wName.type = "combo"; + wStrength.type = "number"; + + // Clear custom compute sizes if any + if (wEnabled.computeSize) delete wEnabled.computeSize; + if (wName.computeSize) delete wName.computeSize; + if (wStrength.computeSize) delete wStrength.computeSize; + + this.widgets.push(wEnabled); + this.widgets.push(wName); + this.widgets.push(wStrength); + } + } + + const HEADER_H = 60; + // 3 lines per slot (Toggle ~20 + Name ~26 + Strength ~26 + margins) + const SLOT_H = 80; + const TOGGLE_ALL_H = toggleAllWidget ? 30 : 0; + const PADDING = 20; + const targetH = HEADER_H + TOGGLE_ALL_H + (count * SLOT_H) + PADDING; + + this.setSize([this.size[0], targetH]); + + if (app.canvas) app.canvas.setDirty(true, true); + }; + + node.onPropertyChanged = function (property, value) { + if (property === "visibleLoraCount") { + const w = this.widgets.find(x => x.name === "🔢 LoRA Count"); + if (w) w.value = value.toString(); + this.updateLoraSlots(); + } + }; + + const origOnConfigure = node.onConfigure; + node.onConfigure = function () { + if (origOnConfigure) origOnConfigure.apply(this, arguments); + setTimeout(() => node.updateLoraSlots(), 100); + }; + + setTimeout(() => { + initCache(); + node.updateLoraSlots(); + }, 100); + } +});