Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0d77b446c7 |
@@ -27,6 +27,7 @@
|
||||
|
||||
- **Bundle headers fit narrow nodes and the Settings preview.** On a narrow Context node, or in the Display settings' live preview, a bundle's header pushed its lock, internal and remove buttons past the frame and stretched the bundle wider than the rows around it. The frame now keeps its width: the name, the state badge and the "3 mods · 1 drifted" summary shorten first, and when the header is narrow the badge word gives way to its coloured dot (point at it for what it means).
|
||||
- **Toggle negatives show in the Assembler's negative preview.** A toggle's Negative (or Off negative), and the picked toggle's negative in Pick one, went to the negative prompt on Run but never showed in the Assembler's negative preview. The preview shows them now.
|
||||
- **A template row carries the negatives of what it reads.** A Context Injector template row like `$cloak with a silver clasp` now takes `$cloak`'s negative ("torn fabric") along, as a combine does, so an Assembler that prints `$outfit` sends it to the negative output; before, only the row's own Negative went. A WP Toggles value that reads a `$variable` does the same. The Assembler's negative preview shows them too.
|
||||
- **WP Var nodes read `$name.bool`.** WP Var → Bool (and the other Var nodes) can now read a toggle's switch as `$name.bool`. A toggle with a Value holds that value in its variable, so the **Batches and grids** example's Quick preview stopped picking the small image size. Its Var → Bool now reads `$quick_preview.bool`.
|
||||
- **WP Toggles variables reach the Assembler.** The Pick one variable (like `$time`) never showed in a downstream Prompt Assembler, even with its box ticked, so the Assembler said "No upstream variables yet". It shows now, and the box is called **Show in Assembler**. Each toggle also has a globe on its row in the edit view, on by default: it keeps the toggle's true/false variable internal, for rules only, and turning it off lets the Assembler print it, in Any on and Pick one alike. **Show** now hides the `$variable` under the name in the edit view too, so you can see what it does.
|
||||
- **Pop-up panels in the manager have their padding back.** Since the move to Tailwind 4, the manager's pop-up panels (the Test Runner's **Add to stack** picker and others built on the same panel) lost their inner padding, so the search box, kind chips, match count and Sort sat flush against the panel's edges. They have room on every side again.
|
||||
|
||||
@@ -1664,7 +1664,7 @@
|
||||
},
|
||||
{
|
||||
"title": "Join several inputs",
|
||||
"text": "Click Add template row and write a template like $input_0 by $input_1. A template row isn't tied to one input, stays when you rewire, and runs after the other rows, so it can also use the variables they made ($outfit).\nHow it works\nA row made for a wired input can only use its own input in its template. It disappears when you unplug that input. Up to 10 inputs per Injector; chain a second one for more. Text works best: images and latents turn into their text description, which is rarely useful in a prompt.\nSetting a variable here clears the negatives it had from earlier nodes; the row's own negative takes their place. A hidden row, or one that only feeds a derivation, never adds to a negative."
|
||||
"text": "Click Add template row and write a template like $input_0 by $input_1. A template row isn't tied to one input, stays when you rewire, and runs after the other rows, so it can also use the variables they made ($outfit).\nHow it works\nA row made for a wired input can only use its own input in its template. It disappears when you unplug that input. Up to 10 inputs per Injector; chain a second one for more. Text works best: images and latents turn into their text description, which is rarely useful in a prompt.\nSetting a variable here clears the negatives it had from earlier nodes; the row's own negative takes their place. A template row also carries the negatives of the variables it reads, before its own: $outfit = \"$cloak with a silver clasp\" brings $cloak's \"torn fabric\". A hidden row, or one that only feeds a derivation, never adds to a negative."
|
||||
},
|
||||
{
|
||||
"title": "Works with",
|
||||
@@ -1760,7 +1760,7 @@
|
||||
},
|
||||
{
|
||||
"title": "Use a toggle in the prompt",
|
||||
"text": "A toggle's variable is internal by default: rules read it, but the Assembler doesn't offer it, since true or false rarely belongs in a prompt. To print it, open the edit view and turn off the globe on its row. In Pick one, Show in Assembler does the same for the picked variable, so $time can print the picked toggle's Value.\nGive a toggle a Value and its variable holds that text while it is on and nothing while it is off: a Rain toggle with the Value \"heavy rain\" makes $rain print \"heavy rain\" or nothing. The switch itself is still there as $rain.bool, true or false, for rules and conditions. Its Negative goes with the variable while it is on, so an Assembler that prints $rain also sends \"sunny, dry\" to its negative output.\nIn Any on a toggle can also say what to write while it is off: its Off value and Off negative. A Buttons toggle Anime / Real with the Value \"anime style\" and the Off value \"photorealistic\" writes one or the other, and each side's negative keeps the other look out. Leave them empty and off prints nothing, as before."
|
||||
"text": "A toggle's variable is internal by default: rules read it, but the Assembler doesn't offer it, since true or false rarely belongs in a prompt. To print it, open the edit view and turn off the globe on its row. In Pick one, Show in Assembler does the same for the picked variable, so $time can print the picked toggle's Value.\nGive a toggle a Value and its variable holds that text while it is on and nothing while it is off: a Rain toggle with the Value \"heavy rain\" makes $rain print \"heavy rain\" or nothing. The switch itself is still there as $rain.bool, true or false, for rules and conditions. Its Negative goes with the variable while it is on, so an Assembler that prints $rain also sends \"sunny, dry\" to its negative output. A Value that reads a $variable brings that variable's negatives along too.\nIn Any on a toggle can also say what to write while it is off: its Off value and Off negative. A Buttons toggle Anime / Real with the Value \"anime style\" and the Off value \"photorealistic\" writes one or the other, and each side's negative keeps the other look out. Leave them empty and off prints nothing, as before."
|
||||
},
|
||||
{
|
||||
"title": "Mute or bypass nodes",
|
||||
|
||||
@@ -277,6 +277,32 @@ describe("collectUpstreamProducers", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("collectUpstreamInjectorNegatives: a template row carries the negatives of what it reads", () => {
|
||||
const inj: LiteNodeLike = {
|
||||
id: 1,
|
||||
type: "WP_ContextInjector",
|
||||
outputs: [{ name: "context", links: [100], type: "PIPELINE_CONTEXT" }],
|
||||
widgets: [{
|
||||
name: "wp_rows",
|
||||
value: JSON.stringify({
|
||||
version: 1,
|
||||
rows: [
|
||||
{ kind: "general", binding: "outfit", enabled: true, template: "$cloak, $input_0 $$x", negative: "dull" },
|
||||
{ binding: "cloak", enabled: true, slot_name: "input_0", negative: "torn fabric" },
|
||||
{ kind: "general", binding: "raw", enabled: true, template: "$input_0" },
|
||||
],
|
||||
}),
|
||||
}],
|
||||
};
|
||||
const pov = ctxWriting(2, "Other", "unused", 100);
|
||||
const g = chain([inj, pov], { 100: { origin_id: 1, target_id: 2 } });
|
||||
expect(collectUpstreamInjectorNegatives(g, pov)).toEqual({
|
||||
cloak: "torn fabric",
|
||||
outfit: "torn fabric, dull",
|
||||
raw: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("collectUpstreamInjectorNegatives: a toggle's own negative, or null when it has none", () => {
|
||||
const rows = [
|
||||
{ id: "a", kind: "toggle", name: "Rain", var: "rain", on: true, pin: false, out: 0, style: "switch", value: "in the rain", negative: "sunny sky" },
|
||||
|
||||
+34
-5
@@ -760,26 +760,55 @@ export function collectUpstreamInjectorNegatives(
|
||||
continue;
|
||||
}
|
||||
if (n.type === TOGGLES_NODE && !isSkippedMode(n)) {
|
||||
// A write replaces the variable's negatives with the toggle's own.
|
||||
for (const [v, neg] of Object.entries(staticToggleNegatives(n))) out[v] = neg || null;
|
||||
// A write replaces the variable's negatives with the toggle's own, after
|
||||
// those of the `$vars` its value reads (as the node files them).
|
||||
const values = staticToggleValues(n);
|
||||
const negs = staticToggleNegatives(n);
|
||||
const before = { ...out };
|
||||
for (const v of Object.keys(negs)) out[v] = inheritNegatives(before, values[v] ?? "", negs[v] ?? "");
|
||||
continue;
|
||||
}
|
||||
if (n.type !== "WP_ContextInjector" || isSkippedMode(n)) continue;
|
||||
const inj = parseCached<{
|
||||
version: 1;
|
||||
rows?: Array<{ binding?: string; enabled?: boolean; negative?: string }>;
|
||||
rows?: Array<{ binding?: string; enabled?: boolean; negative?: string; kind?: string; template?: string }>;
|
||||
}>(widgetValue(n, "wp_rows"), { version: 1, rows: [] });
|
||||
for (const row of inj.rows ?? []) {
|
||||
// Socket rows first, then template rows in order, as the node runs them.
|
||||
const rows = inj.rows ?? [];
|
||||
const ordered = [...rows.filter((r) => r.kind !== "general"), ...rows.filter((r) => r.kind === "general")];
|
||||
for (const row of ordered) {
|
||||
if (row.enabled !== true) continue;
|
||||
const binding = (row.binding ?? "").trim();
|
||||
if (!binding) continue;
|
||||
const neg = typeof row.negative === "string" ? row.negative.trim() : "";
|
||||
out[binding] = neg || null;
|
||||
// A template row carries the negatives of the variables it reads.
|
||||
out[binding] = row.kind === "general"
|
||||
? inheritNegatives(out, row.template ?? "", neg)
|
||||
: neg || null;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
const READ_RE = /\$\$|\$([A-Za-z_][A-Za-z0-9_]*)/g;
|
||||
const SOCKET_RE = /^input_\d+$/;
|
||||
|
||||
/** `own` after the negatives (in `known`) of each `$var` `text` reads, or
|
||||
* `null` when there are none. Raw sockets (`$input_N`) carry nothing. */
|
||||
function inheritNegatives(known: Record<string, string | null>, text: string, own: string): string | null {
|
||||
const parts: string[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (const m of text.matchAll(READ_RE)) {
|
||||
const name = m[1];
|
||||
if (!name || SOCKET_RE.test(name) || seen.has(name)) continue;
|
||||
seen.add(name);
|
||||
const got = known[name];
|
||||
if (got) parts.push(got);
|
||||
}
|
||||
if (own) parts.push(own);
|
||||
return parts.length ? parts.join(", ") : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Walk upstream from `node` and return the set of every wildcard
|
||||
* module's uuid (8-hex `id`) reachable in the chain. Used by the
|
||||
|
||||
@@ -89,8 +89,10 @@ import VarToken from "../../../components/docs/VarToken.vue";
|
||||
</DocDetails>
|
||||
<Banner tone="info" note>
|
||||
Setting a variable here clears the negatives it had from earlier nodes; the row's own
|
||||
negative takes their place. A hidden row, or one that only feeds a derivation, never adds
|
||||
to a negative.
|
||||
negative takes their place. A template row also carries the negatives of the variables it
|
||||
reads, before its own: <VarToken>$outfit</VarToken> = "$cloak with a silver clasp" brings
|
||||
<VarToken>$cloak</VarToken>'s "torn fabric". A hidden row, or one that only feeds a
|
||||
derivation, never adds to a negative.
|
||||
</Banner>
|
||||
</DocSection>
|
||||
|
||||
|
||||
@@ -85,7 +85,8 @@ import VarToken from "../../../components/docs/VarToken.vue";
|
||||
<VarToken>$rain</VarToken> print "heavy rain" or nothing. The switch itself is still there as
|
||||
<VarToken>$rain.bool</VarToken>, <code>true</code> or <code>false</code>, for rules and
|
||||
conditions. Its <b>Negative</b> goes with the variable while it is on, so an Assembler that
|
||||
prints <VarToken>$rain</VarToken> also sends "sunny, dry" to its negative output.
|
||||
prints <VarToken>$rain</VarToken> also sends "sunny, dry" to its negative output. A Value
|
||||
that reads a <VarToken>$variable</VarToken> brings that variable's negatives along too.
|
||||
</p>
|
||||
<p>
|
||||
In Any on a toggle can also say what to write while it is <b>off</b>: its <b>Off value</b>
|
||||
|
||||
@@ -405,6 +405,41 @@ def test_injector_row_negative_replaces_the_variables_negatives():
|
||||
assert upstream.internals[negatives.NEG_KEY]["character"][0]["text"] == "hat"
|
||||
|
||||
|
||||
def test_injector_template_row_carries_negatives_of_what_it_reads():
|
||||
import json
|
||||
|
||||
from wp_nodes.injector_node import WPContextInjector
|
||||
from wp_nodes.types import ContextPayload
|
||||
|
||||
upstream = ContextPayload(
|
||||
context={"hat": "a red hat"}, debug={},
|
||||
internals={negatives.NEG_KEY: {
|
||||
"hat": [{"text": "blurry hat", "pick": None, "source": "hat"}],
|
||||
}},
|
||||
)
|
||||
rows = json.dumps({"version": 1, "rows": [
|
||||
{"_uid": "a", "slot_name": "input_0", "binding": "cloak",
|
||||
"enabled": True, "template": "wearing $input_0",
|
||||
"negative": "torn fabric"},
|
||||
# Reads $cloak (a socket row) and $hat (upstream): both carry over.
|
||||
{"_uid": "b", "kind": "general", "binding": "outfit",
|
||||
"enabled": True, "template": "$cloak and $hat, $input_0"},
|
||||
# Its own negative follows the inherited ones.
|
||||
{"_uid": "c", "kind": "general", "binding": "look",
|
||||
"enabled": True, "template": "$cloak", "negative": "dull"},
|
||||
# Only raw sockets: nothing to inherit.
|
||||
{"_uid": "d", "kind": "general", "binding": "raw",
|
||||
"enabled": True, "template": "$input_0 $$cloak"},
|
||||
]})
|
||||
out = WPContextInjector.execute(
|
||||
wp_rows=rows, upstream=upstream, input_0="cloak",
|
||||
).values[0]
|
||||
table = out.internals[negatives.NEG_KEY]
|
||||
assert [e["text"] for e in table["outfit"]] == ["torn fabric", "blurry hat"]
|
||||
assert [e["text"] for e in table["look"]] == ["torn fabric", "dull"]
|
||||
assert "raw" not in table
|
||||
|
||||
|
||||
def test_trace_writes_carry_the_negative():
|
||||
ctx = _run([_wc("hair", [_opt(0, "red hair", "blonde")])])
|
||||
write = ctx["__wp_trace__"][0]["writes"][0]
|
||||
|
||||
@@ -303,3 +303,29 @@ def test_trace_details_say_why_a_toggle_is_off_and_how_pick_one_chose():
|
||||
assert pick["picked"] == "Dusk" and pick["forced_by"] == "Dusk"
|
||||
assert pick["upstream_on"] == ["Night"]
|
||||
assert trace["night"]["detail"]["upstream_read"] == "yes"
|
||||
|
||||
|
||||
def test_a_value_carries_the_negatives_of_what_it_reads():
|
||||
up = ContextPayload(
|
||||
context={"weather": "storm", "shade": "dark"}, debug={},
|
||||
internals={negatives.NEG_KEY: {
|
||||
"weather": [{"text": "clear sky", "pick": None, "source": "weather"}],
|
||||
"shade": [{"text": "bright", "pick": None, "source": "shade"}],
|
||||
}},
|
||||
)
|
||||
cfg = {"mode": "any", "rows": [
|
||||
{"id": "1", "kind": "toggle", "name": "Rain", "var": "rain", "on": True,
|
||||
"value": "heavy $weather rain", "negative": "sunny"},
|
||||
{"id": "2", "kind": "toggle", "name": "Mist", "var": "mist", "on": True,
|
||||
"value": "thin mist"},
|
||||
]}
|
||||
neg = _run(config=cfg, upstream=up).values[0].internals[negatives.NEG_KEY]
|
||||
assert [e["text"] for e in neg["rain"]] == ["clear sky", "sunny"]
|
||||
assert "mist" not in neg
|
||||
# The pick's value reads upstream $shade: its negatives come along.
|
||||
one = {"mode": "one", "group_var": "time", "rows": [
|
||||
{"id": "1", "kind": "toggle", "name": "Night", "var": "night", "on": True,
|
||||
"value": "$shade night", "negative": "noon"},
|
||||
]}
|
||||
neg = _run(config=one, upstream=up).values[0].internals[negatives.NEG_KEY]
|
||||
assert [e["text"] for e in neg["time"]] == ["bright", "noon"]
|
||||
|
||||
@@ -150,16 +150,22 @@ class WPContextInjector(io.ComfyNode):
|
||||
|
||||
def _file_row_negative(
|
||||
binding_name: str, row: dict, slots: dict[str, Any],
|
||||
reads: list[str] | None = None,
|
||||
) -> None:
|
||||
# A template row that reads `$var`s carries their negatives, as a
|
||||
# combine does (negatives follow usage), then its own Negative.
|
||||
entries = negatives.entries_for_reads(
|
||||
{negatives.NEG_KEY: neg_table}, [(n, None) for n in reads or []],
|
||||
)
|
||||
raw = row.get("negative")
|
||||
text = (
|
||||
_render_template(raw.strip(), slots).strip()
|
||||
if isinstance(raw, str) and raw.strip() else ""
|
||||
)
|
||||
if text:
|
||||
neg_table[binding_name] = [
|
||||
{"text": text, "pick": None, "source": "injector"},
|
||||
]
|
||||
entries.append({"text": text, "pick": None, "source": "injector"})
|
||||
if entries:
|
||||
neg_table[binding_name] = entries
|
||||
else:
|
||||
neg_table.pop(binding_name, None)
|
||||
|
||||
@@ -342,7 +348,14 @@ class WPContextInjector(io.ComfyNode):
|
||||
merged.update(slot_values)
|
||||
stored = _render_template(template_str, merged)
|
||||
ctx[stripped] = stored
|
||||
_file_row_negative(stripped, row, merged)
|
||||
# Variables the template read (not raw sockets: those carry no
|
||||
# negatives of their own).
|
||||
var_reads = [
|
||||
m.group(1) for m in _TEMPLATE_TOKEN_RE.finditer(template_str)
|
||||
if m.group(1) and m.group(1) not in slot_values
|
||||
and m.group(1) in merged
|
||||
]
|
||||
_file_row_negative(stripped, row, merged, var_reads)
|
||||
is_internal = bool(row.get("internal", False))
|
||||
if is_internal:
|
||||
internal_keys.add(stripped)
|
||||
|
||||
@@ -78,11 +78,15 @@ class WPToggles(io.ComfyNode):
|
||||
# then the pick, which may hold a toggle's value.
|
||||
value_warnings: list = []
|
||||
read_ctx = {**upstream_ctx, toggles.BOOLS_KEY: on_off}
|
||||
for name in [*sorted(valued - {group}), *([group] if group in values else [])]:
|
||||
value_order = [*sorted(valued - {group}), *([group] if group in values else [])]
|
||||
value_reads: dict[str, list] = {}
|
||||
for name in value_order:
|
||||
text = values[name]
|
||||
if "$" in text or "{" in text:
|
||||
value_reads[name] = []
|
||||
values[name] = template.resolve_variables(
|
||||
text, {**read_ctx, **values, "__wp_warnings__": value_warnings},
|
||||
reads=value_reads[name],
|
||||
)
|
||||
|
||||
def resolve_neg(text: str) -> str:
|
||||
@@ -115,14 +119,23 @@ class WPToggles(io.ComfyNode):
|
||||
filed = {k: v for k, v in own_negs.items() if v}
|
||||
if neg_text:
|
||||
filed[group] = neg_text
|
||||
if filed:
|
||||
out_internals[negatives.NEG_KEY] = {
|
||||
**(out_internals.get(negatives.NEG_KEY) or {}),
|
||||
**{
|
||||
k: [{"text": v, "pick": None, "source": "WP_Toggles"}]
|
||||
for k, v in filed.items()
|
||||
},
|
||||
}
|
||||
# A value that reads `$vars` carries their negatives (negatives
|
||||
# follow usage), then the toggle's own. In resolve order, so the pick
|
||||
# reading a toggle's value gets that toggle's negatives too.
|
||||
table = dict(out_internals.get(negatives.NEG_KEY) or {})
|
||||
for k, v in filed.items():
|
||||
if k not in value_reads:
|
||||
table[k] = [{"text": v, "pick": None, "source": "WP_Toggles"}]
|
||||
for k in value_order:
|
||||
entries = negatives.entries_for_reads(
|
||||
{negatives.NEG_KEY: table}, value_reads.get(k, []),
|
||||
)
|
||||
if filed.get(k):
|
||||
entries.append({"text": filed[k], "pick": None, "source": "WP_Toggles"})
|
||||
if entries:
|
||||
table[k] = entries
|
||||
if table or negatives.NEG_KEY in out_internals:
|
||||
out_internals[negatives.NEG_KEY] = table
|
||||
# `$name.bool` reads these, across nodes, whatever the variable holds.
|
||||
if on_off:
|
||||
out_internals[toggles.BOOLS_KEY] = {
|
||||
|
||||
Reference in New Issue
Block a user