diff --git a/README.md b/README.md index 10062f0..9c31e86 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,10 @@ Custom nodes for ComfyUI - image resizing, dataset prep & prompt automation for All nodes appear under the **CraftKit** category in the node menu. +Every node renders correctly in both classic and **Nodes 2.0** (ComfyUI's newer Vue-based node UI): + + + --- ## Nodes @@ -83,9 +87,29 @@ Use this as a **pipeline node** - IMAGE in, IMAGE out. No files are saved to dis --- +### 📏 Smart Resolution Multiplier + + + +Multiply image dimensions by a factor and output the results as integers. + +SeedVR2 takes a single `resolution` INT (the longest side) - not a width and height separately. Standard math nodes give you a FLOAT or require multiple steps to get there. This node does it cleanly in one step: give it your image and a multiplier, and it outputs `width`, `height`, and `resolution` (longest side) ready to connect directly to SeedVR2's `resolution` input. + +Also useful for LoRA dataset prep - using mixed resolutions in your training set generally produces better results than training on a single fixed size. Smart Resolution Multiplier makes it easy to dynamically calculate the right target size per image rather than hardcoding a value. + +| Input | Type | Default | Description | +|---|---|---|---| +| Image | IMAGE | - | Source image - its current width/height are read directly, no separate Get Image Size node needed | +| Multiplier | FLOAT | 2.0 | Multiply width and height by this factor | +| Round to multiple of | INT | 8 | Snap dimensions to this multiple | + +**Outputs:** Width, Height, Resolution (longest side as INT → directly into SeedVR2) + +--- + ### 📁 Smart Batch Resize - + Load **all images from a folder**, resize each one by longest side, and save into a subfolder. Build clean dataset filenames from a prefix, the original name, and/or a sequential counter - combined with an optional resolution suffix. @@ -118,26 +142,6 @@ Includes a **Browse folder** button to pick the input folder directly from the n --- -### 📏 Smart Resolution Multiplier - - - -Multiply image dimensions by a factor and output the results as integers. - -SeedVR2 takes a single `resolution` INT (the longest side) - not a width and height separately. Standard math nodes give you a FLOAT or require multiple steps to get there. This node does it cleanly in one step: give it your image and a multiplier, and it outputs `width`, `height`, and `resolution` (longest side) ready to connect directly to SeedVR2's `resolution` input. - -Also useful for LoRA dataset prep - using mixed resolutions in your training set generally produces better results than training on a single fixed size. Smart Resolution Multiplier makes it easy to dynamically calculate the right target size per image rather than hardcoding a value. - -| Input | Type | Default | Description | -|---|---|---|---| -| Image | IMAGE | - | Source image - its current width/height are read directly, no separate Get Image Size node needed | -| Multiplier | FLOAT | 2.0 | Multiply width and height by this factor | -| Round to multiple of | INT | 8 | Snap dimensions to this multiple | - -**Outputs:** Width, Height, Resolution (longest side as INT → directly into SeedVR2) - ---- - ## Example workflows Ready-to-load workflows are included in [`example_workflows/`](example_workflows): @@ -175,8 +179,8 @@ Restart ComfyUI. All nodes appear under **CraftKit** in the node menu: - `CraftKit` → **Smart Prompt Controller 📋** - `CraftKit` → **Smart Profile Switch 🧩** - `CraftKit` → **Smart Resize 📐** -- `CraftKit` → **Smart Batch Resize 📁** - `CraftKit` → **Smart Resolution Multiplier 📏** +- `CraftKit` → **Smart Batch Resize 📁** --- diff --git a/assets/screenshot-nodes2-compat.png b/assets/screenshot-nodes2-compat.png new file mode 100644 index 0000000..a2ffe0d Binary files /dev/null and b/assets/screenshot-nodes2-compat.png differ diff --git a/assets/screenshot-smart-batch-resize_v3.png b/assets/screenshot-smart-batch-resize_v3.png deleted file mode 100644 index 084641c..0000000 Binary files a/assets/screenshot-smart-batch-resize_v3.png and /dev/null differ diff --git a/assets/screenshot-smart-batch-resize_v4.png b/assets/screenshot-smart-batch-resize_v4.png new file mode 100644 index 0000000..05ad010 Binary files /dev/null and b/assets/screenshot-smart-batch-resize_v4.png differ diff --git a/docs/superpowers/plans/2026-07-28-nodes2-widget-compat.md b/docs/superpowers/plans/2026-07-28-nodes2-widget-compat.md index e01b4e4..5d3ee12 100644 --- a/docs/superpowers/plans/2026-07-28-nodes2-widget-compat.md +++ b/docs/superpowers/plans/2026-07-28-nodes2-widget-compat.md @@ -680,3 +680,42 @@ Executed inline (not subagent-driven), autonomously while the user slept, then j Tasks 1, 2, 4, 5, 6, 7 landed as specified. Task 3 (`createPresetPickerWidget`) needed one addition beyond what was originally planned: callers must guard the reposition-splice with `!isVueNodes()`. This wasn't anticipated by the original research (which correctly identified *that* a dual-path widget was needed, but not this specific interaction between splice-repositioning and Vue's post-`configure()` reactivity). Task 8's manual verification sweep is exactly what caught it — this is the reason the plan's "no automated JS test harness, verify manually" constraint insisted on an actual save → reload round-trip check per node, not just a visual glance at a freshly-added node. - **WorkflowOrganizer is explicitly out of scope** per the user's instruction ("workflow organiser doen we later checken") — this plan touches only ComfyUI-CraftKit. + +--- + +## Implementation Log (2026-08-13) — simplification + the widget.width staleness bug + +Picks up after the (2026-08-07) log above. Two follow-up rounds: first a deliberate architecture simplification, then a hard-to-reproduce click bug that needed a second opinion from a fresh model to actually crack. + +### Simplification: dropped the dual-path DOM widget entirely + +The (2026-08-07) log accepted a visual trade-off: under Nodes 2.0 the preset-chip row sat at the *bottom* of the node instead of directly under "Longest side (px)", because repositioning the `addDOMWidget`-based Nodes-2.0 variant mid-list desynced Vue's save/reload binding. The user flagged this as bad UX once they actually saw it side by side with classic mode. + +Fix: dropped the `addDOMWidget`/`isVueNodes()` dual-path in `preset_picker_widget.mjs` entirely, keeping only the single canvas-drawn (`type: "custom"`) implementation in both renderers. This is safe because the preset picker is click-only (no hover state was ever implemented), and canvas `"custom"` widgets were already proven immune to the reposition/reload bug (unlike `addDOMWidget`). `js/shared/nodes2.mjs` (the `isVueNodes()` helper) became unused and was deleted. Reposition-splice guards were made unconditional again in both `smart_batch_resize.js` and `smart_resize.js`. + +Net result: preset chips now sit in the same, correct position in both renderers, with one shared draw/hit-test implementation instead of two. + +### Widget-visibility helper, and the real WidgetLegacy.vue render-trigger model + +Separately, "Counter start" (shown/hidden based on the "Add counter" toggle) and the customtext output widget that all 4 nodes hide after execution both used a `computeSize = () => [0,-4]` collapse trick. This works in classic mode (canvas layout calls `computeSize()` every frame) but silently does nothing under Nodes 2.0 — confirmed by reading `useProcessedWidgets.ts`'s `computeProcessedWidgets()`: Nodes 2.0's widget list is a Vue `shallowReactive` array (see `useGraphNodeManager.ts`, `extractVueNodeData()`) that only tracks the array's own indices/length as reactive dependencies, never properties nested inside each widget object (like `widget.options.hidden`) — so mutating those never triggers a re-render. + +Fix, in new `js/shared/widget_visibility.mjs` (`setWidgetVisible(node, widget, visible)`): set `widget.hidden`/`widget.options.hidden` *and* perform a structural no-op mutation on `node.widgets` (push a dummy widget, immediately splice it back out) to force Vue's shallowReactive array to actually notice something changed and recompute. Verified this exact mechanism via live console testing before writing the real fix — a plain `widget.options.hidden = true` + `node.setDirtyCanvas()` provably did nothing in Nodes 2.0 (confirmed via `Object.getOwnPropertyDescriptor(node, 'widgets')` showing the Vue-installed getter/setter), while the push+splice nudge worked immediately. + +Along the way, also discovered (via reading `WidgetLegacy.vue`'s actual source, extracted from the frontend package's sourcemap) that **`node.setDirtyCanvas()` does not repaint a Nodes-2.0 `WidgetLegacy`-bridged widget at all** — only calling the widget's own `triggerDraw()` (a function `WidgetLegacy.vue` assigns onto the widget object once mounted) does. This explained two more stale-visual bugs: the active/orange preset chip not updating after a click, and the status box text not updating after a run, in Nodes 2.0 specifically. Fixed by calling `this.triggerDraw?.()` (in `mouse()`) / `statusWidget.triggerDraw?.()` (in `onExecuted`) alongside the existing `setDirtyCanvas()` calls, everywhere this pattern occurred across all 4 node JS files. + +Also found and fixed: the section-divider line (`createDividerWidget` in `canvas_widgets.mjs`) was being drawn correctly but was visually near-invisible under Nodes 2.0 — its `#333` stroke color survived classic mode's native-resolution canvas fine, but got softened into invisibility by Nodes 2.0's extra 2x-backing-store + CSS-zoom-transform rendering path. Fixed by brightening to `#555` and centering the label with flanking lines on both sides (`── LABEL ──`) instead of label-left/line-right, per user request. + +### The click-reliability bug that needed a second opinion + +After the above fixes, the user reported the preset chips "still not good" in classic mode specifically (Nodes 2.0 was fine): clicks on the right-hand chips of a *wide* node (they'd resized one to ~580px) either landed on the wrong chip or did nothing at all — worst on the rightmost ("1536") chip. Extensive live-tested hypotheses (screenshot-timing artifacts, `getWidgetOnPos` hit-detection, `_rects` staleness after `setSize()`, node-selection-order effects, `HIT_PAD`/chip-size tuning) all failed to nail a reproducible root cause through iterative testing — some rounds even made it look *worse* after a "bigger chips" fix landed, which the user called out immediately as suspicious. + +Spawned a fresh Opus subagent as a genuine second opinion, with full context (exact symptom history, files, and everything already ruled out) but explicit instructions to re-derive from the actual source rather than trust the existing narrative. It found the real root cause on the first pass by reading `WidgetLegacy.vue` and `LGraphNode.ts` (both extracted via the frontend package's sourcemaps) side by side: + +- `WidgetLegacy.vue` (Nodes 2.0's canvas-draw bridge) permanently stamps `widgetInstance.width = width` (its own, possibly-narrower container width) onto the **shared widget object** on every mount — and its `onBeforeUnmount` only resets `triggerDraw`, never `width`. +- Classic LiteGraph's own click hit-test, `getWidgetOnPos` (`LGraphNode.ts` ~line 2281), reads `widget.width || nodeWidth` to decide the clickable X range — **not** `node.size[0]`. +- Net effect: once a node has ever been rendered under Nodes 2.0 (even briefly, even in a since-closed tab), classic mode's click detection for that widget stays capped at whatever (possibly narrow) width Nodes 2.0 last stamped, forever, regardless of the node's actual current width, until the widget object itself is recreated. +- This also explained the "made it worse" observation precisely: an earlier fix (`c6c26a3`, this project's commit reading live `node.size[0]` in `draw()` instead of the stale `widgetWidth` param) fixed the *drawing* to be full-width, but the *hit-test* stayed capped at the stale stamped width — so chips started being drawn wider than they were actually clickable, which is strictly worse UX than being uniformly-too-narrow-but-consistent. + +Fix, in `preset_picker_widget.mjs`'s `draw()`: `this.width = w;` (writing the live, correct width back onto the widget object) on every single draw call, in both renderers — so a stale Nodes-2.0 stamp gets self-corrected on the very next classic-mode repaint, and vice versa. Verified by deliberately reproducing the exact failure mode (stamp `widget.width = 300` on a 580px-wide node, confirm a normal redraw restores it to 580, confirm a click on the far-right chip then registers correctly) rather than just re-testing the original vague symptom. + +**Lesson for future Nodes 2.0 compat work in this repo:** any widget property that Nodes 2.0's `WidgetLegacy.vue` bridge writes onto the shared widget object (`.width`, `.y`, `.triggerDraw` are the three confirmed so far) must be treated as **owned by whichever renderer painted last**, not as a stable/cached value — always recompute and reassign it from the live `node`/DOM state at the top of every `draw()`, never trust it surviving between renderer switches. When a live-tested bug resists several iterations of hypothesis-and-retest without a clean repro, escalating to a fresh model with full context and explicit "don't trust the existing narrative, re-derive from source" instructions found the answer in one pass where continued iteration likely wouldn't have — worth doing sooner next time a bug proves this sticky, rather than as a last resort. diff --git a/js/shared/canvas_widgets.mjs b/js/shared/canvas_widgets.mjs index 69dfd4e..52b2e15 100644 --- a/js/shared/canvas_widgets.mjs +++ b/js/shared/canvas_widgets.mjs @@ -31,19 +31,28 @@ export function createDividerWidget(label) { }, draw(ctx, node, widgetWidth, y, height) { const w = node?.size?.[0] || widgetWidth; - const lx = 14; + const margin = 14; + const gap = 8; ctx.save(); ctx.font = "bold 10px sans-serif"; ctx.textBaseline = "middle"; - ctx.textAlign = "left"; + ctx.textAlign = "center"; + const cy = y + height / 2; + const cx = w / 2; + const textW = ctx.measureText(label).width; ctx.fillStyle = "#888"; - ctx.fillText(label, lx, y + height / 2); - const lineX = lx + ctx.measureText(label).width + 8; - ctx.beginPath(); - ctx.moveTo(lineX, y + height / 2); - ctx.lineTo(w - 14, y + height / 2); - ctx.strokeStyle = "#333"; + ctx.fillText(label, cx, cy); + // #555 rather than a more subtle #333 — Nodes 2.0 renders this canvas + // at 2x internal scale and then CSS-scales it again with the graph + // zoom, which softens a thin line enough that #333 became invisible + // there (classic mode draws at native resolution and stayed sharp). + ctx.strokeStyle = "#555"; ctx.lineWidth = 1; + ctx.beginPath(); + ctx.moveTo(margin, cy); + ctx.lineTo(cx - textW / 2 - gap, cy); + ctx.moveTo(cx + textW / 2 + gap, cy); + ctx.lineTo(w - margin, cy); ctx.stroke(); ctx.restore(); }, diff --git a/js/shared/nodes2.mjs b/js/shared/nodes2.mjs deleted file mode 100644 index 79a2e68..0000000 --- a/js/shared/nodes2.mjs +++ /dev/null @@ -1,7 +0,0 @@ -// Detects whether ComfyUI's "Modern Node Design (Nodes 2.0)" Vue renderer is -// active, so widgets that need genuinely different implementations per -// renderer (anything with hit-testing/hover, not just a draw() override) can -// pick the right one at node-creation time. -export function isVueNodes() { - return !!window.LiteGraph?.vueNodesMode; -} diff --git a/js/shared/preset_picker_widget.mjs b/js/shared/preset_picker_widget.mjs index f3373c5..976def0 100644 --- a/js/shared/preset_picker_widget.mjs +++ b/js/shared/preset_picker_widget.mjs @@ -1,9 +1,23 @@ import { app } from "../../../scripts/app.js"; -import { isVueNodes } from "./nodes2.mjs"; -const PRESET_ROW_HEIGHT = 22; +// A single canvas-drawn implementation, used in both classic LiteGraph and +// Nodes 2.0 (via the WidgetLegacy.vue bridge — type: "custom" isn't a +// recognized Vue registry name, so it falls through to the bridge, same as +// the divider/status widgets in canvas_widgets.mjs). No separate DOM-widget +// path: an earlier version used addDOMWidget for Nodes 2.0, but repositioning +// that DOM widget into the middle of node.widgets (to sit under longest_side +// instead of at the end) desynced Vue's value-to-row binding for every widget +// after it on save/reload. Canvas "custom" widgets don't have that problem — +// they've been confirmed to survive reposition + reload fine — so there's no +// need for the dual-path/DOM version this node's click-only (no hover) +// interaction never actually required. +const PRESET_ROW_HEIGHT = 26; +// Hit-test area is padded a few px beyond the visible chip on every side — +// forgiving actual mouse precision, which drifts a little between press and +// release, unlike pixel-perfect synthetic clicks. +const HIT_PAD = 3; -function createClassicPresetWidget(node, targetWidget, presets) { +export function createPresetPickerWidget(node, targetWidget, presets) { const widget = { name: "size_presets", type: "custom", @@ -18,12 +32,21 @@ function createClassicPresetWidget(node, targetWidget, presets) { // Read the live node width rather than the (possibly stale-cached) // widgetWidth parameter — see the comment in canvas_widgets.mjs. const w = node?.size?.[0] || widgetWidth; + // Nodes 2.0's WidgetLegacy bridge permanently stamps widget.width + // from its own container onto this widget object (and never + // clears it on unmount). Classic LiteGraph's own click hit-test + // (getWidgetOnPos) reads widget.width — not node.size[0] — to + // decide the clickable X range, so once this node has ever been + // painted under Nodes 2.0, classic mode silently loses clicks on + // the right side of a wide node forever. Keep width in sync on + // every draw so classic's hit-test always agrees with reality. + this.width = w; const margin = 14; - const gap = 5; + const gap = 7; const innerW = Math.max(40, w - margin * 2); const n = presets.length; const cellW = Math.max(10, (innerW - gap * (n - 1)) / n); - const h = Math.min(height - 2, 18); + const h = Math.min(height - 2, 22); const top = y + (height - h) / 2; const current = targetWidget.value; this._rects = []; @@ -57,10 +80,15 @@ function createClassicPresetWidget(node, targetWidget, presets) { if (event.type !== "pointerdown" && event.type !== "mousedown") return false; const [lx, ly] = pos; for (const r of this._rects) { - if (lx >= r.x1 && lx <= r.x2 && ly >= r.y1 && ly <= r.y2) { + if (lx >= r.x1 - HIT_PAD && lx <= r.x2 + HIT_PAD && ly >= r.y1 - HIT_PAD && ly <= r.y2 + HIT_PAD) { targetWidget.value = r.size; targetWidget.callback?.(r.size, app.canvas, node); node.setDirtyCanvas(true, true); + // Nodes 2.0's WidgetLegacy bridge only repaints its canvas + // via the widget's own triggerDraw() (set on it once mounted), + // not via setDirtyCanvas — without this the newly-active chip + // never highlights even though the value did change. + this.triggerDraw?.(); return true; } } @@ -69,57 +97,3 @@ function createClassicPresetWidget(node, targetWidget, presets) { }; return node.addCustomWidget(widget); } - -function createVuePresetWidget(node, targetWidget, presets) { - const root = document.createElement("div"); - root.style.display = "flex"; - root.style.gap = "5px"; - root.style.padding = "0 14px"; - root.style.width = "100%"; - root.style.boxSizing = "border-box"; - - const buttons = presets.map((size) => { - const btn = document.createElement("button"); - btn.textContent = String(size); - btn.style.flex = "1"; - btn.style.padding = "4px 0"; - btn.style.borderRadius = "4px"; - btn.style.border = "1px solid #555"; - btn.style.background = "#2a2a2a"; - btn.style.color = "#ccc"; - btn.style.fontSize = "11px"; - btn.style.fontFamily = "sans-serif"; - btn.style.cursor = "pointer"; - const syncActive = () => { - const active = Number(targetWidget.value) === size; - btn.style.background = active ? "#f28f41" : "#2a2a2a"; - btn.style.borderColor = active ? "#f28f41" : "#555"; - btn.style.color = active ? "#111" : "#ccc"; - }; - btn.addEventListener("click", () => { - targetWidget.value = size; - targetWidget.callback?.(size, app.canvas, node); - for (const b of buttons) b.syncActive?.(); - node.setDirtyCanvas(true, true); - }); - btn.syncActive = syncActive; - syncActive(); - root.appendChild(btn); - return btn; - }); - - const widget = node.addDOMWidget("size_presets", "craftkit_preset_picker", root, { - serialize: false, - hideOnZoom: false, - }); - // Fixed-height row, not a flexible grower — same reasoning as Pixaroma's - // button-row DOM widget. - widget.computeLayoutSize = undefined; - return widget; -} - -export function createPresetPickerWidget(node, targetWidget, presets) { - return isVueNodes() - ? createVuePresetWidget(node, targetWidget, presets) - : createClassicPresetWidget(node, targetWidget, presets); -} diff --git a/js/shared/widget_visibility.mjs b/js/shared/widget_visibility.mjs new file mode 100644 index 0000000..4f6a265 --- /dev/null +++ b/js/shared/widget_visibility.mjs @@ -0,0 +1,36 @@ +// Dynamically show/hide a widget (e.g. a field that only makes sense when +// another toggle is on) in a way that works in BOTH classic LiteGraph and +// Nodes 2.0. +// +// Classic mode collapses a row by giving it computeSize() => [0, -4] — the +// canvas layout loop calls computeSize() every frame, so this is enough on +// its own there. +// +// Nodes 2.0 never calls computeSize() at all; its widget list is a Vue +// `shallowReactive` array (see ComfyUI's useGraphNodeManager.ts) that only +// tracks the array's own indices/length, not properties nested inside each +// widget (like widget.options.hidden). So setting widget.hidden/options.hidden +// is necessary but NOT sufficient — nothing tells Vue to re-run the computed +// that reads it. A push-then-splice-it-back no-op on node.widgets forces a +// real structural array mutation, which Vue's reactive array DOES notify on, +// causing the widget list to re-evaluate and pick up the new hidden state. +export function setWidgetVisible(node, widget, visible) { + // Widgets don't always define their own computeSize (many rely on + // LiteGraph's built-in default row sizing), so it can legitimately be + // undefined — guard capture with a separate flag rather than truthiness, + // or a later call would "capture" the already-collapsed [0,-4] function. + if (!widget._visibilityDefaultCaptured) { + widget._defaultComputeSize = widget.computeSize; + widget._visibilityDefaultCaptured = true; + } + widget.computeSize = visible ? widget._defaultComputeSize : () => [0, -4]; + widget.hidden = !visible; + if (!widget.options) widget.options = {}; + widget.options.hidden = !visible; + + const nudge = { name: "__visibility_nudge__", type: "custom", value: null, computeSize: () => [0, 0], draw() {} }; + node.widgets.push(nudge); + node.widgets.splice(node.widgets.indexOf(nudge), 1); + + node.setDirtyCanvas(true, true); +} diff --git a/js/smart_batch_resize.js b/js/smart_batch_resize.js index 5bc22eb..4ec8b06 100644 --- a/js/smart_batch_resize.js +++ b/js/smart_batch_resize.js @@ -1,7 +1,7 @@ import { app } from "../../scripts/app.js"; -import { createDividerWidget, createStatusWidget } from "./shared/canvas_widgets.mjs?v=2"; -import { createPresetPickerWidget } from "./shared/preset_picker_widget.mjs?v=2"; -import { isVueNodes } from "./shared/nodes2.mjs?v=2"; +import { createDividerWidget, createStatusWidget } from "./shared/canvas_widgets.mjs?v=3"; +import { createPresetPickerWidget } from "./shared/preset_picker_widget.mjs?v=6"; +import { setWidgetVisible } from "./shared/widget_visibility.mjs?v=2"; app.registerExtension({ name: "Craftopia.SmartBatchResize", @@ -41,21 +41,12 @@ app.registerExtension({ const PRESETS = [512, 768, 1024, 1536]; const presetWidget = createPresetPickerWidget(node, longestSideWidget, PRESETS); - // Move preset row to right after longest_side — classic mode only. - // Under Nodes 2.0, splicing the DOM widget into the middle of - // node.widgets after configure() has already run desyncs Vue's - // value-to-row binding for every widget after it (values appear - // shifted by one row after save/reload, even though the saved - // widgets_values array itself is correct). Leaving the DOM widget - // wherever addDOMWidget placed it avoids that; classic mode's - // canvas-drawn widget doesn't have this issue, so it still repositions. - if (!isVueNodes()) { - const lsIdx = node.widgets.indexOf(longestSideWidget); - const pwIdx = node.widgets.indexOf(presetWidget); - if (pwIdx !== lsIdx + 1) { - node.widgets.splice(pwIdx, 1); - node.widgets.splice(lsIdx + 1, 0, presetWidget); - } + // Move preset row to right after longest_side + const lsIdx = node.widgets.indexOf(longestSideWidget); + const pwIdx = node.widgets.indexOf(presetWidget); + if (pwIdx !== lsIdx + 1) { + node.widgets.splice(pwIdx, 1); + node.widgets.splice(lsIdx + 1, 0, presetWidget); } } @@ -110,12 +101,8 @@ app.registerExtension({ const useCounterWidget = node.widgets?.find(w => w.name === "use_counter"); const counterStartWidget = node.widgets?.find(w => w.name === "counter_start"); if (useCounterWidget && counterStartWidget) { - const defaultComputeSize = counterStartWidget.computeSize; const updateCounterVisibility = () => { - counterStartWidget.computeSize = useCounterWidget.value - ? defaultComputeSize - : () => [0, -4]; - node.setDirtyCanvas(true); + setWidgetVisible(node, counterStartWidget, useCounterWidget.value); }; const origCallback = useCounterWidget.callback; useCounterWidget.callback = function (...args) { @@ -133,7 +120,7 @@ app.registerExtension({ // Hide any customtext widget already present (e.g. from a previously run workflow) const hideCustomText = () => { for (const w of node.widgets) { - if (w.type === "customtext") w.computeSize = () => [0, -4]; + if (w.type === "customtext") setWidgetVisible(node, w, false); } }; hideCustomText(); @@ -145,6 +132,9 @@ app.registerExtension({ if (output?.text?.[0]) { statusWidget._text = output.text[0]; hideCustomText(); + // setDirtyCanvas alone doesn't repaint Nodes 2.0's canvas bridge — + // see the comment in preset_picker_widget.mjs. + statusWidget.triggerDraw?.(); } node.setDirtyCanvas(true); }; diff --git a/js/smart_profile_switch.js b/js/smart_profile_switch.js index d698d5c..a84d7c1 100644 --- a/js/smart_profile_switch.js +++ b/js/smart_profile_switch.js @@ -1,5 +1,6 @@ import { app } from "../../scripts/app.js"; -import { createDividerWidget, createStatusWidget } from "./shared/canvas_widgets.mjs?v=2"; +import { createDividerWidget, createStatusWidget } from "./shared/canvas_widgets.mjs?v=3"; +import { setWidgetVisible } from "./shared/widget_visibility.mjs?v=2"; app.registerExtension({ name: "Craftopia.SmartProfileSwitch", @@ -58,9 +59,12 @@ app.registerExtension({ statusWidget._text = output.text[0]; for (const w of node.widgets) { if (w !== statusWidget && w.type === "customtext") { - w.computeSize = () => [0, -4]; + setWidgetVisible(node, w, false); } } + // setDirtyCanvas alone doesn't repaint Nodes 2.0's canvas bridge — + // see the comment in preset_picker_widget.mjs. + statusWidget.triggerDraw?.(); } node.setDirtyCanvas(true); }; diff --git a/js/smart_prompt_controller.js b/js/smart_prompt_controller.js index 82821aa..e0d7c4d 100644 --- a/js/smart_prompt_controller.js +++ b/js/smart_prompt_controller.js @@ -1,5 +1,6 @@ import { app } from "../../scripts/app.js"; -import { createStatusWidget } from "./shared/canvas_widgets.mjs?v=2"; +import { createStatusWidget } from "./shared/canvas_widgets.mjs?v=3"; +import { setWidgetVisible } from "./shared/widget_visibility.mjs?v=2"; app.registerExtension({ name: "Craftopia.SmartPromptController", @@ -34,12 +35,15 @@ app.registerExtension({ if (output?.text?.[0]) { statusWidget._text = output.text[0]; - // Collapse the default customtext widget ComfyUI adds for ui.text + // Hide the default customtext widget ComfyUI adds for ui.text for (const w of node.widgets) { if (w !== statusWidget && w.type === "customtext") { - w.computeSize = () => [0, -4]; + setWidgetVisible(node, w, false); } } + // setDirtyCanvas alone doesn't repaint Nodes 2.0's canvas bridge — + // see the comment in preset_picker_widget.mjs. + statusWidget.triggerDraw?.(); } node.setDirtyCanvas(true); diff --git a/js/smart_resize.js b/js/smart_resize.js index 210fffd..09d7264 100644 --- a/js/smart_resize.js +++ b/js/smart_resize.js @@ -1,6 +1,5 @@ import { app } from "../../scripts/app.js"; -import { createPresetPickerWidget } from "./shared/preset_picker_widget.mjs?v=2"; -import { isVueNodes } from "./shared/nodes2.mjs?v=2"; +import { createPresetPickerWidget } from "./shared/preset_picker_widget.mjs?v=6"; app.registerExtension({ name: "Craftopia.SmartResize", @@ -27,18 +26,12 @@ app.registerExtension({ return data; }; - // Move preset row to right after longest_side — classic mode only. - // Under Nodes 2.0, splicing the DOM widget into the middle of - // node.widgets after configure() has already run desyncs Vue's - // value-to-row binding for every widget after it (see the comment in - // smart_batch_resize.js for the full explanation). - if (!isVueNodes()) { - const lsIdx = node.widgets.indexOf(longestSideWidget); - const pwIdx = node.widgets.indexOf(presetWidget); - if (pwIdx !== lsIdx + 1) { - node.widgets.splice(pwIdx, 1); - node.widgets.splice(lsIdx + 1, 0, presetWidget); - } + // Move preset row to right after longest_side + const lsIdx = node.widgets.indexOf(longestSideWidget); + const pwIdx = node.widgets.indexOf(presetWidget); + if (pwIdx !== lsIdx + 1) { + node.widgets.splice(pwIdx, 1); + node.widgets.splice(lsIdx + 1, 0, presetWidget); } // Force a full size/layout recompute now that the preset widget has diff --git a/pyproject.toml b/pyproject.toml index 1f90bdf..bca7380 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "comfyui-craftkit" description = "Smart resize, batch resize, resolution multiplier and prompt controller nodes for ComfyUI" -version = "1.1.0" +version = "1.1.1" readme = "README.md" requires-python = ">=3.10" license = { file = "LICENSE" }