diff --git a/nodes/power_compare_video.py b/nodes/power_compare_video.py index 6b9c51b..64d6030 100644 --- a/nodes/power_compare_video.py +++ b/nodes/power_compare_video.py @@ -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) diff --git a/web/power_compare_video/compare_row_widget.js b/web/power_compare_video/compare_row_widget.js index 826d6bb..d7ebbf6 100644 --- a/web/power_compare_video/compare_row_widget.js +++ b/web/power_compare_video/compare_row_widget.js @@ -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); diff --git a/web/power_compare_video/power_compare_video.js b/web/power_compare_video/power_compare_video.js index 54f8af0..3be1abe 100644 --- a/web/power_compare_video/power_compare_video.js +++ b/web/power_compare_video/power_compare_video.js @@ -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');