Handle reference images via WanVaceToVideo.reference_image instead,
which properly VAE-encodes, channels-doubles, and prepends the frame
alongside the control video latent. Avoids double-prepending when
both nodes tried to handle it.
V2 checks widget.type to decide whether to render; setting only
widget.hidden = true was insufficient. Adding widget.type = "hidden"
matches the V2-compatible pattern used across the ComfyUI ecosystem.
In ComfyUI's V2 (Vue) renderer, node height is derived from the DOM
widget's rendered size, so setting dom.wrap.style.height inside onResize
creates a feedback loop: each DOM change causes V2 to fire onResize again
with a larger size[1], growing the node without bound.
The fix measures the actual node-height overhead (everything above
dom.wrap) from V2's own first onResize call, where size[1] is DOM-derived
and therefore encodes the exact overhead needed for a zero-change
equilibrium. The handler returns early without touching the DOM until this
measurement is taken, so the loop never starts. A secondary recalibration
step corrects the overhead if V2's internal chrome value shifts post-init
(observed in diagnostic data). The dom.wrap.offsetHeight === h guard is
the terminal condition for both cases.
V1 behavior is unchanged: overhead is pre-seeded with the hardcoded
CTRL_H + NODE_CHROME and no measurement or recalibration runs.
Thank you to @fullthrottler for identifying the root cause and suggesting a temporary fix.
If a workflow is loaded with the outpaint node outside the visible
viewport, ComfyUI gives the DOM widget clientWidth=0. The double-rAF
initLayout call returned false, and the old code did an early return
that skipped wireInteractions, setupResizeObserver, and the
execution_success listener entirely. The node had no event handlers
and no way to recover.
- Double-rAF no longer returns early on layout failure; interactions
and the ResizeObserver are always wired up
- ResizeObserver handles !st.initialized by running the full deferred
init sequence the first time the wrap gets a valid size
- initLayout returns false early when maxW or maxH <= 0, preventing
degenerate scale values from a partially-visible wrap
- Control video pad color is now customizable, allowing direct use with the LTX-2 outpainting lora and any other applications that require a specific color.
- Node renamed to Video Outpaint to reflect its utility beyond VACE.
- Output resolution is now preserved across reloads and re-runs.
Previously, settings like 720x1280 (9:16) would silently revert to
1280x720 whenever the node refreshed its preview frames - on page
load and after every workflow execution.
- Aspect ratio lock button state is now saved with the workflow. If
you unlocked the AR (showing the open padlock), that choice is
remembered when you reload. Previously it always came back as locked.
- Aspect ratio chip highlight no longer desyncs when the node scrolls
off-screen. Scrolling the node out of view and back could cause the
wrong aspect ratio chip to appear highlighted (e.g. showing 16:9
active when the crop was actually 9:16). This is now fixed.
Canvas wrap changed from fixed height to min-height so the node can grow vertically
CANVAS_H comment updated to "minimum" height; initLayout and fitCropInView now use dom.wrap.clientHeight instead of the constant
ResizeObserver bails out early if clientHeight <= 0 (collapsed/hidden node)
node.onResize hook added: enforces a minimum width of 520px (with left-edge compensation), and grows dom.wrap.style.height to fill extra vertical space
NODE_CHROME constant (72px) and MIN_W (520px) added; CTRL_H bumped 170→185
Output resolution logic
Removed the "auto" button from the output size row; label changed from "out:" to "output resolution:"
Added syncOutToAR(st) helper — keeps outW/outH AR-consistent with cropAR
Added defaultOut(st) helper — computes output size from source pixel area shaped to current cropAR
Output fields now always have a value (defaulted via defaultOut); clearing a field resets to default rather than going to "auto/0"
Output W/H inputs: invalid input now resets to default instead of zeroing out
Chip (preset AR) clicks now preserve pixel area and call syncOutToAR
AR lock / crop interaction fixes
Two places had inverted logic (if (st.arLocked) → if (!st.arLocked)) for updating cropAR during snap/center moves
Drag-resize and edge-drag now call syncOutToAR(st) after crop changes
render() now uses cropAR for active chip highlight (instead of raw crop box ratio), with tolerance loosened 0.001→0.005
hasOutScale label only shows → arrow when output differs from crop size
Initialization / restore
After restoring crop state, ensures outW/outH are set and AR-consistent
wireInteractions called before render (order swap)
crop_state widget gets cs.hidden = true
Backward-compat comment added for removed matchSrcFlag (parts[6])