- Add Wildcard Prompt Composer and Wildcard Character Composer builder nodes (core/builder, web/prompt_builder.js, builders/, vp wildcard lists) - Add browser-based wildcard review tool (tools/wildcard_review) with manifest-based undo - Add wildcard catalog/curation, surreal compiler/lint/scene, profile token budget - README: document all 18 nodes, bump to v2.2.0, fix stale node/platform lists; CHANGELOG 2.2.0 - Rewrite NODE_COMPARISON for 18 nodes; fix QUICKSTART (dead test_setup.py ref, menu path) and QUICK_REFERENCE node list - gitignore review manifests and CLAUDE.md Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
141 lines
5.5 KiB
Python
141 lines
5.5 KiB
Python
"""
|
|
Your arrangement of the slot buttons: builders_user/button_layout.yaml, written by the panel's
|
|
"Arrange" mode and applied to every builder after the shipped YAML and my_buttons.yaml.
|
|
|
|
moved: # a button shown in another section and/or under another group heading
|
|
shot: {section: composition, group: ""}
|
|
order: # button order inside a section (ids not listed keep their place, after these)
|
|
setting: [location, background, landmark]
|
|
hidden: [nose, lips]
|
|
|
|
The shipped builders/*.yaml is never changed; "Reset layout" deletes this file.
|
|
|
|
No ComfyUI dependency.
|
|
|
|
(c) Eric Hiss. Dual license, see LICENSE.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
from typing import Dict, List, Optional
|
|
|
|
try:
|
|
import yaml
|
|
except ImportError: # pragma: no cover - PyYAML ships with ComfyUI
|
|
yaml = None
|
|
|
|
LAYOUT_FILE = "button_layout.yaml"
|
|
|
|
|
|
def empty_layout() -> Dict:
|
|
return {"moved": {}, "order": {}, "hidden": []}
|
|
|
|
|
|
def read_layout(path: str) -> Dict:
|
|
"""The layout file, cleaned. Missing or broken -> an empty layout."""
|
|
layout = empty_layout()
|
|
if yaml is None or not path or not os.path.isfile(path):
|
|
return layout
|
|
try:
|
|
with open(path, "r", encoding="utf-8") as handle:
|
|
data = yaml.safe_load(handle) or {}
|
|
except Exception as error:
|
|
print(f"[Wildcard Composer] skipped {path}: {error}")
|
|
return layout
|
|
if not isinstance(data, dict):
|
|
return layout
|
|
for slot_id, move in (data.get("moved") or {}).items():
|
|
if isinstance(move, dict):
|
|
entry = {}
|
|
if move.get("section"):
|
|
entry["section"] = str(move["section"]).strip()
|
|
if "group" in move:
|
|
entry["group"] = str(move.get("group") or "").strip()
|
|
if entry:
|
|
layout["moved"][str(slot_id)] = entry
|
|
for section, ids in (data.get("order") or {}).items():
|
|
if isinstance(ids, list):
|
|
layout["order"][str(section)] = [str(slot_id) for slot_id in ids if slot_id]
|
|
layout["hidden"] = sorted({str(slot_id) for slot_id in (data.get("hidden") or []) if slot_id})
|
|
return layout
|
|
|
|
|
|
def write_layout(path: str, layout: Dict) -> None:
|
|
if yaml is None:
|
|
raise RuntimeError("PyYAML is not installed")
|
|
os.makedirs(os.path.dirname(path) or ".", exist_ok=True)
|
|
header = ("# Your arrangement of the Wildcard Composer buttons (the panel's Arrange mode writes it).\n"
|
|
"# Delete this file, or use Reset layout, to get the shipped layout back.\n")
|
|
clean = {"moved": layout.get("moved") or {}, "order": layout.get("order") or {},
|
|
"hidden": sorted(set(layout.get("hidden") or []))}
|
|
temp = path + ".tmp"
|
|
with open(temp, "w", encoding="utf-8") as handle:
|
|
handle.write(header + yaml.safe_dump(clean, sort_keys=True, allow_unicode=True, width=120))
|
|
os.replace(temp, path)
|
|
|
|
|
|
def apply_layout(builders: Dict, layout: Dict) -> None:
|
|
"""Move, regroup, hide and reorder the slots of every builder (in place)."""
|
|
moved = layout.get("moved") or {}
|
|
hidden = set(layout.get("hidden") or [])
|
|
order = layout.get("order") or {}
|
|
for builder in builders.values():
|
|
for slot in builder.slots:
|
|
move = moved.get(slot.id)
|
|
if move:
|
|
slot.section = move.get("section", slot.section)
|
|
if "group" in move:
|
|
slot.group = move["group"]
|
|
slot.hidden = slot.id in hidden
|
|
positions = {section: {slot_id: index for index, slot_id in enumerate(ids)} for section, ids in order.items()}
|
|
original = {slot.id: index for index, slot in enumerate(builder.slots)}
|
|
|
|
def key(slot):
|
|
listed = positions.get(slot.section, {})
|
|
if slot.id in listed:
|
|
return (0, listed[slot.id], 0)
|
|
return (1, 0, original[slot.id])
|
|
|
|
builder.slots.sort(key=key)
|
|
|
|
|
|
def layout_move(layout: Dict, slot_id: str, section: Optional[str] = None, group: Optional[str] = None,
|
|
before: Optional[str] = None, section_ids: Optional[List[str]] = None) -> Dict:
|
|
"""
|
|
Record a move: to `section` (and `group`), placed before the button `before` (None = at the end).
|
|
section_ids: the ids currently shown in the target section, in order (from the panel), so the
|
|
new order can be written in full.
|
|
"""
|
|
layout = {"moved": dict(layout.get("moved") or {}), "order": dict(layout.get("order") or {}),
|
|
"hidden": list(layout.get("hidden") or [])}
|
|
entry = dict(layout["moved"].get(slot_id, {}))
|
|
if section:
|
|
entry["section"] = section
|
|
if group is not None:
|
|
entry["group"] = group
|
|
if entry:
|
|
layout["moved"][slot_id] = entry
|
|
if section_ids is not None and (section or entry.get("section")):
|
|
target = section or entry["section"]
|
|
ids = [i for i in section_ids if i != slot_id]
|
|
at = ids.index(before) if before in ids else len(ids)
|
|
ids.insert(at, slot_id)
|
|
layout["order"][target] = ids
|
|
for other, other_ids in list(layout["order"].items()):
|
|
if other != target and slot_id in other_ids:
|
|
layout["order"][other] = [i for i in other_ids if i != slot_id]
|
|
return layout
|
|
|
|
|
|
def layout_hide(layout: Dict, slot_id: str, hidden: bool) -> Dict:
|
|
layout = {"moved": dict(layout.get("moved") or {}), "order": dict(layout.get("order") or {}),
|
|
"hidden": list(layout.get("hidden") or [])}
|
|
ids = set(layout["hidden"])
|
|
if hidden:
|
|
ids.add(slot_id)
|
|
else:
|
|
ids.discard(slot_id)
|
|
layout["hidden"] = sorted(ids)
|
|
return layout
|