节点完善

This commit is contained in:
wjluoxiao
2026-11-14 04:05:36 +08:00
parent 6e2e3d3ba7
commit 1975e983b6
10 changed files with 415 additions and 269 deletions
+7 -7
View File
@@ -489,10 +489,10 @@
},
"XB_ImagePromptPresetPro": {
"display_name": "XB-BOX - 🖼️ Image Prompt Preset Pro",
"description": "Image Prompt Preset + Prompt Enhance/Reverse fused: empty latent type + aspect params + 8-category element panel; optional LLM enhance/reverse (local model or online API, enhance preset / task preset / extra settings / sampling params). Outputs: prompt / prompt list / prompt settings / empty latent.",
"description": "Image Prompt Preset + Prompt Enhance/Reverse fused: latent type + aspect params + 8-category element panel; optional LLM enhance/reverse (local model or online API, enhance preset / task preset / extra settings / sampling params). Outputs: prompt / prompt list / prompt settings / empty latent.",
"inputs": {
"latent_kind": {
"name": "Empty Latent Type"
"name": "Latent Type"
},
"output_lang": {
"name": "Output Language"
@@ -525,7 +525,7 @@
"name": "Batch Size"
},
"use_llm": {
"name": "✅ Enable LLM Enhance"
"name": "Enable LLM"
},
"backend": {
"name": "LLM Backend"
@@ -560,10 +560,10 @@
},
"XB_QwenPromptPreset": {
"display_name": "XB-BOX - 🖼️ Qwen2.1 Prompt Preset",
"description": "Image prompt preset + official Load CLIP + official Generate Text: empty latent type / aspect params + 8-category element panel, then the text encoder turns the body into a full prompt. Outputs: prompt / empty latent.",
"description": "Image prompt preset + official Load CLIP + official Generate Text: latent type / aspect params + 8-category element panel, then the text encoder turns the body into a full prompt. Outputs: prompt / empty latent.",
"inputs": {
"latent_kind": {
"name": "Empty Latent Type"
"name": "Latent Type"
},
"output_lang": {
"name": "Output Language"
@@ -669,7 +669,7 @@
"name": "Preset Mode"
},
"latent_kind": {
"name": "Empty Latent Type"
"name": "Latent Type"
},
"three_view_text": {
"name": "Preset Text (manage presets from the buttons above)"
@@ -707,7 +707,7 @@
"description": "Everything in Image Prompt Preset (latent type + aspect ratio / size / batch + 8-element prompt panels + preset files), plus two pickers: Pose Reference and Outfit Reference. Bind a catalogued gallery (filter by system / category / angle / tag, grouped by pose with an angle strip) or any plain image folder (grid by file name). Each reference can be disabled: a disabled output produces nothing (like muting it) and skips downstream nodes. Outputs: prompt / empty latent / pose image / outfit image.",
"inputs": {
"latent_kind": {
"name": "Empty Latent Type"
"name": "Latent Type"
},
"output_lang": {
"name": "Output Language"
+8 -8
View File
@@ -5263,10 +5263,10 @@
},
"XB_ImagePromptPresetPro": {
"display_name": "XB-BOX - 🖼️ 生图提示词预设Pro",
"description": "生图提示词预设 + 提示词增强反推 融合:空latent类型/画幅参数 + 8 大类元素面板拼装提示词;可选 LLM 反推(本地模型或在线 API,增强预设 / 反推预设 / 追加设定 / 采样参数 / 生成后控制)。输出:提示词 / 空latent / 提示词列表 / 提示词设定。",
"description": "生图提示词预设 + 提示词增强反推 融合:latent类型/画幅参数 + 8 大类元素面板拼装提示词;可选 LLM 反推(本地模型或在线 API,增强预设 / 反推预设 / 追加设定 / 采样参数 / 生成后控制)。输出:提示词 / 空latent / 提示词列表 / 提示词设定。",
"inputs": {
"latent_kind": {
"name": "空latent类型"
"name": "latent类型"
},
"output_lang": {
"name": "输出语言"
@@ -5299,7 +5299,7 @@
"name": "生成数量"
},
"use_llm": {
"name": "✅ 启用 LLM 反推"
"name": "启用LLM"
},
"backend": {
"name": "LLM 后端"
@@ -5334,10 +5334,10 @@
},
"XB_QwenPromptPreset": {
"display_name": "XB-BOX - 🖼️ Qwen2.1提示词预设",
"description": "生图提示词预设 + 官方 Load CLIP + 官方 Generate Text:空latent类型 / 画幅参数 + 8 大类元素面板拼装提示词,再用文本编码器把正文生成成完整提示词。输出:提示词 / 空latent。",
"description": "生图提示词预设 + 官方 Load CLIP + 官方 Generate Text:latent类型 / 画幅参数 + 8 大类元素面板拼装提示词,再用文本编码器把正文生成成完整提示词。输出:提示词 / 空latent。",
"inputs": {
"latent_kind": {
"name": "空latent类型"
"name": "latent类型"
},
"output_lang": {
"name": "输出语言"
@@ -5443,7 +5443,7 @@
"name": "预设模式"
},
"latent_kind": {
"name": "空latent类型"
"name": "latent类型"
},
"three_view_text": {
"name": "预设句(右上角按钮管理预设)"
@@ -5478,10 +5478,10 @@
},
"XB_ImagePreset": {
"display_name": "XB-BOX - 🖼️ 生图提示词预设(图)",
"description": "生图提示词预设的全部能力(空latent类型 + 画幅比例/宽高/数量 + 8 大类元素面板拼词 + 预设文件),外加「🎬 姿态参考图选择 / 👚 穿搭参考图选择」两个按钮:可绑定带素材清单的整套图库(体系/分类/视角/标签筛选 + 姿势分组 + 视角条)或任意普通图片文件夹(按文件名多宫格)。参考图可单独勾选「禁用」:禁用后该输出不做任何输出(等同 mute),下游节点会被跳过。输出:提示词 / 空latent / 姿态图 / 穿搭图。",
"description": "生图提示词预设的全部能力(latent类型 + 画幅比例/宽高/数量 + 8 大类元素面板拼词 + 预设文件),外加「🎬 姿态参考图选择 / 👚 穿搭参考图选择」两个按钮:可绑定带素材清单的整套图库(体系/分类/视角/标签筛选 + 姿势分组 + 视角条)或任意普通图片文件夹(按文件名多宫格)。参考图可单独勾选「禁用」:禁用后该输出不做任何输出(等同 mute),下游节点会被跳过。输出:提示词 / 空latent / 姿态图 / 穿搭图。",
"inputs": {
"latent_kind": {
"name": "空latent类型"
"name": "latent类型"
},
"output_lang": {
"name": "输出语言"
+1 -1
View File
@@ -32,7 +32,7 @@ class XB_ImagePreset(XB_ImagePromptPreset):
RETURN_NAMES = ("提示词", "空latent", "姿态图", "穿搭图")
FUNCTION = "generate"
CATEGORY = "XB_ToolBox/Image_Params"
DESCRIPTION = ("生图预设:在「生图提示词预设」的全部能力(空latent类型 + 画幅比例/宽高/数量 + 8 大类元素面板拼词 + "
DESCRIPTION = ("生图预设:在「生图提示词预设」的全部能力(latent类型 + 画幅比例/宽高/数量 + 8 大类元素面板拼词 + "
"预设文件)之上,新增「🎬 姿态参考图选择 / 👚 穿搭参考图选择」两个按钮:可绑定带素材清单的整套图库"
"(按体系/分类/视角/标签筛选,按姿势编号分组并带视角条)或任意普通图片文件夹(按文件名多宫格)。"
"参考图可单独勾选「禁用」:禁用后该输出不做任何输出(等同 mute),下游节点会被跳过。"
+9 -9
View File
@@ -3,7 +3,7 @@
一个节点顶替「提示词输入框 + 画幅比例节点 + 官方空 latent」三件套:
· 表面参数(顺序 = 节点上从上到下的显示顺序):
空latent类型 / 输出语言 / 预设模式 / 画幅比例 / 宽度 / 高度 / 生成数量
latent类型 / 输出语言 / 预设模式 / 画幅比例 / 宽度 / 高度 / 生成数量
—— 画幅比例 + 步长锁定 + 比例联动 100% 复刻本包「XB-BOX - 🖼️ 图片参数大全」
(去掉强度(小数)/强度(整数)/缩放尺寸三项)
—— 预设句不再是表面参数(节点上没有文本显示框):选哪个预设模式,就用该模式预设 txt 的内容
@@ -33,7 +33,7 @@
· `manager_settings`(内部字段,由前端面板隐藏,表面不显示)只存**元素面板配置 JSON**
(已选词条 / 自建槽位 / 补充描述 / 自动保存),随工作流保存、节点间互不影响;
· `internal_prompt`(内部字段,同上前端隐藏)存提示词正文;
· 空latent类型 / 输出语言 / 预设模式 / 预设句 都是**节点表面参数**(随工作流保存)。
· latent类型 / 输出语言 / 预设模式 / 预设句 都是**节点表面参数**(随工作流保存)。
预设模式 = 预设 txt 文件(见 xb_presets.py):
· 目录 `xb_skill/XB_ImagePromptPreset/ZH` 与 `.../EN`,**一个 txt = 一个选项**;
@@ -56,7 +56,7 @@ from ...libs import xb_presets
ASPECT_RATIO_OPTIONS = ["Free", "1:1", "16:9", "9:16", "4:3", "3:4", "21:9", "2:3", "3:2"]
ASPECT_RATIO_MAP = {"1:1": 1.0, "16:9": 16 / 9, "9:16": 9 / 16,
"4:3": 4 / 3, "3:4": 3 / 4, "21:9": 21 / 9, "2:3": 2 / 3, "3:2": 3 / 2}
SIZE_STEP = 16 # 宽高步长的兜底值(实际以「空latent类型」的官方最小步长为准:8/16/32)
SIZE_STEP = 16 # 宽高步长的兜底值(实际以「latent类型」的官方最小步长为准:8/16/32)
SIZE_MIN = 16
BATCH_MAX = 4096 # 官方 EmptyLatentImage / EmptySD3LatentImage 的 batch 上限
MAX_RESOLUTION = int(getattr(nodes, "MAX_RESOLUTION", 16384) or 16384)
@@ -321,7 +321,7 @@ ELEMENT_CATEGORIES = ("prefix", "angle", "subject", "action", "clothes", "props"
def _default_settings():
"""节点默认配置(与前端 js/xb_image_prompt_preset.js 的 defaultSettings() 一一对应)。
⚠️ 自 0.9.x 起:输出语言 / 预设模式 / 预设句 / 空latent类型 已上提为**节点表面参数**,
⚠️ 自 0.9.x 起:输出语言 / 预设模式 / 预设句 / latent类型 已上提为**节点表面参数**,
不再存在这份 JSON 里(JSON 只留元素面板配置 + 自动保存开关)。
"""
return {
@@ -812,7 +812,7 @@ def build_prompt(lang, preset_mode, three_view_text, body, has_image=None, io_mo
class XB_ImagePromptPreset:
"""XB-BOX - 🖼️ 生图提示词预设:画幅参数 + 提示词拼装 + 官方空 latent 一体化。
· 表面参数顺序 = 空latent类型 / 输出语言 / 预设句(带「添加预设 / 编辑预设 / 复制文本」三个小按钮)
· 表面参数顺序 = latent类型 / 输出语言 / 预设句(带「添加预设 / 编辑预设 / 复制文本」三个小按钮)
/ 画幅比例 / 宽度 / 高度 / 生成数量(「预设模式」下拉已取消,字段仅作老工作流兼容保留);
· 「提示词」输出 = 预设句 + 节点提示词框正文;
· 「空latent」输出 = 官方空 latent(见 LATENT_KINDS,共 11 种,逐字段对齐,选项按首字母排列)。
@@ -826,7 +826,7 @@ class XB_ImagePromptPreset:
# ── 空latent 类型放最顶(用户要求)──
"latent_kind": (list(LATENT_KINDS), {
"default": DEFAULT_LATENT_KIND,
"tooltip": "空latent类型:选你正在用的模型即可(自动适配形状/下采样/步长)。\n"
"tooltip": "latent类型:选你正在用的模型即可(自动适配形状/下采样/步长)。\n"
"Z-image 16ch·/8·16 Flux2 128ch·/16·16 Flux3 128ch·/16·16(同 Flux2)\n"
"Moss-image 128ch·/16·16(同 Flux2) Qwen-image 16ch·/8·32\n"
"Krea2 16ch·/8·16 Anima 16ch·/8·8 Boogu 16ch·/8·8\n"
@@ -855,12 +855,12 @@ class XB_ImagePromptPreset:
}),
"width": ("INT", {
"default": 1024, "min": SIZE_MIN, "max": MAX_RESOLUTION, "step": SIZE_STEP,
"tooltip": "图片宽度(像素)。固定比例下改宽度会按比例重算高度;步长按「空latent类型」的官方最小步长"
"tooltip": "图片宽度(像素)。固定比例下改宽度会按比例重算高度;步长按「latent类型」的官方最小步长"
"(SDXL/Anima/Boogu=8,Flux2/Flux3/Krea2/SD3/Z-image=16,Qwen-image/Hunyuan=32)",
}),
"height": ("INT", {
"default": 1024, "min": SIZE_MIN, "max": MAX_RESOLUTION, "step": SIZE_STEP,
"tooltip": "图片高度(像素)。固定比例下改高度会按比例重算宽度;步长按「空latent类型」的官方最小步长(8/16/32)",
"tooltip": "图片高度(像素)。固定比例下改高度会按比例重算宽度;步长按「latent类型」的官方最小步长(8/16/32)",
}),
"batch_size": ("INT", {
"default": 1, "min": 1, "max": BATCH_MAX,
@@ -886,7 +886,7 @@ class XB_ImagePromptPreset:
RETURN_NAMES = ("提示词", "空latent")
FUNCTION = "generate"
CATEGORY = "XB_ToolBox/Image_Params"
DESCRIPTION = ("生图提示词预设:空latent类型(主流模型一键适配 Z-image/Flux2/Flux3/Moss-image/Qwen-image/Krea2/"
DESCRIPTION = ("生图提示词预设:latent类型(主流模型一键适配 Z-image/Flux2/Flux3/Moss-image/Qwen-image/Krea2/"
"Anima/Boogu/SDXL/SD3/Hunyuan)+ 输出语言/预设模式 + 画幅比例/宽度/高度/生成数量"
"(步长按各模型官方最小步长 8/16/32 + 比例联动)+ 提示词拼装(8 大类:风格/视角/主体/姿态/穿搭/道具/光影/背景)。"
"预设模式的选项与设定词来自 xb_skill/XB_ImagePromptPreset 下的 ZH / EN 预设 txt(按系统语言自动选择)。"
+6 -6
View File
@@ -4,7 +4,7 @@ XB-BOX - 🖼️ 生图提示词预设Pro
融合「XB-BOX - 🖼️ 生图提示词预设」与「XB-llama - ✨ 提示词增强反推」两个节点:
① 生图预设(原节点全部能力)
· 空latent类型(9 种主流模型,逐字段对齐官方 latent_format)
· latent类型(9 种主流模型,逐字段对齐官方 latent_format)
· 输出语言 / 预设模式 / 设定词(三视图·四视图·五视图·背景纯透明)
- 设定词既作为 LLM 的**增强参考**,也(启用 LLM 时)被**原封不动**加在最终提示词最顶端;
- 用户改过的设定词按「模式 + 语言」存在本节点里,换模式 / 换语言都不会丢。
@@ -69,7 +69,7 @@ from ...libs.xb_skills import SKILL_NONE, skill_list, skill_sig, skill_text
ASPECT_RATIO_OPTIONS = ["Free", "1:1", "16:9", "9:16", "4:3", "3:4", "21:9", "2:3", "3:2"]
ASPECT_RATIO_MAP = {"1:1": 1.0, "16:9": 16 / 9, "9:16": 9 / 16,
"4:3": 4 / 3, "3:4": 3 / 4, "21:9": 21 / 9, "2:3": 2 / 3, "3:2": 3 / 2}
SIZE_STEP = 16 # 宽高步长的兜底值(实际以「空latent类型」的官方最小步长为准:8/16/32)
SIZE_STEP = 16 # 宽高步长的兜底值(实际以「latent类型」的官方最小步长为准:8/16/32)
SIZE_MIN = 16
BATCH_MAX = 4096 # 官方 EmptyLatentImage / EmptySD3LatentImage 的 batch 上限
MAX_RESOLUTION = int(getattr(nodes, "MAX_RESOLUTION", 16384) or 16384)
@@ -725,7 +725,7 @@ class XB_ImagePromptPresetPro:
# ══ ① 生图预设(顺序与「🖼️ 生图提示词预设」保持一致)══
"latent_kind": (list(LATENT_KINDS), {
"default": DEFAULT_LATENT_KIND,
"tooltip": "空latent类型:选你正在用的模型即可(自动适配形状/下采样/步长)",
"tooltip": "latent类型:选你正在用的模型即可(自动适配形状/下采样/步长)",
}),
"output_lang": (list(OUTPUT_LANGS), {
"default": DEFAULT_LANG,
@@ -758,11 +758,11 @@ class XB_ImagePromptPresetPro:
}),
"width": ("INT", {
"default": 1024, "min": SIZE_MIN, "max": MAX_RESOLUTION, "step": SIZE_STEP,
"tooltip": "图片宽度(步长按「空latent类型」的官方最小步长 8/16/32)",
"tooltip": "图片宽度(步长按「latent类型」的官方最小步长 8/16/32)",
}),
"height": ("INT", {
"default": 1024, "min": SIZE_MIN, "max": MAX_RESOLUTION, "step": SIZE_STEP,
"tooltip": "图片高度(步长按「空latent类型」的官方最小步长 8/16/32)",
"tooltip": "图片高度(步长按「latent类型」的官方最小步长 8/16/32)",
}),
"batch_size": ("INT", {
"default": 1, "min": 1, "max": BATCH_MAX,
@@ -823,7 +823,7 @@ class XB_ImagePromptPresetPro:
OUTPUT_IS_LIST = (False, False, True, False)
FUNCTION = "generate"
CATEGORY = "XB_ToolBox/Image_Params"
DESCRIPTION = ("生图提示词预设Pro:生图预设(空latent类型/输出语言/预设模式/画幅参数 + 8 大类元素面板提示词拼装)"
DESCRIPTION = ("生图提示词预设Pro:生图预设(latent类型/输出语言/预设模式/画幅参数 + 8 大类元素面板提示词拼装)"
"+ LLM 提示词增强反推(本地模型或在线 API、增强预设、反推预设、追加设定、采样参数、生成后控制)。"
"「启用 LLM 反推」关 = 原预设行为;开 = 图/文 → 提示词。输出:提示词 / 空latent / 提示词列表 / 提示词设定。")
+7 -7
View File
@@ -5,7 +5,7 @@ XB-BOX - 🖼️ Qwen2.1提示词预设
把「LLM 反推」换成 **官方 Load CLIP + 官方 Generate Text** 这套纯本地文本生成链路。
① 生图预设
· 空latent类型(9 种主流模型,逐字段对齐官方 latent_format)
· latent类型(9 种主流模型,逐字段对齐官方 latent_format)
· 输出语言 / 预设模式 / 设定词(三视图·四视图·五视图·背景纯透明)
· 画幅比例 / 宽度 / 高度 / 生成数量
· 元素面板提示词拼装(8 大类词表,前端 js/xb_qwen_prompt_preset.js)
@@ -48,7 +48,7 @@ import comfy.sd
ASPECT_RATIO_OPTIONS = ["Free", "1:1", "16:9", "9:16", "4:3", "3:4", "21:9", "2:3", "3:2"]
ASPECT_RATIO_MAP = {"1:1": 1.0, "16:9": 16 / 9, "9:16": 9 / 16,
"4:3": 4 / 3, "3:4": 3 / 4, "21:9": 21 / 9, "2:3": 2 / 3, "3:2": 3 / 2}
SIZE_STEP = 16 # 宽高步长的兜底值(实际以「空latent类型」的官方最小步长为准:8/16/32)
SIZE_STEP = 16 # 宽高步长的兜底值(实际以「latent类型」的官方最小步长为准:8/16/32)
SIZE_MIN = 16
BATCH_MAX = 4096 # 官方 EmptyLatentImage / EmptySD3LatentImage 的 batch 上限
MAX_RESOLUTION = int(getattr(nodes, "MAX_RESOLUTION", 16384) or 16384)
@@ -679,7 +679,7 @@ from ...libs.xb_skills import skill_sig as _skill_signer
from ...libs.xb_skills import skill_text as _read_skill_text
DEFAULT_LANG = OUTPUT_LANGS[0]
# 用户要求:本节点的空latent类型默认 Qwen-image(与节点定位一致)
# 用户要求:本节点的latent类型默认 Qwen-image(与节点定位一致)
DEFAULT_LATENT = "Qwen-image" if "Qwen-image" in LATENT_KINDS else DEFAULT_LATENT_KIND
# ══════════════════════════════════════════════════════════════
@@ -894,7 +894,7 @@ class XB_QwenPromptPreset:
# ══ ① 生图预设(与「🖼️ 生图提示词预设」一致)══
"latent_kind": (list(LATENT_KINDS), {
"default": DEFAULT_LATENT,
"tooltip": "空latent类型:选你正在用的模型即可(自动适配形状/下采样/步长)",
"tooltip": "latent类型:选你正在用的模型即可(自动适配形状/下采样/步长)",
}),
"output_lang": (list(OUTPUT_LANGS), {
"default": DEFAULT_LANG,
@@ -929,11 +929,11 @@ class XB_QwenPromptPreset:
}),
"width": ("INT", {
"default": 1024, "min": SIZE_MIN, "max": MAX_RESOLUTION, "step": SIZE_STEP,
"tooltip": "图片宽度(步长按「空latent类型」的官方最小步长 8/16/32)",
"tooltip": "图片宽度(步长按「latent类型」的官方最小步长 8/16/32)",
}),
"height": ("INT", {
"default": 1024, "min": SIZE_MIN, "max": MAX_RESOLUTION, "step": SIZE_STEP,
"tooltip": "图片高度(步长按「空latent类型」的官方最小步长 8/16/32)",
"tooltip": "图片高度(步长按「latent类型」的官方最小步长 8/16/32)",
}),
"batch_size": ("INT", {
"default": 1, "min": 1, "max": BATCH_MAX,
@@ -1047,7 +1047,7 @@ class XB_QwenPromptPreset:
OUTPUT_IS_LIST = (False, False)
FUNCTION = "generate"
CATEGORY = "XB_ToolBox/Image_Params"
DESCRIPTION = ("Qwen2.1提示词预设:生图预设(空latent类型/输出语言/预设模式/画幅参数 + 8 大类元素面板拼装)"
DESCRIPTION = ("Qwen2.1提示词预设:生图预设(latent类型/输出语言/预设模式/画幅参数 + 8 大类元素面板拼装)"
"+ 官方 Load CLIP 与官方 Generate Text(纯本地文本生成)。"
"「启用 / 关闭 LLM 增强」在弹窗「🤖 LLM设置」顶部(默认启用;关闭 = 只输出拼装好的提示词)。"
"输出:提示词 / 空latent。")
+241 -2
View File
@@ -701,12 +701,251 @@ export function bindNodes2NodeFill(node, box, opts = {}) {
return uninstall;
}
/**
* ============================================================
* 稳定的节点高度管理(Nodes 2.0 / Vue 节点)
* ============================================================
* 实测得到的两条硬事实(这是所有尺寸问题的根源):
* ① **DOM 渲染高度 = max(node.size[1], 内容真实需求) + 标题栏(30)**,
* 与画布缩放无关(offsetHeight 在 0.5x / 1x / 1.5x 下完全一致)。
* 正常(不溢出)时恒有 `offsetHeight == size[1] + 30`。
* ② 一旦 `size[1] < 内容需求`,DOM 就会**撑破**节点盒子:视觉底边跑到 size 之外,
* 于是「拖拽弹跳」「底边压不上网格」全部来自这一个错位。
*
* 因此本实现只做两件事,且只在就绪后 / 松手后各跑一次:
* · **喂饱**:`size[1]` 不允许小于 DOM 实测需求(`offsetHeight - 30`),
* 且必须连续两次读到同样的溢出量才采纳 —— 布局中间帧的瞬时大值不会被永久固化
* (这是「偶发自动变高」的根因);
* · **压线**:`视觉底边 = pos.y + size[1] + 30`,把它补到网格线(δ 最多 9px)。
* 因为标题栏 30 本身是网格整数倍,只要 size 与 pos 都是网格倍数就自然对齐。
* · 不劫持 `onResize`:拖拽完全交给 LiteGraph 原生处理,避免和原生吸附互相打架。
*
* @param {object} node LiteGraph 节点
* @param {object} opts
* · minWidth 最小宽度(默认 420)
* · rows 节点表面「可见参数行数」(数字或函数)
* · domFixedH 兜底常量估算用:DOM 区固定高度
* · textMinH 兜底常量估算用:提示词框最小高度
* @returns {{ apply:Function, align:Function, markReady:Function, measure:Function, rows:Function,
* destroy:Function, chrome:Function, domHeight:Function, grid:Function, isReady:Function }}
*/
export function installStableNodeHeight(node, opts = {}) {
const MIN_W = Number(opts.minWidth) || 420;
const TITLE_H = 30; // Vue 节点标题栏高度(实测恒定)
const ROW_H = 26; // 原生 widget 单行高度
const DOM_FIXED_H = Number.isFinite(opts.domFixedH) ? opts.domFixedH : 116;
const TEXT_MIN_H = Number.isFinite(opts.textMinH) ? opts.textMinH : 300;
const HARD_MIN = 420;
const rowsOf = typeof opts.rows === "function" ? opts.rows : () => (Number(opts.rows) || 0);
/** 画布网格步长(ComfyUI 设置 → LiteGraph.CANVAS_GRID_SIZE → 10) */
const gridOf = () => {
try {
const v = app?.ui?.settings?.getSettingValue?.("Comfy.SnapToGrid.GridSize");
if (Number.isFinite(v) && v > 0) return v;
} catch (_) { /* ignore */ }
const lg = Number(window.LiteGraph?.CANVAS_GRID_SIZE);
return Number.isFinite(lg) && lg > 0 ? lg : 10;
};
/** 取节点**根**元素:同一个 data-node-id 还会命中节点内部的「节点编号」徽标(宽 20~30px),
* 它的高度与节点毫无关系,必须排除,否则量出来的高度是胡说八道。 */
const nodeEl = () => {
try {
const list = document.querySelectorAll(`[data-node-id="${node.id}"]`);
let best = null;
for (const el of list) {
if (!String(el.className || "").includes("lg-node")) continue;
if (!el.isConnected) continue;
if (!best || el.getBoundingClientRect().width > best.getBoundingClientRect().width) best = el;
}
return best;
} catch (_) { return null; }
};
/**
* DOM 实测高度,并**归一化到图单位**。
* ⎠ 必须自校准:DOM 量出来的是渲染像素,受 UI 缩放 / 浏览器缩放 / 画布变换影响;
* 而 node.size 是图单位。两者比例不为 1 时,差值会随 size 线性放大,
* 于是每一轮都判定「还差一点」→ 再长高 → 差值更大 → **指数级一直变高**(用户报障)。
* 缩放因子用「宽度」自校准(DOM 宽度就是按 node.size[0] 给的),不依赖任何假设。
* ⎡ 同时只认 .lg-node 根元素,避开「节点编号」徽标。
*/
const nodeMetrics = () => {
const el = nodeEl();
if (!el) return null;
let r;
try { r = el.getBoundingClientRect(); } catch (_) { return null; }
const wNode = Number(node.size?.[0]) || 0;
if (!(r.width > 0) || !(r.height > 0) || !(wNode > 0)) return null;
const k = r.width / wNode; // 渲染缩放因子(正常为 1)
if (!(k > 0.05) || !(k < 20)) return null;
return { k, h: r.height / k };
};
/** 节点 DOM 实际渲染高度(已归一化到与 node.size 同单位) */
const domHeight = () => {
const m = nodeMetrics();
return m ? m.h : 0;
};
let ready = false;
let pendingNeed = 0;
let retries = 0;
let alignTimer = null;
let verifyUntil = 0;
let confirmedNeed = 0; // 已确认的内容需求(同宽度下不变),用于拖拽到下限时即时弹回、不反复抖
let confirmedW = 0;
let grownTo = 0; // 上一次「因内容溢出而长高」到的 size
let grownNeed = 0; // 长高时读到的需求
let frozen = false; // 长高后溢出不减 ⇒ 溢出与 size 无关,永不再长(防指数级跑飞)
/** 延时再对齐一次(去重)——用于「溢出量需要两次确认」的二次采样 */
const scheduleAlign = (delay) => {
if (alignTimer) return;
alignTimer = setTimeout(() => {
alignTimer = null;
try { align(); } catch (_) { /* ignore */ }
}, delay || 120);
};
/** 兜底估算(仅供下限/调试;真实值以 DOM 实测为准) */
const measure = () => Math.max(HARD_MIN, Math.round(TITLE_H + rowsOf() * ROW_H + DOM_FIXED_H + TEXT_MIN_H));
/** DOM 实测的「内容真实需求」高度;0 表示 DOM 还没挂载 */
const domNeed = () => {
const h = domHeight();
return h > 0 ? h - TITLE_H : 0;
};
/** chrome = DOM 高度 − size,正常应为 30(标题栏);大于 30 说明内容溢出 */
const chromeOf = () => {
const h = domHeight();
const cur = Number(node.size?.[1]) || 0;
return h > 0 && cur > 0 ? Math.round(h - cur) : 0;
};
const lockWidth = () => {
try {
const w = Number.isFinite(node.size?.[0]) ? node.size[0] : 0;
if (w >= MIN_W) return;
node.setSize?.([MIN_W, Math.max(Number(node.size?.[1]) || HARD_MIN, HARD_MIN)]);
} catch (_) { /* ignore */ }
};
/** 对齐一次(同步)——不能用 await:页面不可见时 requestAnimationFrame 不会回调,
* 一旦 await 它就会永远挂住。改为同步写入 + 多次调用(markReady 四拍 / 松手后延时)收敛。 */
const align = () => {
if (!ready || !node.size) return 0;
lockWidth();
const w = Number(node.size?.[0]) || MIN_W;
let h = Number(node.size?.[1]) || HARD_MIN;
let changed = false;
// ① 喂饱:连续两次读到同样的溢出量才采纳,避免布局中间帧把节点永久顶高
const need = domNeed();
if (need > h + 2) {
if (frozen) {
// 已判定「长高治不了溢出」:不再动 size(否则每轮都长一点 → 一直变高)
pendingNeed = 0;
retries = 0;
} else if (grownTo && Math.abs(h - grownTo) < 2 && need > grownNeed + 2) {
// 已按需求长过一次,DOM 反而又变高了 ⇒ 量出来的高度与 size 脱钩,长高无效,冻结
frozen = true;
pendingNeed = 0;
retries = 0;
try {
console.warn(`[XB-BOX] 节点高度增长无法收敛(size=${Math.round(h)} 需求=${Math.round(need)} 宽度=${Math.round(w)}),已冻结自动长高:${node.title || node.type}`);
} catch (_) { /* ignore */ }
} else if (Math.abs(need - confirmedNeed) <= 2 && Math.abs(w - confirmedW) <= 1) {
h = Math.ceil(need); // 已知需求:立即弹回,不再等二次采样
pendingNeed = 0;
retries = 0;
} else if (Math.abs(need - pendingNeed) <= 2) {
h = Math.ceil(need);
confirmedNeed = need;
confirmedW = w;
pendingNeed = 0;
retries = 0;
} else {
pendingNeed = need;
if (retries++ < 8) scheduleAlign(150); // 自己排一次二次采样,不依赖外部再调
}
} else {
pendingNeed = 0;
retries = 0;
}
// ② 压线:视觉底边 = pos.y + size + 标题栏
const g = gridOf() > 0 ? gridOf() : 10;
const bottom = (Number(node.pos?.[1]) || 0) + Math.round(h) + TITLE_H;
const delta = (g - (bottom % g)) % g;
if (delta > 0.5) h = Math.round(h) + delta;
if (Math.abs(h - (Number(node.size?.[1]) || 0)) >= 1) {
const prevSize = Number(node.size?.[1]) || 0;
const growing = h > prevSize + 2;
if (growing) { grownTo = Math.round(h); grownNeed = need; }
else if (frozen && h < prevSize - 2) { frozen = false; grownTo = 0; grownNeed = 0; } // 被拖矮了 → 重新允许长一次
node.setSize([w, Math.round(h)]);
node.setDirtyCanvas?.(true, true);
changed = true;
verifyUntil = Date.now() + 4000; // 尺寸刚变过,允许后续对开渲染核对
} else if (!frozen && chromeOf() > TITLE_H + 4 && Date.now() < verifyUntil) {
// 内容仍溢出但短时内没读到稳定需求(DOM 重排布有延迟)→ 再等一拍复查
scheduleAlign(300);
}
return changed ? 1 : 0;
};
/** DOM 挂载 + 内部字段藏好之后调用一次(此后才允许写尺寸) */
const markReady = () => {
ready = true;
pendingNeed = 0;
retries = 0;
confirmedNeed = 0;
confirmedW = 0;
grownTo = 0;
grownNeed = 0;
frozen = false;
verifyUntil = Date.now() + 5000;
[0, 120, 400, 900].forEach((d) => setTimeout(() => { try { align(); } catch (_) { /* ignore */ } }, d));
};
// ⚠️ 不劫持 onResize:拖拽全交给 LiteGraph 原生(自己劫持是「弹跳好几格」的直接原因)
// 工作流加载 / 复制节点后重新对齐(configure 会覆盖 size,必须重新喂一次)
try {
const prevCfg = node.onConfigure;
node.onConfigure = function (...args) {
const r = prevCfg?.apply(this, args);
ready = false;
pendingNeed = 0;
retries = 0;
confirmedNeed = 0;
confirmedW = 0;
grownTo = 0;
grownNeed = 0;
frozen = false;
verifyUntil = 0;
setTimeout(() => { try { markReady(); } catch (_) { /* ignore */ } }, 150);
return r;
};
} catch (_) { /* ignore */ }
const destroy = () => { try { node.__xbStableHeight = null; } catch (_) { /* ignore */ } };
const api = {
apply: align, align, markReady, measure, rows: rowsOf, destroy,
chrome: chromeOf, domHeight, grid: gridOf,
isReady: () => ready,
};
try { node.__xbStableHeight = api; } catch (_) { /* ignore */ }
return api;
}
/**
* 遍历「根图 + 所有子图(含嵌套)」,返回全部节点。
* 放在子图里的节点不在 app.graph._nodes 里,轮询时必须用这个。
*/
export function allNodes() {
const out = [];
export function allNodes() { const out = [];
const seenGraphs = new Set();
const seenNodes = new Set();
const visit = (g) => {
+43 -74
View File
@@ -2,7 +2,7 @@
* XB-BOX - 🖼️ 生图提示词预设 — 前端面板
* ============================================================
* 节点:XB_ImagePromptPreset(后端 nodes_image_prompt_preset.py,V1 经典 API)
* 表面:空latent类型 / 输出语言 / 预设模式 / (预设句:选中有预设文件的档位时显示)/ 画幅比例 / 宽度 / 高度 / 生成数量
* 表面:latent类型 / 输出语言 / 预设模式 / (预设句:选中有预设文件的档位时显示)/ 画幅比例 / 宽度 / 高度 / 生成数量
* + 节点提示词框 + 8 个分类按钮(4 列 × 2 行):🏷️ 风格 | 🎥 视角 | 👤 主体 | 🎬 姿态 | 👚 装扮 | 🎒 道具 | 💡 光影 | 🏞️ 背景
* 每个按钮一个面板,面板顶部「分类签」切子类;选项行整行可点(加入 → 变蓝 ✔ / 再点移除)
* 道具类点行会先弹「与主体的关系」多宫格标签窗(30 个关系词)
@@ -26,7 +26,7 @@
import { app } from "../../scripts/app.js";
import { api } from "../../scripts/api.js";
import { bindNodes2NodeFill, installLatentGuard } from "./xb_compat.js";
import { installLatentGuard, installStableNodeHeight } from "./xb_compat.js";
import { openRefPicker } from "./xb_gallery_picker.js";
const NODE_TYPE = "XB_ImagePromptPreset";
@@ -109,7 +109,7 @@ function modeHasImage(node) {
const ASPECT_MAP = { "1:1": 1, "16:9": 16 / 9, "9:16": 9 / 16, "4:3": 4 / 3, "3:4": 3 / 4, "21:9": 21 / 9, "2:3": 2 / 3, "3:2": 3 / 2 };
const SIZE_STEP = 16; // width/height 的兜底步长(实际以「空latent类型」的官方最小步长为准)
const SIZE_STEP = 16; // width/height 的兜底步长(实际以「latent类型」的官方最小步长为准)
const SIZE_MIN = 16;
const SIZE_MAX = 16384;
const BATCH_MAX = 4096;
@@ -1660,7 +1660,7 @@ function bindWheelZoom(root) {
* 配置读写
* ============================================================ */
function defaultSettings() {
// ⚠️ 自 0.9.x 起:输出语言 / 预设模式 / 预设句 / 空latent类型 已上提为**节点表面参数**,
// ⚠️ 自 0.9.x 起:输出语言 / 预设模式 / 预设句 / latent类型 已上提为**节点表面参数**,
// 不再存在这份 JSON 里;JSON 只留元素面板配置 + 自动保存开关。
return {
auto_save: true,
@@ -1819,7 +1819,7 @@ function pickOpt(options, value, def) {
const v = (value == null) ? "" : String(value).trim();
return options.includes(v) ? v : def;
}
/** 步长锁定(与后端 _round_step 一致:floor(v/step + 0.5);step/min/max 由空latent类型决定) */
/** 步长锁定(与后端 _round_step 一致:floor(v/step + 0.5);step/min/max 由latent类型决定) */
function roundStep(v, step = SIZE_STEP, minSize = SIZE_MIN, maxSize = SIZE_MAX) {
const n = Math.floor((Number(v) || 0) / step + 0.5) * step;
return Math.max(minSize, Math.min(maxSize, n));
@@ -2791,7 +2791,7 @@ function setupNode(node) {
};
hideInternal();
// ── 「空latent」输出没连线时:空latent类型 / 画幅比例 / 宽度 / 高度 / 生成数量 全部变灰不可操作
// ── 「空latent」输出没连线时:latent类型 / 画幅比例 / 宽度 / 高度 / 生成数量 全部变灰不可操作
// (接了「空latent」输出连线后自动恢复;接不接都只是 UI 层面,后端取值不受影响)
const refreshLatentGuard = installLatentGuard(node, ["latent_kind", "aspect_ratio", "width", "height", "batch_size"]);
// ── 节点表面 DOM:结构照抄「短剧导演台」(js/asset_manager.js)──
@@ -2949,7 +2949,7 @@ function setupNode(node) {
// 只剩一个提示词文本框:预设内容已经并进正文,面板不再额外前置
presetText: "",
});
/** 当前空latent类型对应的步长/上下限(后端按同一张表算,保证所见即所得) */
/** 当前latent类型对应的步长/上下限(后端按同一张表算,保证所见即所得) */
const sizeOpt = () => {
const sp = latentSpec(surfaceOf().kind);
return { step: sp.step, min: sp.min, max: sp.max };
@@ -3416,19 +3416,19 @@ function setupNode(node) {
node.setDirtyCanvas?.(true, true);
};
// 空latent类型 → 同步 width/height/batch 的原生 step & 上下限(箭头/拖动按官方步长走)
// latent类型 → 同步 width/height/batch 的原生 step & 上下限(箭头/拖动按官方步长走)
const applyKindLimits = () => {
const sp = latentSpec(surfaceOf().kind);
for (const w of [wW, wH]) {
if (w && w.options) { w.options.step = w.options.step2 = sp.step; w.options.min = sp.min; w.options.max = sp.max; }
}
if (wB && wB.options) wB.options.max = sp.batchMax;
if (wKind && wKind.options) wKind.options.tooltip = `空latent类型(${sp.desc})\n尺寸步长 ${sp.step},batch 上限 ${sp.batchMax}。\n选你正在用的模型即可,形状/下采样/步长会自动适配。`;
if (wKind && wKind.options) wKind.options.tooltip = `latent类型(${sp.desc})\n尺寸步长 ${sp.step},batch 上限 ${sp.batchMax}。\n选你正在用的模型即可,形状/下采样/步长会自动适配。`;
const b = parseInt(readWidgetValue(wB), 10) || 1;
if (b > sp.batchMax) setWidgetValue(wB, sp.batchMax);
};
// ── 画幅比例 + 步长联动(语义对齐 js/xb_video.js;步长按当前空latent类型) ──
// ── 画幅比例 + 步长联动(语义对齐 js/xb_video.js;步长按当前latent类型) ──
let syncing = false;
const snapByWidth = () => {
if (syncing) return;
@@ -3538,79 +3538,45 @@ function setupNode(node) {
node._xbDomWidget = widget;
node._xbSyncWidth = syncDomWidth; // 供 canvas.onDrawForeground 每帧调用
// 尺寸刷新 —— 照抄短剧导演台 `refreshSize`:
// ① 用 LiteGraph 自己的 computeSize() 算高度(不读任何 DOM 尺寸 → 无反馈环)
// ② 只做「下限保护」:未设置 / 小于下限时才 setSize,绝不覆盖用户拖拽出来的高度
// ③ 只在「初始化(3 次) + 工作流加载(configure)」调用,打字 / 面板保存 / onResize 一律不调
// ⚠️ 这就是「整个界面卡顿」的隔离带:一旦在打字或 onResize 里调它,
// setSize → onResize → setSize 会形成抖动环,每帧整画布重绘 → 全界面卡。
// 下限 = 标题 + 各可见 widget 实际高度(多行 STRING 会更高)+ DOM 固定行 + 提示词框最小高度。
// 这样 4 个或 8 个表面参数(三视图预设句框显/隐)都自适应,不用手写魔数。
const widgetRowH = (w) => {
try {
const s = w && w.computeSize ? w.computeSize(node.size?.[0] || 420) : null;
if (Array.isArray(s) && Number.isFinite(s[1]) && s[1] > 0) return s[1];
} catch (_) {}
return (w && (w.type === "hidden" || w.hidden)) ? 0 : 26;
};
const minNodeHeight = () => {
let h = 30; // 节点标题
for (const w of (node.widgets || [])) h += widgetRowH(w);
return Math.max(420, Math.round(h + DOM_FIXED_H + TEXT_MIN_H));
};
refreshSize = () => {
try {
lockMinWidth();
syncDomWidth();
const size = node.computeSize?.();
const computed = (Array.isArray(size) && Number.isFinite(size[1])) ? size[1] : 0;
const need = Math.max(computed, minNodeHeight()); // 取「原生计算」与「DOM 需求」的较大值
if (!node.size || !Number.isFinite(node.size[1]) || node.size[1] < need) {
node.setSize?.([node.size?.[0] || MIN_W, need]);
}
} catch (_) {}
node.setDirtyCanvas?.(true, true);
};
// ⚠️ 不能在这里同步跑 refreshSize:工作流加载时 configure 还没应用存档尺寸,
// 先按默认 widget 算出来的高度会让节点「长高一下」,随后被存档高度拉回 → 刷新瞬间高度闪烁。
// 排到本轮任务末尾(configure 已落定)再做下限保护。
setTimeout(refreshSize, 0);
setTimeout(refreshSize, 60);
setTimeout(refreshSize, 1200); // 兜底:加载工作流后 widget 才铺完时再校正一次(只增不减)
// 尺寸刷新 —— 用「常量 + 白名单行数」计算,绝不读 widget 运行时状态
// ⚠️ 历史坑(已由 installStableNodeHeight 彻底修好):以前用 node.computeSize() +
// 遍历 node.widgets 求和,工作流加载/切图时 widget 被重建、hidden 标记短暂丢失,
// 会把「全部 widget 可见」的巨大高度一次固化下来(且只增不减)→ 节点偶发自动变高。
const SURFACE_ROWS = 6; // 表面可见参数:latent类型/输出语言/画幅比例/宽度/高度/生成数量
const stableH = installStableNodeHeight(node, {
minWidth: MIN_W,
rows: SURFACE_ROWS,
domFixedH: DOM_FIXED_H,
textMinH: TEXT_MIN_H,
getDomWidget: () => widget,
});
const minNodeHeight = () => stableH.measure();
refreshSize = () => { stableH.align(); syncDomWidth(); };
node._xbRefreshSize = refreshSize;
node._xbEl = container; // 调试/自动化定位用
// Nodes 2.0:面板里的提示词框不会自己长高(节点高度由 DOM 内容主导,flex:1 停在 min-height)
// → 默认 300px;用户拖节点尺寸手柄时按指针位移实时跟随(两个方向都跟手);
// 切回经典模式自动还原原样,交回原有 flex 布局。
// ⚠️ 不能只把「提示词框」钉成固定 px:节点拖高后容器高度仍由内容决定 ⇒ 框下方留一片空白
// (用户报障)。这里改钉**容器**:高度 = `--node-height`(前端自带的 CSS 变量)− 实测 chrome,
// 容器内的 flex 链(标签 + 提示词框 flex:1)自然把框撑到节点下边缘。
try {
node._xbUninstallNodeFill = bindNodes2NodeFill(node, container, { min: DOM_MIN_H });
} catch (e) { console.warn("[XB-生图提示词预设] 2.0 高度跟随安装失败", e); }
// ⚠️ 不再使用 bindNodes2NodeFill:它会按 `calc(--node-height - 实测 top - PAD)` 写死容器高度,
// 实测 top 一旦偏小,容器就会被算得比可用空间大 ⇒ DOM 撑破节点 ⇒ 视觉底边失控 / 拖拽弹跳。
// 容器自身的 `height:100%` + 内部 flex 链已经能把提示词框撑到节点下边缘,无需额外干预。
// 节点 resize:立即同步 overlay 宽度 + 重绘;防抖后只校正「最小宽度 / 最小高度」。
// ⚠️ 高度下限是硬约束(输入框永远 ≥500px):拖小节点时一次性拉回,
// 拉回后尺寸已等于下限 → 不会再次触发 setSize,不会形成抖动环。
// 节点 resize:只同步 overlay 宽度 + 重绘。
// 高度下限/用户高度由 installStableNodeHeight 内部处理(它区分「用户拖拽」与「程序写入」),
// 这里不再调 setSize,避免与之打架形成抖动环。
let _xbResizeTimer = null;
let _xbClamping = false;
const prevOnResize = node.onResize;
node.onResize = function (...args) {
const rr = prevOnResize?.apply(this, args);
try {
syncDomWidth();
if (!_xbClamping) {
const need = minNodeHeight();
const cur = Number.isFinite(node.size?.[1]) ? node.size[1] : 0;
if (cur < need) {
_xbClamping = true;
try { node.setSize?.([node.size?.[0] || MIN_W, need]); } finally { _xbClamping = false; }
}
}
node.setDirtyCanvas?.(true, true);
clearTimeout(_xbResizeTimer);
_xbResizeTimer = setTimeout(() => { lockMinWidth(); syncDomWidth(); node.setDirtyCanvas?.(true, true); }, 200);
_xbResizeTimer = setTimeout(() => {
lockMinWidth();
syncDomWidth();
// 松手后补一次网格对齐:原生拖拽按网格步进 size,但视觉底边还要加 chrome 才压线
try { stableH.align(); } catch (_) {}
node.setDirtyCanvas?.(true, true);
}, 200);
} catch (_) {}
return rr;
};
@@ -3652,7 +3618,7 @@ function setupNode(node) {
if (PRESET_TEXT_DEFAULT[out.preset_mode] || MODES.includes(out.preset_mode)) setWidgetValue(wMode, out.preset_mode);
if (typeof out.three_view_text === "string" && out.three_view_text.trim()) setWidgetValue(w3v, out.three_view_text);
if (String(out.latent_channels || "").startsWith("16")) setWidgetValue(wKind, LATENT_KINDS[1]);
notify("已把旧版「输出设置」里的语言/模式/预设句/空latent类型迁到节点表面参数", "success");
notify("已把旧版「输出设置」里的语言/模式/预设句/latent类型迁到节点表面参数", "success");
}
if (out !== undefined) {
delete data.output;
@@ -3697,12 +3663,15 @@ function setupNode(node) {
const boot = () => {
hideInternal(); // 工作流加载后 widget 可能被重建 → 重新藏
// ⚠️ 关键时序:必须先 hideInternal()(内部字段全部藏好)再 markReady(),
// 否则「隐藏还没打上」的那一拍会把全可见高度固化下来(旧的偶发变高就是这样产生的)。
try { stableH.markReady(); } catch (_) {}
if (isGalleryNode(node)) { refreshRefButton("pose"); refreshRefButton("costume"); }
refreshLatentGuard(); // 「空latent」未连线 → 空latent类型/画幅/宽高/数量变灰
refreshLatentGuard(); // 「空latent」未连线 → latent类型/画幅/宽高/数量变灰
migrateLegacyOutput();
migrateLegacyPreset();
syncPromptBoxFromWidget();
applyKindLimits(); // 按当前空latent类型同步 step / 上下限
applyKindLimits(); // 按当前latent类型同步 step / 上下限
applyPresetTextVisibility();
const ar = String(readWidgetValue(wAR) ?? "Free");
const [nw, nh] = normalizeSize(ar, readWidgetValue(wW), readWidgetValue(wH), sizeOpt());
+45 -76
View File
@@ -15,7 +15,7 @@
* XB-BOX - 🖼️ 生图提示词预设 — 前端面板
* ============================================================
* 节点:XB_ImagePromptPreset(后端 nodes_image_prompt_preset.py,V1 经典 API)
* 表面:空latent类型 / 输出语言 / 预设模式 / (三视图/四视图/五视图预设句)/ 画幅比例 / 宽度 / 高度 / 生成数量
* 表面:latent类型 / 输出语言 / 预设模式 / (三视图/四视图/五视图预设句)/ 画幅比例 / 宽度 / 高度 / 生成数量
* + 节点提示词框 + 8 个分类按钮(4 列 × 2 行):🏷️ 风格 | 🎥 视角 | 👤 主体 | 🎬 姿态 | 👚 装扮 | 🎒 道具 | 💡 光影 | 🏞️ 背景
* 每个按钮一个面板,面板顶部「分类签」切子类;选项行整行可点(加入 → 变蓝 ✔ / 再点移除)
* 道具类点行会先弹「与主体的关系」多宫格标签窗(30 个关系词)
@@ -35,7 +35,7 @@
import { app } from "../../scripts/app.js";
import { api } from "../../scripts/api.js";
import { bindNodes2NodeFill, installLatentGuard } from "./xb_compat.js";
import { installLatentGuard, installStableNodeHeight } from "./xb_compat.js";
const NODE_TYPE = "XB_ImagePromptPresetPro";
@@ -83,7 +83,7 @@ function modeHasImage(node) {
const ASPECT_MAP = { "1:1": 1, "16:9": 16 / 9, "9:16": 9 / 16, "4:3": 4 / 3, "3:4": 3 / 4, "21:9": 21 / 9, "2:3": 2 / 3, "3:2": 3 / 2 };
const SIZE_STEP = 16; // width/height 的兜底步长(实际以「空latent类型」的官方最小步长为准)
const SIZE_STEP = 16; // width/height 的兜底步长(实际以「latent类型」的官方最小步长为准)
const SIZE_MIN = 16;
const SIZE_MAX = 16384;
const BATCH_MAX = 4096;
@@ -1510,7 +1510,7 @@ function bindWheelZoom(root) {
* 配置读写
* ============================================================ */
function defaultSettings() {
// ⚠️ 自 0.9.x 起:输出语言 / 预设模式 / 预设句 / 空latent类型 已上提为**节点表面参数**,
// ⚠️ 自 0.9.x 起:输出语言 / 预设模式 / 预设句 / latent类型 已上提为**节点表面参数**,
// 不再存在这份 JSON 里;JSON 只留元素面板配置 + 自动保存开关。
return {
auto_save: true,
@@ -1671,7 +1671,7 @@ function pickOpt(options, value, def) {
const v = (value == null) ? "" : String(value).trim();
return options.includes(v) ? v : def;
}
/** 步长锁定(与后端 _round_step 一致:floor(v/step + 0.5);step/min/max 由空latent类型决定) */
/** 步长锁定(与后端 _round_step 一致:floor(v/step + 0.5);step/min/max 由latent类型决定) */
function roundStep(v, step = SIZE_STEP, minSize = SIZE_MIN, maxSize = SIZE_MAX) {
const n = Math.floor((Number(v) || 0) / step + 0.5) * step;
return Math.max(minSize, Math.min(maxSize, n));
@@ -2653,7 +2653,7 @@ function setupNode(node) {
};
hideInternal();
// ── 「空latent」输出没连线时:空latent类型 / 画幅比例 / 宽度 / 高度 / 生成数量 全部变灰不可操作
// ── 「空latent」输出没连线时:latent类型 / 画幅比例 / 宽度 / 高度 / 生成数量 全部变灰不可操作
// (接了「空latent」输出连线后自动恢复;后端取值不受影响)
const refreshLatentGuard = installLatentGuard(node, ["latent_kind", "aspect_ratio", "width", "height", "batch_size"]);
@@ -2725,7 +2725,7 @@ function setupNode(node) {
kind: pickOpt(LATENT_KINDS, readWidgetValue(wKind), DEFAULT_LATENT_KIND),
presetText: String(readWidgetValue(w3v) ?? ""),
});
/** 当前空latent类型对应的步长/上下限(后端按同一张表算,保证所见即所得) */
/** 当前latent类型对应的步长/上下限(后端按同一张表算,保证所见即所得) */
const sizeOpt = () => {
const sp = latentSpec(surfaceOf().kind);
return { step: sp.step, min: sp.min, max: sp.max };
@@ -2839,19 +2839,19 @@ function setupNode(node) {
node.setDirtyCanvas?.(true, true);
};
// 空latent类型 → 同步 width/height/batch 的原生 step & 上下限(箭头/拖动按官方步长走)
// latent类型 → 同步 width/height/batch 的原生 step & 上下限(箭头/拖动按官方步长走)
const applyKindLimits = () => {
const sp = latentSpec(surfaceOf().kind);
for (const w of [wW, wH]) {
if (w && w.options) { w.options.step = w.options.step2 = sp.step; w.options.min = sp.min; w.options.max = sp.max; }
}
if (wB && wB.options) wB.options.max = sp.batchMax;
if (wKind && wKind.options) wKind.options.tooltip = `空latent类型(${sp.desc})\n尺寸步长 ${sp.step},batch 上限 ${sp.batchMax}。\n选你正在用的模型即可,形状/下采样/步长会自动适配。`;
if (wKind && wKind.options) wKind.options.tooltip = `latent类型(${sp.desc})\n尺寸步长 ${sp.step},batch 上限 ${sp.batchMax}。\n选你正在用的模型即可,形状/下采样/步长会自动适配。`;
const b = parseInt(readWidgetValue(wB), 10) || 1;
if (b > sp.batchMax) setWidgetValue(wB, sp.batchMax);
};
// ── 画幅比例 + 步长联动(语义对齐 js/xb_video.js;步长按当前空latent类型) ──
// ── 画幅比例 + 步长联动(语义对齐 js/xb_video.js;步长按当前latent类型) ──
let syncing = false;
const snapByWidth = () => {
if (syncing) return;
@@ -2976,79 +2976,45 @@ function setupNode(node) {
node._xbDomWidget = widget;
node._xbSyncWidth = syncDomWidth; // 供 canvas.onDrawForeground 每帧调用
// 尺寸刷新 —— 照抄短剧导演台 `refreshSize`:
// ① 用 LiteGraph 自己的 computeSize() 算高度(不读任何 DOM 尺寸 → 无反馈环)
// ② 只做「下限保护」:未设置 / 小于下限时才 setSize,绝不覆盖用户拖拽出来的高度
// ③ 只在「初始化(3 次) + 工作流加载(configure)」调用,打字 / 面板保存 / onResize 一律不调
// ⚠️ 这就是「整个界面卡顿」的隔离带:一旦在打字或 onResize 里调它,
// setSize → onResize → setSize 会形成抖动环,每帧整画布重绘 → 全界面卡。
// 下限 = 标题 + 各可见 widget 实际高度(多行 STRING 会更高)+ DOM 固定行 + 提示词框最小高度。
// 这样 4 个或 8 个表面参数(三视图预设句框显/隐)都自适应,不用手写魔数。
const widgetRowH = (w) => {
try {
const s = w && w.computeSize ? w.computeSize(node.size?.[0] || 420) : null;
if (Array.isArray(s) && Number.isFinite(s[1]) && s[1] > 0) return s[1];
} catch (_) {}
return (w && (w.type === "hidden" || w.hidden)) ? 0 : 26;
};
const minNodeHeight = () => {
let h = 30; // 节点标题
for (const w of (node.widgets || [])) h += widgetRowH(w);
return Math.max(420, Math.round(h + DOM_FIXED_H + TEXT_MIN_H));
};
refreshSize = () => {
try {
lockMinWidth();
syncDomWidth();
const size = node.computeSize?.();
const computed = (Array.isArray(size) && Number.isFinite(size[1])) ? size[1] : 0;
const need = Math.max(computed, minNodeHeight()); // 取「原生计算」与「DOM 需求」的较大值
if (!node.size || !Number.isFinite(node.size[1]) || node.size[1] < need) {
node.setSize?.([node.size?.[0] || MIN_W, need]);
}
} catch (_) {}
node.setDirtyCanvas?.(true, true);
};
// ⚠️ 不能在这里同步跑 refreshSize:工作流加载时 configure 还没应用存档尺寸,
// 先按默认 widget 算出来的高度会让节点「长高一下」,随后被存档高度拉回 → 刷新瞬间高度闪烁。
// 排到本轮任务末尾(configure 已落定)再做下限保护。
setTimeout(refreshSize, 0);
setTimeout(refreshSize, 60);
setTimeout(refreshSize, 1200); // 兜底:加载工作流后 widget 才铺完时再校正一次(只增不减)
// 尺寸刷新 —— 用「常量 + 白名单行数」计算,绝不读 widget 运行时状态
// ⚠️ 历史坑(已由 installStableNodeHeight 彻底修好):以前用 node.computeSize() +
// 遍历 node.widgets 求和,工作流加载/切图时 widget 被重建、hidden 标记短暂丢失,
// 会把「全部 widget 可见」的巨大高度一次固化下来(且只增不减)→ 节点偶发自动变高。
const SURFACE_ROWS = 7; // 表面可见参数:latent类型/输出语言/画幅比例/宽度/高度/生成数量/启用LLM反推
const stableH = installStableNodeHeight(node, {
minWidth: MIN_W,
rows: SURFACE_ROWS,
domFixedH: DOM_FIXED_H,
textMinH: TEXT_MIN_H,
getDomWidget: () => widget,
});
const minNodeHeight = () => stableH.measure();
refreshSize = () => { stableH.align(); syncDomWidth(); };
node._xbRefreshSize = refreshSize;
node._xbEl = container; // 调试/自动化定位用
// Nodes 2.0:面板里的提示词框不会自己长高(节点高度由 DOM 内容主导,flex:1 停在 min-height)
// → 默认 300px;用户拖节点尺寸手柄时按指针位移实时跟随(两个方向都跟手);
// 切回经典模式自动还原原样,交回原有 flex 布局。
// ⚠️ 不能只把「提示词框」钉成固定 px:节点拖高后容器高度仍由内容决定 ⇒ 框下方留一片空白
// (用户报障)。这里改钉**容器**:高度 = `--node-height`(前端自带的 CSS 变量)− 实测 chrome,
// 容器内的 flex 链(标签 + 提示词框 flex:1)自然把框撑到节点下边缘。
try {
node._xbUninstallNodeFill = bindNodes2NodeFill(node, container, { min: DOM_MIN_H });
} catch (e) { console.warn("[XB-生图提示词预设] 2.0 高度跟随安装失败", e); }
// ⚠️ 不再使用 bindNodes2NodeFill:它会按 `calc(--node-height - 实测 top - PAD)` 写死容器高度,
// 实测 top 一旦偏小,容器就会被算得比可用空间大 ⇒ DOM 撑破节点 ⇒ 视觉底边失控 / 拖拽弹跳。
// 容器自身的 `height:100%` + 内部 flex 链已经能把提示词框撑到节点下边缘,无需额外干预。
// 节点 resize:立即同步 overlay 宽度 + 重绘;防抖后只校正「最小宽度 / 最小高度」。
// ⚠️ 高度下限是硬约束(输入框永远 ≥500px):拖小节点时一次性拉回,
// 拉回后尺寸已等于下限 → 不会再次触发 setSize,不会形成抖动环。
// 节点 resize:只同步 overlay 宽度 + 重绘。
// 高度下限/用户高度由 installStableNodeHeight 内部处理(区分「用户拖拽」与「程序写入」),
// 这里不再调 setSize,避免与之打架形成抖动环。
let _xbResizeTimer = null;
let _xbClamping = false;
const prevOnResize = node.onResize;
node.onResize = function (...args) {
const rr = prevOnResize?.apply(this, args);
try {
syncDomWidth();
if (!_xbClamping) {
const need = minNodeHeight();
const cur = Number.isFinite(node.size?.[1]) ? node.size[1] : 0;
if (cur < need) {
_xbClamping = true;
try { node.setSize?.([node.size?.[0] || MIN_W, need]); } finally { _xbClamping = false; }
}
}
node.setDirtyCanvas?.(true, true);
clearTimeout(_xbResizeTimer);
_xbResizeTimer = setTimeout(() => { lockMinWidth(); syncDomWidth(); node.setDirtyCanvas?.(true, true); }, 200);
_xbResizeTimer = setTimeout(() => {
lockMinWidth();
syncDomWidth();
// 松手后补一次网格对齐:原生拖拽按网格步进 size,但视觉底边还要加 chrome 才压线
try { stableH.align(); } catch (_) {}
node.setDirtyCanvas?.(true, true);
}, 200);
} catch (_) {}
return rr;
};
@@ -3090,7 +3056,7 @@ function setupNode(node) {
if (MODES.includes(out.preset_mode)) setWidgetValue(wMode, out.preset_mode);
if (typeof out.three_view_text === "string" && out.three_view_text.trim()) setWidgetValue(w3v, out.three_view_text);
if (String(out.latent_channels || "").startsWith("16")) setWidgetValue(wKind, LATENT_KINDS[1]);
notify("已把旧版「输出设置」里的语言/模式/预设句/空latent类型迁到节点表面参数", "success");
notify("已把旧版「输出设置」里的语言/模式/预设句/latent类型迁到节点表面参数", "success");
}
if (out !== undefined) {
delete data.output;
@@ -3136,11 +3102,14 @@ function setupNode(node) {
const boot = () => {
hideInternal(); // 工作流加载后 widget 可能被重建 → 重新藏
// ⚠️ 关键时序:必须先 hideInternal()(内部字段全部藏好)再 markReady(),
// 否则「隐藏还没打上」的那一拍会把全可见高度固化下来(旧的偶发变高就是这样产生的)。
try { stableH.markReady(); } catch (_) {}
repairComboWidgets(); // 老工作流残留的下拉坏值 → 改回默认(后端也不再报错)
refreshLatentGuard(); // 「空latent」未连线 → 空latent类型/画幅/宽高/数量变灰
refreshLatentGuard(); // 「空latent」未连线 → latent类型/画幅/宽高/数量变灰
migrateLegacyOutput();
syncPromptBoxFromWidget();
applyKindLimits(); // 按当前空latent类型同步 step / 上下限
applyKindLimits(); // 按当前latent类型同步 step / 上下限
applyPresetTextVisibility();
// 设定词:以「用户存档 → 当前模版默认」为准
// · 空 → 当前模版默认(无预设 → 默认就是空 → 保持空);
@@ -3225,7 +3194,7 @@ const IO_T2I_ONLY = "文生图";
const XBR_SEED_LABEL = { randomize: "随机", fixed: "固定", increment: "增加", decrement: "减少" };
const XBR_PANEL_BUTTONS = [
{ id: "llm", label: "🤖 LLM设置", title: "🤖 LLM设置", subtitle: "LLM 反推的后端与推理配置;本弹窗里的「输出语言」是全节点唯一的语言设置(决定词表加载语言 + 最终提示词语言)。" },
{ id: "preset", label: "✨ 预设参数", title: "✨ 预设参数", subtitle: "设定词(含预设词工具)/ 提示词增强(设定词来源:skill 或 预设词)。空latent类型与输出语言在节点表面。" },
{ id: "preset", label: "✨ 预设参数", title: "✨ 预设参数", subtitle: "设定词(含预设词工具)/ 提示词增强(设定词来源:skill 或 预设词)。latent类型与输出语言在节点表面。" },
];
const XBR_API_ENDPOINT = "/xb_toolbox/llm_api_settings";
@@ -4396,7 +4365,7 @@ function xbrRenderHelp(body) {
"【节点表面】",
" · 第 1 行按钮:🤖 LLM设置 | ✨ 预设参数",
" · 第 2 行按钮:🎨 风格 | 📐 视角 | 👤 主体 | 🕺 姿态 | 👗 装扮 | 🎒 道具 | 💡 光影 | 🏞️ 背景",
" · 画幅比例 / 宽度 / 高度 / 生成数量:数值会按当前空latent类型的官方步长自动对齐",
" · 画幅比例 / 宽度 / 高度 / 生成数量:数值会按当前latent类型的官方步长自动对齐",
" · ✅ 启用 LLM 反推:关 = 只输出拼装好的提示词,不加载模型、不调 API",
" · 提示词框右上角「复制文本」;下面是参数设定显示,实时告诉你当前是什么配置",
"",
+48 -79
View File
@@ -10,7 +10,7 @@
* XB-BOX - 🖼️ 生图提示词预设 — 前端面板
* ============================================================
* 节点:XB_ImagePromptPreset(后端 nodes_image_prompt_preset.py,V1 经典 API)
* 表面:空latent类型 / 输出语言 / 预设模式 / (三视图/四视图/五视图预设句)/ 画幅比例 / 宽度 / 高度 / 生成数量
* 表面:latent类型 / 输出语言 / 预设模式 / (三视图/四视图/五视图预设句)/ 画幅比例 / 宽度 / 高度 / 生成数量
* + 节点提示词框 + 8 个分类按钮(4 列 × 2 行):🏷️ 风格 | 🎥 视角 | 👤 主体 | 🎬 姿态 | 👚 装扮 | 🎒 道具 | 💡 光影 | 🏞️ 背景
* 每个按钮一个面板,面板顶部「分类签」切子类;选项行整行可点(加入 → 变蓝 ✔ / 再点移除)
* 道具类点行会先弹「与主体的关系」多宫格标签窗(30 个关系词)
@@ -29,7 +29,7 @@
*/
import { app } from "../../scripts/app.js";
import { bindNodes2NodeFill } from "./xb_compat.js";
import { installStableNodeHeight } from "./xb_compat.js";
const NODE_TYPE = "XB_QwenPromptPreset";
@@ -104,7 +104,7 @@ function modeHasImage(node) {
const ASPECT_MAP = { "1:1": 1, "16:9": 16 / 9, "9:16": 9 / 16, "4:3": 4 / 3, "3:4": 3 / 4, "21:9": 21 / 9, "2:3": 2 / 3, "3:2": 3 / 2 };
const SIZE_STEP = 16; // width/height 的兜底步长(实际以「空latent类型」的官方最小步长为准)
const SIZE_STEP = 16; // width/height 的兜底步长(实际以「latent类型」的官方最小步长为准)
const SIZE_MIN = 16;
const SIZE_MAX = 16384;
const BATCH_MAX = 4096;
@@ -1607,7 +1607,7 @@ function bindWheelZoom(root) {
* 配置读写
* ============================================================ */
function defaultSettings() {
// ⚠️ 自 0.9.x 起:输出语言 / 预设模式 / 预设句 / 空latent类型 已上提为**节点表面参数**,
// ⚠️ 自 0.9.x 起:输出语言 / 预设模式 / 预设句 / latent类型 已上提为**节点表面参数**,
// 不再存在这份 JSON 里;JSON 只留元素面板配置 + 自动保存开关。
return {
auto_save: true,
@@ -1768,7 +1768,7 @@ function pickOpt(options, value, def) {
const v = (value == null) ? "" : String(value).trim();
return options.includes(v) ? v : def;
}
/** 步长锁定(与后端 _round_step 一致:floor(v/step + 0.5);step/min/max 由空latent类型决定) */
/** 步长锁定(与后端 _round_step 一致:floor(v/step + 0.5);step/min/max 由latent类型决定) */
function roundStep(v, step = SIZE_STEP, minSize = SIZE_MIN, maxSize = SIZE_MAX) {
const n = Math.floor((Number(v) || 0) / step + 0.5) * step;
return Math.max(minSize, Math.min(maxSize, n));
@@ -2863,7 +2863,7 @@ function setupNode(node) {
kind: pickOpt(LATENT_KINDS, readWidgetValue(wKind), DEFAULT_LATENT_KIND),
presetText: String(readWidgetValue(w3v) ?? ""),
});
/** 当前空latent类型对应的步长/上下限(后端按同一张表算,保证所见即所得) */
/** 当前latent类型对应的步长/上下限(后端按同一张表算,保证所见即所得) */
const sizeOpt = () => {
const sp = latentSpec(surfaceOf().kind);
return { step: sp.step, min: sp.min, max: sp.max };
@@ -2977,19 +2977,19 @@ function setupNode(node) {
node.setDirtyCanvas?.(true, true);
};
// 空latent类型 → 同步 width/height/batch 的原生 step & 上下限(箭头/拖动按官方步长走)
// latent类型 → 同步 width/height/batch 的原生 step & 上下限(箭头/拖动按官方步长走)
const applyKindLimits = () => {
const sp = latentSpec(surfaceOf().kind);
for (const w of [wW, wH]) {
if (w && w.options) { w.options.step = w.options.step2 = sp.step; w.options.min = sp.min; w.options.max = sp.max; }
}
if (wB && wB.options) wB.options.max = sp.batchMax;
if (wKind && wKind.options) wKind.options.tooltip = `空latent类型(${sp.desc})\n尺寸步长 ${sp.step},batch 上限 ${sp.batchMax}。\n选你正在用的模型即可,形状/下采样/步长会自动适配。`;
if (wKind && wKind.options) wKind.options.tooltip = `latent类型(${sp.desc})\n尺寸步长 ${sp.step},batch 上限 ${sp.batchMax}。\n选你正在用的模型即可,形状/下采样/步长会自动适配。`;
const b = parseInt(readWidgetValue(wB), 10) || 1;
if (b > sp.batchMax) setWidgetValue(wB, sp.batchMax);
};
// ── 画幅比例 + 步长联动(语义对齐 js/xb_video.js;步长按当前空latent类型) ──
// ── 画幅比例 + 步长联动(语义对齐 js/xb_video.js;步长按当前latent类型) ──
let syncing = false;
const snapByWidth = () => {
if (syncing) return;
@@ -3113,79 +3113,45 @@ function setupNode(node) {
node._xbDomWidget = widget;
node._xbSyncWidth = syncDomWidth; // 供 canvas.onDrawForeground 每帧调用
// 尺寸刷新 —— 照抄短剧导演台 `refreshSize`:
// ① 用 LiteGraph 自己的 computeSize() 算高度(不读任何 DOM 尺寸 → 无反馈环)
// ② 只做「下限保护」:未设置 / 小于下限时才 setSize,绝不覆盖用户拖拽出来的高度
// ③ 只在「初始化(3 次) + 工作流加载(configure)」调用,打字 / 面板保存 / onResize 一律不调
// ⚠️ 这就是「整个界面卡顿」的隔离带:一旦在打字或 onResize 里调它,
// setSize → onResize → setSize 会形成抖动环,每帧整画布重绘 → 全界面卡。
// 下限 = 标题 + 各可见 widget 实际高度(多行 STRING 会更高)+ DOM 固定行 + 提示词框最小高度。
// 这样 4 个或 8 个表面参数(三视图预设句框显/隐)都自适应,不用手写魔数。
const widgetRowH = (w) => {
try {
const s = w && w.computeSize ? w.computeSize(node.size?.[0] || 420) : null;
if (Array.isArray(s) && Number.isFinite(s[1]) && s[1] > 0) return s[1];
} catch (_) {}
return (w && (w.type === "hidden" || w.hidden)) ? 0 : 26;
};
const minNodeHeight = () => {
let h = 30; // 节点标题
for (const w of (node.widgets || [])) h += widgetRowH(w);
return Math.max(420, Math.round(h + DOM_FIXED_H + TEXT_MIN_H));
};
refreshSize = () => {
try {
lockMinWidth();
syncDomWidth();
const size = node.computeSize?.();
const computed = (Array.isArray(size) && Number.isFinite(size[1])) ? size[1] : 0;
const need = Math.max(computed, minNodeHeight()); // 取「原生计算」与「DOM 需求」的较大值
if (!node.size || !Number.isFinite(node.size[1]) || node.size[1] < need) {
node.setSize?.([node.size?.[0] || MIN_W, need]);
}
} catch (_) {}
node.setDirtyCanvas?.(true, true);
};
// ⚠️ 不能在这里同步跑 refreshSize:工作流加载时 configure 还没应用存档尺寸,
// 先按默认 widget 算出来的高度会让节点「长高一下」,随后被存档高度拉回 → 刷新瞬间高度闪烁。
// 排到本轮任务末尾(configure 已落定)再做下限保护。
setTimeout(refreshSize, 0);
setTimeout(refreshSize, 60);
setTimeout(refreshSize, 1200); // 兜底:加载工作流后 widget 才铺完时再校正一次(只增不减)
// 尺寸刷新 —— 用「常量 + 白名单行数」计算,绝不读 widget 运行时状态
// ⚠️ 历史坑(已由 installStableNodeHeight 彻底修好):以前用 node.computeSize() +
// 遍历 node.widgets 求和,工作流加载/切图时 widget 被重建、hidden 标记短暂丢失,
// 会把「全部 widget 可见」的巨大高度一次固化下来(且只增不减)→ 节点偶发自动变高。
const SURFACE_ROWS = 4; // 表面可见参数:画幅比例/宽度/高度/生成数量(其余全在弹窗里)
const stableH = installStableNodeHeight(node, {
minWidth: MIN_W,
rows: SURFACE_ROWS,
domFixedH: DOM_FIXED_H,
textMinH: TEXT_MIN_H,
getDomWidget: () => widget,
});
const minNodeHeight = () => stableH.measure();
refreshSize = () => { stableH.align(); syncDomWidth(); };
node._xbRefreshSize = refreshSize;
node._xbEl = container; // 调试/自动化定位用
// Nodes 2.0:面板里的提示词框不会自己长高(节点高度由 DOM 内容主导,flex:1 停在 min-height)
// → 默认 300px;用户拖节点尺寸手柄时按指针位移实时跟随(两个方向都跟手);
// 切回经典模式自动还原原样,交回原有 flex 布局。
// ⚠️ 不能只把「提示词框」钉成固定 px:节点拖高后容器高度仍由内容决定 ⇒ 框下方留一片空白
// (用户报障)。这里改钉**容器**:高度 = `--node-height`(前端自带的 CSS 变量)− 实测 chrome,
// 容器内的 flex 链(标签 + 提示词框 flex:1)自然把框撑到节点下边缘。
try {
node._xbUninstallNodeFill = bindNodes2NodeFill(node, container, { min: DOM_MIN_H });
} catch (e) { console.warn("[XB-生图提示词预设] 2.0 高度跟随安装失败", e); }
// ⚠️ 不再使用 bindNodes2NodeFill:它会按 `calc(--node-height - 实测 top - PAD)` 写死容器高度,
// 实测 top 一旦偏小,容器就会被算得比可用空间大 ⇒ DOM 撑破节点 ⇒ 视觉底边失控 / 拖拽弹跳。
// 容器自身的 `height:100%` + 内部 flex 链已经能把提示词框撑到节点下边缘,无需额外干预。
// 节点 resize:立即同步 overlay 宽度 + 重绘;防抖后只校正「最小宽度 / 最小高度」。
// ⚠️ 高度下限是硬约束(输入框永远 ≥500px):拖小节点时一次性拉回,
// 拉回后尺寸已等于下限 → 不会再次触发 setSize,不会形成抖动环。
// 节点 resize:只同步 overlay 宽度 + 重绘。
// 高度下限/用户高度由 installStableNodeHeight 内部处理(区分「用户拖拽」与「程序写入」),
// 这里不再调 setSize,避免与之打架形成抖动环。
let _xbResizeTimer = null;
let _xbClamping = false;
const prevOnResize = node.onResize;
node.onResize = function (...args) {
const rr = prevOnResize?.apply(this, args);
try {
syncDomWidth();
if (!_xbClamping) {
const need = minNodeHeight();
const cur = Number.isFinite(node.size?.[1]) ? node.size[1] : 0;
if (cur < need) {
_xbClamping = true;
try { node.setSize?.([node.size?.[0] || MIN_W, need]); } finally { _xbClamping = false; }
}
}
node.setDirtyCanvas?.(true, true);
clearTimeout(_xbResizeTimer);
_xbResizeTimer = setTimeout(() => { lockMinWidth(); syncDomWidth(); node.setDirtyCanvas?.(true, true); }, 200);
_xbResizeTimer = setTimeout(() => {
lockMinWidth();
syncDomWidth();
// 松手后补一次网格对齐:原生拖拽按网格步进 size,但视觉底边还要加 chrome 才压线
try { stableH.align(); } catch (_) {}
node.setDirtyCanvas?.(true, true);
}, 200);
} catch (_) {}
return rr;
};
@@ -3227,7 +3193,7 @@ function setupNode(node) {
if (MODES.includes(out.preset_mode)) setWidgetValue(wMode, out.preset_mode);
if (typeof out.three_view_text === "string" && out.three_view_text.trim()) setWidgetValue(w3v, out.three_view_text);
if (String(out.latent_channels || "").startsWith("16")) setWidgetValue(wKind, LATENT_KINDS[1]);
notify("已把旧版「输出设置」里的语言/模式/预设句/空latent类型迁到节点表面参数", "success");
notify("已把旧版「输出设置」里的语言/模式/预设句/latent类型迁到节点表面参数", "success");
}
if (out !== undefined) {
delete data.output;
@@ -3239,9 +3205,12 @@ function setupNode(node) {
// ── 载入 / 执行后的对齐 ──
const boot = () => {
hideInternal(); // 工作流加载后 widget 可能被重建 → 重新藏
// ⚠️ 关键时序:必须先 hideInternal()(内部字段全部藏好)再 markReady(),
// 否则「隐藏还没打上」的那一拍会把全可见高度固化下来(旧的偶发变高就是这样产生的)。
try { stableH.markReady(); } catch (_) {}
migrateLegacyOutput();
syncPromptBoxFromWidget();
applyKindLimits(); // 按当前空latent类型同步 step / 上下限
applyKindLimits(); // 按当前latent类型同步 step / 上下限
applyPresetTextVisibility();
// 设定词:以「用户存档 → 当前模版默认」为准
// · 空 → 当前模版默认(无预设 → 默认就是空 → 保持空);
@@ -3319,7 +3288,7 @@ const XBR_MTP_MODES = ["auto", "off", "2", "3", "4", "5"];
const XBR_LLM_MODES = ["启用LLM增强", "关闭LLM增强"];
const XBR_PANEL_BUTTONS = [
{ id: "llm", label: "🤖 LLM设置", title: "🤖 LLM设置", subtitle: "顶部 = LLM 增强总开关(默认启用);模型选择 = 官方 Load CLIP;参数设置 = 官方 Generate Text 全量参数。" },
{ id: "preset", label: "✨ 预设参数", title: "✨ 预设参数", subtitle: "输出语言 / 空latent类型 / 预设模式 / 设定词 / SKILL。提示词正文在节点上的提示词框里编辑。" },
{ id: "preset", label: "✨ 预设参数", title: "✨ 预设参数", subtitle: "输出语言 / latent类型 / 预设模式 / 设定词 / SKILL。提示词正文在节点上的提示词框里编辑。" },
];
/** 全部收进弹窗的节点 widget(节点表面只留画幅参数 / 生成数量) */
const XBR_QWEN_WIDGETS = [
@@ -3615,8 +3584,8 @@ function xbrRenderPreset(body, node, draft, ctx) {
ctx.rerender();
})));
body.append(xbrHint("输出语言决定:词表与设定词按哪种语言加载、生成文本是中文还是英文。"));
body.append(field("空latent类型", selectControl(LATENT_KINDS, draft.kind, (v) => { draft.kind = v; })));
body.append(xbrHint("空latent类型:选你正在用的模型即可(形状 / 下采样 / 尺寸步长自动适配 8/16/32)。"));
body.append(field("latent类型", selectControl(LATENT_KINDS, draft.kind, (v) => { draft.kind = v; })));
body.append(xbrHint("latent类型:选你正在用的模型即可(形状 / 下采样 / 尺寸步长自动适配 8/16/32)。"));
// ── 模版(文生图 / 图生图):决定用哪一套设定词(每个预设模式都有两套)──
body.append(field("模版", radioRow(XBR_IO_MODES, draft.ioMode, (v) => {
draft.ioMode = v;
@@ -3737,11 +3706,11 @@ function xbrRenderHelp(body) {
" 2. 送模型 = 【参考设定·本档设定词】+【正文】",
" (选中 SKILL 时,SKILL 全文另作为系统提示词送到最前面)",
" 3. 输出提示词 = 本档设定词原封不动置顶 + 模型生成的正文",
" 4. 输出空latent = 按空latent类型/画幅参数生成的空 latent",
" 4. 输出空latent = 按latent类型/画幅参数生成的空 latent",
"",
"【三步上手 · 文生图】",
" 1. 🤖 LLM设置 选模型(类型 qwen_image,设备 default)",
" 2. ✨ 预设参数 选输出语言 + 空latent类型 + 预设模式",
" 2. ✨ 预设参数 选输出语言 + latent类型 + 预设模式",
" (文生图组)+ SKILL 模式(保持自动即可)",
" 3. 📊 空latent 接采样器、📝 提示词 接文本编码器,点运行",
"",
@@ -3757,7 +3726,7 @@ function xbrRenderHelp(body) {
"【节点表面】",
" · 第 1 行按钮:🤖 LLM设置 | ✨ 预设参数",
" · 第 2 行按钮:🎨 风格 | 📐 视角 | 👤 主体 | 🕺 姿态 | 👗 装扮 | 🎒 道具 | 💡 光影 | 🏞️ 背景",
" · 画幅比例 / 宽度 / 高度 / 生成数量:按空latent类型的官方步长自动对齐",
" · 画幅比例 / 宽度 / 高度 / 生成数量:按latent类型的官方步长自动对齐",
" · 提示词框右上角 📋 复制;下面是参数设定显示,实时告诉你当前配置",
" · 开关:🤖 LLM设置 顶部「启用LLM增强 / 关闭LLM增强」(默认启用);",
" 关闭后执行节点只用拼装好的提示词,不加载文本编码器、不生成文本",
@@ -3795,7 +3764,7 @@ function xbrRenderHelp(body) {
" · 这两个技能要求输出单行 JSON:正常,见【常见问题】",
"",
"【✨ 预设参数】",
" · 生图预设:输出语言 / 空latent类型 / 预设模式 / 设定词",
" · 生图预设:输出语言 / latent类型 / 预设模式 / 设定词",
" · SKILL 模式(自动 / 手动 / 不用):见上",
" · 设定词:见上(无预设档则不前置)",
"",