diff --git a/__init__.py b/__init__.py index 19325ab..0e1a982 100644 --- a/__init__.py +++ b/__init__.py @@ -1,42 +1,7 @@ -import os, json -import comfy.utils, comfy.sd, folder_paths +from .multi_lora_loader import MultiLoraLoader from .prompt_segments import PromptSegments from .resolution_switcher import ResolutionSwitcher -class MultiLoraLoader: - @classmethod - def INPUT_TYPES(s): - return { - "required": { - "model": ("MODEL",), - "lora_stack": ("STRING", {"default": "[]", "multiline": False}), - } - } - - RETURN_TYPES = ("MODEL",) - FUNCTION = "apply" - CATEGORY = "loaders" - - def apply(self, model, lora_stack="[]"): - try: - stack = json.loads(lora_stack) - except Exception: - return (model,) - - for entry in stack: - if not entry.get("enabled", False): - continue - lora_name = entry.get("lora", "None") - strength = float(entry.get("strength", 1.0)) - if lora_name == "None" or strength == 0: - continue - path = folder_paths.get_full_path("loras", lora_name) - if path and os.path.exists(path): - lora = comfy.utils.load_torch_file(path) - model, _ = comfy.sd.load_lora_for_models(model, None, lora, strength, 0) - - return (model,) - NODE_CLASS_MAPPINGS = { "MultiLoraLoader": MultiLoraLoader, "PromptSegments": PromptSegments, diff --git a/multi_lora_loader.js b/multi_lora_loader.js index c251c70..d72f1d2 100644 --- a/multi_lora_loader.js +++ b/multi_lora_loader.js @@ -1,7 +1,7 @@ import { app } from "../../scripts/app.js"; // 语言配置:将下面一行的 "en" 改成 "zh" 即可切换中文 -const LANG = "en"; +const LANG = "zh"; const I18N = { en: { on_off: "On/Off", lora: "LoRA", strength: "Strength", note: "Note", del: "Del", diff --git a/multi_lora_loader.py b/multi_lora_loader.py new file mode 100644 index 0000000..8a27790 --- /dev/null +++ b/multi_lora_loader.py @@ -0,0 +1,36 @@ +import os, json +import comfy.utils, comfy.sd, folder_paths + +class MultiLoraLoader: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "model": ("MODEL",), + "lora_stack": ("STRING", {"default": "[]", "multiline": False}), + } + } + + RETURN_TYPES = ("MODEL",) + FUNCTION = "apply" + CATEGORY = "loaders" + + def apply(self, model, lora_stack="[]"): + try: + stack = json.loads(lora_stack) + except Exception: + return (model,) + + for entry in stack: + if not entry.get("enabled", False): + continue + lora_name = entry.get("lora", "None") + strength = float(entry.get("strength", 1.0)) + if lora_name == "None" or strength == 0: + continue + path = folder_paths.get_full_path("loras", lora_name) + if path and os.path.exists(path): + lora = comfy.utils.load_torch_file(path) + model, _ = comfy.sd.load_lora_for_models(model, None, lora, strength, 0) + + return (model,) \ No newline at end of file diff --git a/prompt_segments.js b/prompt_segments.js index f9deaab..62a2f1f 100644 --- a/prompt_segments.js +++ b/prompt_segments.js @@ -1,7 +1,7 @@ import { app } from "../../scripts/app.js"; // 语言配置:将下面一行的 "en" 改成 "zh" 即可切换中文 -const LANG = "en"; +const LANG = "zh"; const I18N = { en: { label_ph: "Label...", add: "+ Add Segment", default_label: "Quality Tags", @@ -34,20 +34,20 @@ app.registerExtension({ // When the node is off-screen or zoomed too small, ComfyUI hides // the DOM widget (display:none), which makes scrollHeight return 0. if (!domH || domH <= 0) { - // No reliable layout info. If we already have a measured size, - // keep it (don't collapse). Otherwise fall back to a content - // based estimate so a brand-new hidden node still gets a size. - if (!this._minH) { - const h = this._calcHeight(); - this._minH = h; - this.size[1] = h; - app.graph.setDirtyCanvas(true, true); - } + // No reliable layout info. Use the content-based estimate so + // the node still gets a reasonable height — and so deleting + // segments while hidden actually shrinks the node (otherwise + // _minH would be stuck at the old, larger value and the + // deleted segments leave behind empty, non-interactive space). + const h = this._calcHeight(); + this._minH = h; + this.setSize([this.size[0], h]); + app.graph.setDirtyCanvas(true, true); return; } const h = domH + 50; this._minH = h; - this.size[1] = h; + this.setSize([this.size[0], h]); app.graph.setDirtyCanvas(true, true); }); }; @@ -127,7 +127,16 @@ app.registerExtension({ del.style.cssText = "background:transparent;color:#666;border:none;border-radius:3px;padding:2px 5px;cursor:pointer;font-size:11px;flex-shrink:0;line-height:1;transition:color 0.15s,background 0.15s;"; del.onmouseenter = () => { del.style.color = "#fff"; del.style.background = "#c0392b"; }; del.onmouseleave = () => { del.style.color = "#666"; del.style.background = "transparent"; }; - del.onclick = () => { this._segments.splice(i, 1); this._sync(); this._render(); }; + del.onclick = () => { + this._segments.splice(i, 1); + this._sync(); + // Clear the cached min height so _applySize can actually shrink + // the node. Without this, _minH stays at the old (larger) value + // and the deleted segment's area becomes dead, non-interactive + // space — most visible after copy/paste. + this._minH = 0; + this._render(); + }; top.append(toggle, labelNum, label, del); @@ -219,7 +228,10 @@ app.registerExtension({ const onSerialize = nodeType.prototype.onSerialize; nodeType.prototype.onSerialize = function (o) { onSerialize?.apply(this, arguments); - o.prompt_segments = this._segments; + // Deep copy so a pasted/duplicated node doesn't share the same array + // reference with the original (which would cause edits in one to + // leak into the other). + o.prompt_segments = JSON.parse(JSON.stringify(this._segments)); }; const onRemoved = nodeType.prototype.onRemoved; @@ -233,6 +245,12 @@ app.registerExtension({ onConfigure?.apply(this, arguments); if (o.prompt_segments) { this._segments = o.prompt_segments; + // On paste/duplicate, ComfyUI may carry over a stale cached min + // height and a stale DOM widget element height from the original. + // Reset both so the new node measures fresh and shrinks correctly + // when segments are deleted. + this._minH = 0; + if (this._domWidget?.element) this._domWidget.element.style.height = "auto"; requestAnimationFrame(() => this._render()); } }; diff --git a/resolution_switcher.js b/resolution_switcher.js index 216036f..0e3f3e2 100644 --- a/resolution_switcher.js +++ b/resolution_switcher.js @@ -1,7 +1,7 @@ import { app } from "../../scripts/app.js"; // 语言配置:将下面一行的 "en" 改成 "zh" 即可切换中文 -const LANG = "en"; +const LANG = "zh"; const I18N = { en: { w: "W", h: "H", batch: "Batch", add: "+ Add Resolution", @@ -62,14 +62,14 @@ app.registerExtension({ if (!this._minH) { const h = this._calcHeight(); this._minH = h; - this.size[1] = h; + this.setSize([this.size[0], h]); app.graph.setDirtyCanvas(true, true); } return; } const h = domH + 50; this._minH = h; - this.size[1] = h; + this.setSize([this.size[0], h]); app.graph.setDirtyCanvas(true, true); }); }; @@ -113,8 +113,12 @@ app.registerExtension({ capsule.style.cssText = `width:30px;height:16px;border-radius:8px;background:${active ? "#43a047" : "#555"};position:relative;transition:background 0.15s;border:1px solid ${active ? "#2e7d32" : "#424242"};`; capsule.innerHTML = `
`; toggle.onclick = () => { - // Mutually exclusive: turning one on turns all others off. - this._presets.forEach((q, j) => { q.active = (j === i) ? !q.active : false; }); + // Radio behavior: clicking the active one is a no-op (must + // always keep exactly one active — this node switches + // resolutions, it can't disable resolution). Clicking any + // other moves the active flag to it. + if (active) return; + this._presets.forEach((q, j) => { q.active = (j === i); }); this._sync(); this._render(); }; @@ -240,7 +244,9 @@ app.registerExtension({ const onSerialize = nodeType.prototype.onSerialize; nodeType.prototype.onSerialize = function (o) { onSerialize?.apply(this, arguments); - o.resolution_presets = this._presets; + // Deep copy so a pasted/duplicated node doesn't share the same array + // reference with the original. + o.resolution_presets = JSON.parse(JSON.stringify(this._presets)); }; const onConfigure = nodeType.prototype.onConfigure; diff --git a/resolution_switcher.py b/resolution_switcher.py index 9f519e9..b7d688d 100644 --- a/resolution_switcher.py +++ b/resolution_switcher.py @@ -1,3 +1,4 @@ +import json import torch class ResolutionSwitcher: @@ -21,8 +22,6 @@ class ResolutionSwitcher: CATEGORY = "latent" def build(self, presets="[]"): - import json - # Default: 1024x1024, batch 1 width, height, batch = 1024, 1024, 1 @@ -49,11 +48,3 @@ class ResolutionSwitcher: latent = torch.zeros([batch, 4, height // 8, width // 8]) return ({"samples": latent},) - - -NODE_CLASS_MAPPINGS = { - "ResolutionSwitcher": ResolutionSwitcher, -} -NODE_DISPLAY_NAME_MAPPINGS = { - "ResolutionSwitcher": "Resolution Switcher", -}