B/A switch

This commit is contained in:
JJ
2026-09-09 12:27:11 +01:00
parent 948b59b8fc
commit 0dd7dac457
3 changed files with 30 additions and 22 deletions
+12 -9
View File
@@ -50,11 +50,12 @@ class PowerCompareVideo:
previous run's input (e.g. upstream was fully cached), the
saved frames are left untouched - no promotion, no rewrite -
so B keeps pointing at the last genuinely DIFFERENT video.
output_pick - "A" (default), "B", or "A/B": which video the images
output returns. With images_b connected, B is returned
losslessly; without it, B is decoded back from the cached
previous-run frames. "A/B" returns BOTH videos stitched
into a single video according to ab_stitch.
output_pick - "A" (default), "B", "A/B", or "B/A": which video the
images output returns. With images_b connected, B is
returned losslessly; without it, B is decoded back from
the cached previous-run frames. "A/B" / "B/A" return BOTH
videos stitched into a single video according to
ab_stitch ("A/B": A first - top/left; "B/A": B first).
ab_stitch - Stitch orientation for output_pick = "A/B":
"vertical" (default, A on top / B below) or
"horizontal" (A left / B right). The frontend keeps this
@@ -82,7 +83,7 @@ class PowerCompareVideo:
"optional": {
"fps": ("FLOAT", {"default": 24, "min": 1, "max": 120, "step": 1}),
"images_b": ("IMAGE",),
"output_pick": (["A", "B", "A/B"], {"default": "A"}),
"output_pick": (["A", "B", "A/B", "B/A"], {"default": "A"}),
"ab_stitch": (["vertical", "horizontal"], {"default": "vertical"}),
"start_frame": ("INT", {"default": 0, "min": 0, "max": 10000000, "step": 1}),
"end_frame": ("INT", {"default": 0, "min": 0, "max": 10000000, "step": 1}),
@@ -249,10 +250,12 @@ class PowerCompareVideo:
# is rescaled to match A along the stitch axis, aspect preserved;
# if frame counts differ the shorter video repeats its last frame
# so both play to the end (same rule as the preview).
# - pick B/A: same stitched output with the videos swapped
# (B first: top in vertical, left in horizontal).
pick = "A"
if isinstance(output_pick, str):
pick = output_pick.strip().upper()
if pick not in ("A", "B", "A/B"):
if pick not in ("A", "B", "A/B", "B/A"):
pick = "A"
horizontal = isinstance(ab_stitch, str) and ab_stitch.strip().lower() == "horizontal"
@@ -314,11 +317,11 @@ class PowerCompareVideo:
b_tensor = _crop_tensor(_decode_b_tensor())
if b_tensor is not None:
out_tensor = b_tensor
elif pick == "A/B":
elif pick in ("A/B", "B/A"):
a_tensor = _crop_tensor(images)
b_tensor = _crop_tensor(_decode_b_tensor())
if b_tensor is not None:
out_tensor = _stitch(a_tensor, b_tensor)
out_tensor = _stitch(a_tensor, b_tensor) if pick == "A/B" else _stitch(b_tensor, a_tensor)
# NOTE: every ui value must be a list - the server iterates over each
# value when merging ui outputs (scalars crash with 'float' not iterable)
+10 -7
View File
@@ -2,10 +2,11 @@
* Compare Row Widget for Power Compare Video
*
* Row above the playback area: three mode buttons on the left
* (slide-compare / B-on-right / B-on-bottom, canvas-drawn icons), then three
* pick buttons (A / B / A&B - which video the images output returns; "A/B"
* returns both videos stitched into one), and an fps stepper on the right
* (same look as PowerLoadVideo's top row).
* (slide-compare / B-on-right / B-on-bottom, canvas-drawn icons), then four
* pick buttons (A / B / A/B / B/A - which video the images output returns;
* "A/B"/"B/A" return both videos stitched into one, with the videos swapped
* for "B/A"), and an fps stepper on the right (same look as PowerLoadVideo's
* top row).
* The active compare mode lives on the node (node.compareMode) and is
* persisted via node.properties.compare_mode; the output pick lives on
* node.outputPick and is persisted via node.properties.output_pick plus the
@@ -32,6 +33,7 @@ export class PowerCompareRowWidget extends RgthreeBaseWidget {
pickA: { bounds: [0, 0, 0, 0] },
pickB: { bounds: [0, 0, 0, 0] },
pickAB: { bounds: [0, 0, 0, 0] },
pickBA: { bounds: [0, 0, 0, 0] },
fpsDec: { bounds: [0, 0, 0, 0] },
fpsVal: { bounds: [0, 0, 0, 0] },
fpsInc: { bounds: [0, 0, 0, 0] },
@@ -68,12 +70,13 @@ export class PowerCompareRowWidget extends RgthreeBaseWidget {
}
});
// === Pick A / B / A&B buttons (after a small gap) ===
// === Pick A / B / A&B / B&A buttons (after a small gap) ===
const groupGap = 14;
const picks = [
["pickA", "A", btnW],
["pickB", "B", btnW],
["pickAB", "A/B", btnW + 10],
["pickBA", "B/A", btnW + 10],
];
let pickX = margin + modes.length * (btnW + spacing) + groupGap - spacing;
picks.forEach(([key, pick, w]) => {
@@ -218,8 +221,8 @@ export class PowerCompareRowWidget extends RgthreeBaseWidget {
// Keep the hidden backend widget in sync (serialization + next run)
const w = node.widgets?.find((w) => w.name === "output_pick");
if (w) w.value = pick;
// A/B stitching follows the current compare mode
if (pick === "A/B") {
// A/B + B/A stitching follows the current compare mode
if (pick === "A/B" || pick === "B/A") {
this.applyAbStitchFromMode(node);
}
node.setDirtyCanvas(true, true);
@@ -97,17 +97,18 @@ app.registerExtension({
this.rowWidget.fpsValue = Math.max(1, Math.round(fpsWidget.value));
}
// Output pick (A/B): restored from properties/widget, edited via
// the row's pick buttons - hide the default combo widget
// Output pick (A/B/B/A): restored from properties/widget, edited
// via the row's pick buttons - hide the default combo widget
const validPicks = ['A', 'B', 'A/B', 'B/A'];
this.outputPick = this.properties?.output_pick || 'A';
if (this.outputPick !== 'A' && this.outputPick !== 'B' && this.outputPick !== 'A/B') {
if (!validPicks.includes(this.outputPick)) {
this.outputPick = 'A';
}
const pickWidget = this.widgets.find(w => w.name === 'output_pick');
if (pickWidget) {
pickWidget.computeSize = () => [0, 0];
pickWidget.hidden = true;
if (pickWidget.value !== 'A' && pickWidget.value !== 'B' && pickWidget.value !== 'A/B') {
if (!validPicks.includes(pickWidget.value)) {
pickWidget.value = 'A';
}
this.outputPick = pickWidget.value;
@@ -504,16 +505,17 @@ app.registerExtension({
}
// output_pick widget values are applied just before this hook,
// so the widget wins; fall back to saved properties
const validPicks = ['A', 'B', 'A/B', 'B/A'];
const pickWidget = this.widgets?.find(w => w.name === 'output_pick');
if (pickWidget) {
if (pickWidget.value !== 'A' && pickWidget.value !== 'B' && pickWidget.value !== 'A/B') {
if (!validPicks.includes(pickWidget.value)) {
pickWidget.value = this.properties?.output_pick || 'A';
}
this.outputPick = pickWidget.value;
} else {
this.outputPick = this.properties?.output_pick || 'A';
}
if (this.outputPick !== 'A' && this.outputPick !== 'B' && this.outputPick !== 'A/B') {
if (!validPicks.includes(this.outputPick)) {
this.outputPick = 'A';
}
const stitchWidget = this.widgets?.find(w => w.name === 'ab_stitch');