Load Image + Pad does the same job from the same stage and starts from the file, so the only thing the second node added was taking an IMAGE wire - one idea holding two slots in the menu. Deletes the node, its panel, its help doc and its stage-only shared helpers (padEmptyStateText, findPadPreview), and drops it from NODE_MODULES, PUBLIC_NODE_IDS, the README and the panel guard list. The padding helpers are untouched; Load Image + Pad and Align Image both use them. Two console labels there named the removed node, so _fill_tensor now takes the source name - Align Image passes its own pad_color, and a colour it cannot read is no longer reported against padding. The BHWC and mode errors read "Padding"/"Pad mode" instead. The on-node pad stage moves to ComfyUI-AusBoss-Lab, which has its own AUSBOSS_LAB_PadImage.
131 lines
5.3 KiB
JavaScript
131 lines
5.3 KiB
JavaScript
// Pure geometry and decision logic for the on-node padding canvas
|
|
// (Load Image + Pad). No DOM in here — the drawing and pointer
|
|
// wiring live in js/shared/pad_panel.mjs and the per-node entries, so this
|
|
// module stays testable under node:test.
|
|
|
|
import { clamp, resolvePadding } from "./transform_geometry.mjs";
|
|
|
|
// The whole final-rect edge is the handle; this is the grab tolerance on
|
|
// either side of it, in CSS pixels (hit zones stay larger than the drawn
|
|
// 1.5px dashed line).
|
|
export const EDGE_HIT_BAND = 16;
|
|
|
|
// A pad band thinner than this cannot host its own "+N px" label; the label
|
|
// hops inside the image onto a contrast pill instead.
|
|
export const THIN_BAND_PX = 24;
|
|
|
|
export const PAD_SIDES = ["left", "top", "right", "bottom"];
|
|
|
|
// "sub/dir\\name.png [temp]" -> { filename, subfolder, type } for a /view
|
|
// query. Handles Windows backslashes, subfolders, and the "name [type]"
|
|
// annotation ComfyUI appends outside the input folder.
|
|
export function parseImageReference(value) {
|
|
let text = String(value ?? "").trim();
|
|
if (!text) return null;
|
|
let type = "input";
|
|
const annotated = /^(.*)\s+\[(input|output|temp)\]$/i.exec(text);
|
|
if (annotated) {
|
|
text = annotated[1].trim();
|
|
type = annotated[2].toLowerCase();
|
|
}
|
|
const normalized = text.replaceAll("\\", "/");
|
|
const slash = normalized.lastIndexOf("/");
|
|
const filename = slash < 0 ? normalized : normalized.slice(slash + 1);
|
|
if (!filename) return null;
|
|
return { filename, subfolder: slash < 0 ? "" : normalized.slice(0, slash), type };
|
|
}
|
|
|
|
// Effective per-side padding and canvas size at source scale — the rect the
|
|
// canvas draws. Delegates to resolvePadding so the remainder-to-right/bottom
|
|
// rule has exactly one implementation.
|
|
export function padGeometry(sourceW, sourceH, values) {
|
|
return resolvePadding(values, { width: Math.max(1, sourceW), height: Math.max(1, sourceH) });
|
|
}
|
|
|
|
// Final output size after the multiple AND megapixel math — what the badge
|
|
// shows ("the badge is the truth"). Mirror of plan_pad_canvas in
|
|
// nodes/_pad_helpers.py — keep the two in sync.
|
|
export function finalOutputSize(sourceW, sourceH, values) {
|
|
const base = padGeometry(sourceW, sourceH, values);
|
|
const target = Number(values.target_megapixels) || 0;
|
|
if (target <= 0 || base.outputWidth <= 0 || base.outputHeight <= 0) {
|
|
return { width: base.outputWidth, height: base.outputHeight, scale: 1 };
|
|
}
|
|
const scale = Math.sqrt((target * 1e6) / (base.outputWidth * base.outputHeight));
|
|
const pad = (name) => Math.round(Math.max(0, Number(values[name]) || 0) * scale);
|
|
const final = padGeometry(
|
|
Math.max(1, Math.round(sourceW * scale)),
|
|
Math.max(1, Math.round(sourceH * scale)),
|
|
{
|
|
...values,
|
|
pad_left: pad("pad_left"),
|
|
pad_top: pad("pad_top"),
|
|
pad_right: pad("pad_right"),
|
|
pad_bottom: pad("pad_bottom"),
|
|
target_megapixels: 0,
|
|
},
|
|
);
|
|
return { width: final.outputWidth, height: final.outputHeight, scale };
|
|
}
|
|
|
|
// Which edge of the final rect (CSS-pixel {x, y, width, height}) the point
|
|
// grabs: the whole edge is the handle, corners resolve to the nearer edge,
|
|
// anywhere else is null so the click falls through and the node drags.
|
|
export function hitPadEdge(point, rect, band = EDGE_HIT_BAND) {
|
|
if (!rect || !point) return null;
|
|
const inside =
|
|
point.x >= rect.x - band &&
|
|
point.x <= rect.x + rect.width + band &&
|
|
point.y >= rect.y - band &&
|
|
point.y <= rect.y + rect.height + band;
|
|
if (!inside) return null;
|
|
const distances = {
|
|
left: Math.abs(point.x - rect.x),
|
|
right: Math.abs(point.x - (rect.x + rect.width)),
|
|
top: Math.abs(point.y - rect.y),
|
|
bottom: Math.abs(point.y - (rect.y + rect.height)),
|
|
};
|
|
let best = null;
|
|
for (const side of PAD_SIDES) {
|
|
if (distances[side] > band) continue;
|
|
if (!best || distances[side] < distances[best]) best = side;
|
|
}
|
|
return best;
|
|
}
|
|
|
|
// New raw pad value for a drag: left/top edges move outward with negative
|
|
// deltas, right/bottom with positive. Deltas are in world (source) pixels.
|
|
export function padDragValue(side, startPads, worldDx, worldDy) {
|
|
const start = Math.max(0, Number(startPads?.[side]) || 0);
|
|
const delta =
|
|
side === "left" ? -worldDx : side === "right" ? worldDx : side === "top" ? -worldDy : worldDy;
|
|
return Math.max(0, Math.round(start + delta));
|
|
}
|
|
|
|
export function edgeCursor(side) {
|
|
if (side === "left" || side === "right") return "ew-resize";
|
|
if (side === "top" || side === "bottom") return "ns-resize";
|
|
return "";
|
|
}
|
|
|
|
// Where a side's "+N px" label lives: on the band when it is thick enough to
|
|
// read, hopped onto a contrast pill inside the image when it is thin.
|
|
export function labelMode(bandCssPx, threshold = THIN_BAND_PX) {
|
|
return (Number(bandCssPx) || 0) >= threshold ? "band" : "pill";
|
|
}
|
|
|
|
// Panel height coupled to node width: wide node, taller stage, within reason.
|
|
export function canvasHeightForWidth(width) {
|
|
return Math.round(clamp((Number(width) || 0) * 0.66, 180, 520));
|
|
}
|
|
|
|
// Aspect-fit a world rect into a view with a uniform margin; returns the
|
|
// scale and the view-space origin of the world rect.
|
|
export function fitRect(worldW, worldH, viewW, viewH, margin = 26) {
|
|
const availW = Math.max(1, viewW - margin * 2);
|
|
const availH = Math.max(1, viewH - margin * 2);
|
|
const scale = Math.max(0.001, Math.min(availW / Math.max(1, worldW), availH / Math.max(1, worldH)));
|
|
return { scale, x: (viewW - worldW * scale) / 2, y: (viewH - worldH * scale) / 2 };
|
|
}
|
|
|