Files
EricRollei-Local_LLM_Prompt…/core/builder/layout.py
T
Eric HissandCopilot 373ea8bda3 v2.2.0: Prompt Builder nodes, wildcard review tool, docs refresh
- 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>
2026-10-28 20:36:44 -07:00

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