Stitch Inpaint takes an RGB patch over an RGBA picture and keeps the
picture's own alpha, instead of stopping on a channel mismatch. The halo
fix spreads colours only.
Image Compare keeps alpha in a preview that has see-through parts and
flags it, so the panel shows them over a checkerboard (Slide, A/B and
Erase). The Erase mix lays the top picture over the other, the saved PNG
is RGBA, and the PSD carries each layer's transparency plus a merged
alpha, its flat colours stored over white as Photoshop does. A see-through
B is resized premultiplied. Align measures over gray and warps all four
channels. Solid pictures give byte-identical files.
Refs #232
Edit models often draw the picture a few pixels to the side or a little
zoomed in, so the edges of the paint in Erase show a step. A new ALIGN
button in Erase's tool row measures B against A the way Realign to
Source does and swaps in B moved back over A. Save and Copy use the
lined-up B, and a second click puts B back as it came.
The new route takes only the run's pair key, reads the two previews the
node made, and writes the lined-up B beside them in the temp folder.
A list of pictures runs these nodes once per picture, and the panels
only showed one of them (#204). Arrows (2 / 5) now appear on the node
when a run brings more than one picture: Compare steps through its
pairs, A and B together, and Erase saves the pair on screen. The shared
preview panel gets the same arrows, so Save Image, Mask Refine, Mask by
Name, LaMa Inpaint and Select Frame step through their results. A node
too narrow for the arrows widens to fit while they show.
Picking Erase no longer grows the node, so it never covers its neighbours:
the tool rows wrap on a narrow node and the picture gives up the room.
A node that 2.12.1 grew and saved in Erase still gets its size back.
Copy draws the mix at full size in the browser and writes it to the
clipboard as a PNG; browsers allow this on localhost and over https.
Erase grows a node too small for its tool rows. The size before is kept
in the node's properties, so going back to Slide, A or B returns it,
also after a reload. A side resized while erasing keeps the new size.
The four mode buttons become one switch, Slide | A | B | Erase, whose
marker slides to the choice. In Erase the brush's two jobs and the
picture on top are switches of the same kind, and PSD is a plain
off | on.
Save moves to the bottom right of the node, larger, with a disk icon,
in a row of its own with the name of the files and the PSD choice.
The files can be named. A Name field on the node takes letters,
digits, spaces, dots, dashes and underscores; the server checks the
same rule and refuses anything else, so a folder is never part of a
name. A new optional filename input takes a wired name, such as the
source picture's file name: its folder and extension are dropped. The
save route still names no file to read.
Size and Soft scrub from their label and from the unit inside the box
as well as from the number (the scrub control gains handle() for this).
Workflows saved before load as they were: 60 that use the node were
opened (the pack's examples, published workflows, the oldest saved
shapes), 72 nodes kept their size, mode and links. The node is one
input socket taller.
A fourth mode beside SLIDE, A and B. One picture lies on top of the
other and a brush rubs the top one away, so the other shows through.
ERASE and RESTORE pick what the brush does; Alt or the right mouse
button paints back. Size and Soft set the brush, which shows around the
pointer in teal with its soft edge. It takes Photoshop's keys: [ and ]
for the size, Shift with them for the edge, and Alt with the right
mouse button dragged across for the size or up and down for the edge.
SAVE writes the mix to the output folder as compare_00001.png without
running anything again. With PSD lit it also writes a PSD with both
pictures as layers and the paint as a layer mask on the top one, and
the mask as its own PNG. After a save the row under the picture links
each file for download.
The node keeps the strokes, not a painted picture. The panel draws
them, and the save draws them again on the server at the picture's full
size. The save route takes no file name: a run remembers where its two
previews are and gives the panel a key, Save sends the key back, and
the files get a fixed name. The PSD is written by a small writer of the
pack's own, with no new dependency.
SLIDE is still what the node opens on. The node's inputs and output are
unchanged.
At some node widths and zoom levels a line of the picture that was not on
show ran down the right edge of the one that was. It stood out when one
side was a padded canvas: its grey showed beside the result. Only the
node's view had it, never the saved picture.
The node drew picture B under picture A, and A carried a clip even when
it was shown whole. A browser draws a picture on whole pixels but cuts a
clip where it falls, so on a stage whose width is not a whole number the
clipped A stopped up to a pixel short of B, and B showed as a line.
A picture that is wholly covered is no longer drawn, and a picture shown
whole has no clip.
With the slider on, the panel stayed on whichever picture the pointer
last left over, and a new run did not change it, so a result could
finish behind the picture it was made from.
The panel now shows the result after every run and each time the
pointer leaves. The result is image_b, or image_a when the node feeding
image_b is upstream of the node feeding image_a; when both come out of
one subgraph the same check is made inside it. The A and B buttons
still hold one picture.
Settings > AusBoss > Image Compare > Rest on the result switches the
earlier behaviour back on.
Every node stops at the width it was built for when it is dragged narrower:
the growing panels and the fixed cards. Load Video's IN and OUT boxes stay
whole in a narrow node.
The frontend adds 104 pixels to a growing panel's minimum width and leaves a
fixed-height card's minimum out of its width sum, so a card fell to a 210
pixel default. Panels pass exactMinWidth to fillNodeHeight or call
holdNodeMinWidth, and tests/panel_guards.test.mjs fails a new panel that does
neither.
The A | B badge stayed in the middle of the picture while the split line
moved. Now A rides just left of the line and B just right of it, on the
picture each one names; with one picture showing, its label sits alone.
Both panels filled only from onExecuted. Going to another workflow tab and
back rebuilds the nodes and restores the tab's results in ComfyUI's store
without an onExecuted, so they said "Run to preview" over a result they
had just made, in both renderers. On onNodeOutputsUpdated (a tab's results
restored) an empty panel loads its node's stored record. The output key
helper moves to shared/prompt_scope.mjs for the three users.
Every workflow tab in a page shares one server connection, and ComfyUI
files a node's results by node id. A run from one tab that finished while
another tab was open landed on the open tab's node with the same id. On
Video Crop + Rotate + Pad -> Clip that was a second video under the
buttons that grew the node over the one below; Show Text and Seed also
saved the other workflow's text and seed into this one, and Save Video and
Compare replaced their own result.
- The three Crop + Rotate + Pad nodes stand down ComfyUI's image and video
previews (they send no picture of their own), in both renderers.
- suppressCoreVideoPreview sets hideOutputImages, so Nodes 2.0 no longer
draws Save Video's clip a second time under its player.
- shared/prompt_scope.mjs notes which graph queued each run (the queue
call is wrapped for the prompt id). For a result from another tab, the
AusBoss node here gets this tab's own stored result back, every pack
onExecuted handler returns early, and the result is handed to the owner
tab's AusBoss nodes when that tab is open again. Core nodes are left
alone.
Tests: prompt_scope (scope rules plus a policy check that every
onExecuted handler asks isForeignRun first) and core_preview (every
transform kind suppresses both previews; video suppression sets
hideOutputImages) fail on main and pass here.
Every card and DOM panel was added with options.serialize = false, which
only keeps a widget out of the queued prompt. The frontend saves each
widget whose own serialize is not false and hands saved values back by
position, so each card and panel saved an empty value of its own after the
node's real values, and an input appended to the node later opened holding
it (Stitch Inpaint's seam, Crop For Inpaint's keep_inside). Every DOM widget
now also sets widget.serialize = false, as the frontend's own previews do,
so new saves carry only real values.
Older saves still end in those empty values, and loading them now skips
the cards and panels. On every node that saves by position they come after
all real values (Save Image's card sits mid-list, but Save Image saves by
name), so nothing moves. On frontend 1.53.6 the examples (the 21 from
before the trim and the 7 now) and 14 older saved workflows queue
byte-identical prompts before and after, keep every value, and save and
reopen cleanly.
An input appended later still needs a fallback for those older saves.
tests/saved_widget_values.test.mjs checks that every DOM widget sets the
flag, that the examples keep their values with the panels skipped, and that
each input appended to a node whose older saves end in an empty value is
declared and, as a widget input, has a card resetUnknown fallback that
fires. tests/test_card_fallbacks.py (ComfyUI's Python) checks each fallback
is the input's own default. AGENTS.md states the rule.
The pack's DOM panels must tell ComfyUI's change tracker about their
writes, and several did not: LoRA Loader row edits other than a few
menu actions, Compare's slide / A-B mode, and Mask Refine's AUTO. LoRA
rows now record on every commit, with strength drags recording once on
release, and the other two record when they change a saved value.
- Drag-to-reorder actually drops: the row preview's reparenting silently
released pointer capture, so the gesture now listens on the window for
its whole lifetime.
- Step arrows on every strength box (configured step; Shift = 0.01), with
range tinting carried over.
- The control bar (templates, stack toggle, count, settings) sits directly
on top of the row stack and + Add LoRA pins to the node's bottom edge;
the triggers output got its narrower name (slot links reconnect by index).
- Civitai lookup reads the whole response instead of the first TCP chunk,
so successful lookups stop dying as 'lookup failed'.
- Compare and Load Video declare their height floors WITH the DOM-widget
frame allowance, so minimum-height nodes stop shaving the caption and
trim strip; panel_layout documents the rule.
- README and the LoRA doc catch up; panel_guards comments follow the
body-as-layout-box reality.
Image Compare A/B drew a chip over the top-left of the picture carrying the
resolution and a hint about how to use the panel - covering part of what you
came to look at in order to say something you need once. The resolution moved
to a caption centred under the stage, and when the two sides are different
sizes both are named there: the panel scales them to fit, so nothing else on
screen would reveal the mismatch. The empty and error states still take the
middle of the stage, where there is no picture to hide.
HOLD became an A/B toggle. Press-and-hold meant the comparison only existed
while a mouse button was down, so the two images could never be held side by
side in memory. Each click now swaps the panel and the button names the side
you are looking at; flicking between them in place is how a small difference
actually becomes visible. A workflow saved on "hold" opens on the toggle it
became rather than quietly dropping back to slide.
The example workflow was stale in ways that would have shipped to Civitai:
its overview credited SimpleWatermarkRemover - the deprecated compatibility
id, not the node the graph actually uses - still called the node Refine Mask,
and linked to github.com/auboss, which does not exist. It gained model-setup
and requirements sections, consistent node titles, and the mask settings its
single-frame test was missing: that path shipped with every Mask Refine
control at zero, so the frame it previewed was not the result the full run
would produce, which is the one thing that test exists to show.
Dragging a node taller only ever widened its stage. The frontend
arranges widgets in one pass:
if (w.computeSize) -> fixed height, kept OUT of the split
else if (w.computeLayoutSize) -> joins distributeSpace(freeSpace, ...)
else -> one standard widget row
It is an else-if, so a widget declaring both is pinned by computeSize
and its computeLayoutSize is never called. Every stage, player and
filmstrip here derived that fixed height from the node's WIDTH, so the
extra height became dead space underneath. Compare was fixed by hand;
the other six had the same defect.
Adds js/shared/panel_layout.mjs with one fillNodeHeight() that deletes
computeSize and declares a floor with no ceiling - distributeSpace reads
a missing maxSize as Infinity, so the panel takes whatever is left.
Applied to Compare, Save Video, Load Video, Frame Chooser, Load Image +
Pad, Image Crop+Rotate+Pad, and the shared transform panel that Video
Crop+Rotate+Pad mounts. Removing computeSize also removed where those
nodes got their opening height, so Save Video, Load Video and Load Image
+ Pad now state a default; a saved size still wins through onConfigure.
Measured in the browser: all seven grow 1:1 with the node and clamp at
their own floor.
The panel audit now enforces it - a DOM panel is either a stage that
calls fillNodeHeight and never assigns computeSize, or is listed as
fixed by design - so a new panel cannot quietly reintroduce this.
Six fixes found while testing every node against real workflows.
LoRA Loader: the "wrong base model" warning never fired on a modern stack.
It matched comfy's model class name against a table covering 8 of the 81
classes comfy ships, so Krea 2, Qwen, WAN, LTXV, Z-Image and friends read as
"unknown" and skipped the check entirely - an SD 1.5 LoRA on Krea 2 patched
nothing, changed nothing, and said nothing. It now measures the applied-patch
count across model and CLIP before and after each row and warns when a row
patched nothing, which needs no table, no LoRA metadata, and works on every
model family. base_model_family also stopped mining ss_sd_model_name: that is
the trainer's source filename, and reading substrings out of it labelled any
..._v1.safetensors as SD1.5 and anything containing xl as SDXL.
LM Studio Chat: reasoning returned in the server's own reasoning_content field
now reaches the thinking output. Only inline <think> tags were read, so with
gemma/qwen-style hybrids the whole reply landed in a field the node never
looked at and text came back blank. An empty answer now explains itself,
naming the max_tokens budget when the model ran out of room mid-thought.
Compare: the stage follows the node's height, not only its width. Its panel
declared a computeSize, and the widget layout gives any widget that defines one
a fixed height and leaves it out of the leftover-space split, so dragging the
node taller only added dead space under the image.
Pad Image / Load Image + Pad: a stitcher output, so an outpaint can put the
source back exactly. Padding now builds the same stitcher shape Crop For
Inpaint emits - the crop is simply the whole canvas - so the existing Stitch
Inpaint node serves both and no second stitch node has to exist. Appended
last, so saved workflows keep their links.
Settings menus: editing one row no longer reverts the others. Each save handed
back a value set rebuilt from stored defaults, silently replacing every other
row on a node whose workflow differed from those defaults.
LoRA templates popover: Save was clipped off the panel edge and each saved
row's delete x was stretched across the whole row - one blanket
.ausboss-lora-menu button rule outranked the specialized buttons inside it.
In hold mode the stage captured the pointer on press, which retargeted the
release and ate the mode button's click - one way into HOLD, no way out.
SLIDE and HOLD are now two labeled buttons, and presses born on the
toolbar never reach the stage behaviors.
The frontend sizes a DOM widget's wrapper as widget.width ?? node.width
(GraphView, useDomWidget updateWidgets), and LiteGraph's layout plants
widget.width during draws. Once planted it outranks the node width
forever: resize the node narrower and the wrapper keeps the old width,
parking the panel's right edge - and the LoRA row's strength and info
controls - outside the border. None of the containment guards could
reach it: border-box and the overflow clip act at the wrapper's edge,
which is precisely the thing that is too wide, and the minimum-width
paths only stop the node going BELOW a floor, not the wrapper staying
ABOVE the node.
Diagnosed from a console measurement in the reporting browser (wrapper
styled 470px on a 328px node, scale 1.21) and reproduced exactly by
planting widget.width = 470: overflow +183px. keepDomWidgetWidthAuto
defines width as a discard-writes accessor, so the frontend's lookup
stays on its node-width fallback for the life of the widget. All six
DOM panels call it, the policy test requires it, and the fix was
verified end-to-end in a live classic-renderer session: with the plant
attempted, the wrapper tracked the node through narrow, add-row,
linked, grow and shrink at -13px inside the border throughout.
An OUTPUT_NODE that saves temp previews of each batch's first frame
and renders them stacked in a DOM panel: slide mode clips B to the
pointer with a seam line, hold mode flashes B while pressed. image_a
passes through unchanged so the comparison can sit mid-graph. Pure
clip and mode logic lives in js/shared/compare.mjs under node:test.