"Rotate" is the word people actually search for, and almost nothing in the ecosystem registers under that name, so keeping it in the display name is worth more than a tidier one. The node menu now shows "Transform & Rotate Pro", and the class is TransformRotatePro to match. The registration key stays MP_ImageTransform on purpose. That key is what saved workflows and placed nodes reference, so changing it again would break any node already dropped on a canvas. The key is never shown in the UI, so there is no cost to leaving it as it is - a comment in __init__.py records why it differs from the class name. Category is unchanged at MachinePaintingNodes/Rotate & Scale. README updated throughout.
110 lines
3.9 KiB
JavaScript
110 lines
3.9 KiB
JavaScript
import { app } from "/scripts/app.js";
|
|
|
|
// Rotation controls shared by Transform & Rotate Pro and Remove Background Pro:
|
|
// - the "fixed" method snaps the angle slider to 45 degree increments
|
|
// - Remove Background Pro hides its rotation widgets until the toggle is on
|
|
// Python snaps the angle too, so the result is correct even if the frontend
|
|
// widget options change shape in a future ComfyUI release.
|
|
|
|
const SNAP_DEGREES = 45;
|
|
|
|
const TARGETS = {
|
|
MP_ImageTransform: {
|
|
toggle: null,
|
|
controlled: [],
|
|
},
|
|
RemoveBackgroundPro: {
|
|
toggle: "enable_rotation",
|
|
controlled: ["rotation_method", "rotation_angle", "zoom", "offset_x", "offset_y"],
|
|
},
|
|
};
|
|
|
|
function setAngleStep(widget, fixed) {
|
|
if (!widget || !widget.options) return;
|
|
|
|
if (fixed) {
|
|
// Newer frontends read step2; older ones use step / 10
|
|
widget.options.step2 = SNAP_DEGREES;
|
|
widget.options.step = SNAP_DEGREES * 10;
|
|
widget.options.round = SNAP_DEGREES;
|
|
widget.value = Math.round(widget.value / SNAP_DEGREES) * SNAP_DEGREES;
|
|
} else {
|
|
widget.options.step2 = 0.1;
|
|
widget.options.step = 1;
|
|
widget.options.round = 0.1;
|
|
}
|
|
}
|
|
|
|
// Hide via the boolean `hidden` flag. That is what the frontend actually
|
|
// checks: isWidgetVisible() and getLayoutWidgets() both key off it, and a
|
|
// hidden widget is dropped from the layout pass entirely.
|
|
//
|
|
// Do NOT hide by setting `type = "hidden"` - there is no such widget type.
|
|
// The widget stays "visible" to the layout pass, gets laid out at zero
|
|
// height, and draws on top of the widgets below it, which pushes the node's
|
|
// preview image up over the sliders.
|
|
function toggleWidget(widget, show) {
|
|
if (!widget) return;
|
|
widget.hidden = !show;
|
|
}
|
|
|
|
app.registerExtension({
|
|
name: "MachinePainting.ImageTransform",
|
|
|
|
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
|
const config = TARGETS[nodeData.name];
|
|
if (!config) return;
|
|
|
|
nodeType.prototype.mpSyncTransform = function (resize) {
|
|
const find = (n) => this.widgets?.find((w) => w.name === n);
|
|
|
|
const method = find("rotation_method");
|
|
setAngleStep(find("rotation_angle"), method?.value === "fixed");
|
|
|
|
if (config.toggle) {
|
|
const enabled = find(config.toggle)?.value === true;
|
|
for (const name of config.controlled) {
|
|
toggleWidget(find(name), enabled);
|
|
}
|
|
// Only reflow on an actual user toggle, so loading a workflow
|
|
// doesn't undo a manually resized node. expandToFitContent is
|
|
// litegraph's own helper: it grows the node to clear the newly
|
|
// shown widgets and never shrinks it, so the preview image
|
|
// keeps the room it already had.
|
|
if (resize) this.expandToFitContent?.();
|
|
}
|
|
|
|
this.setDirtyCanvas(true, true);
|
|
};
|
|
|
|
const onNodeCreated = nodeType.prototype.onNodeCreated;
|
|
nodeType.prototype.onNodeCreated = function () {
|
|
onNodeCreated?.apply(this, arguments);
|
|
|
|
const hook = (name) => {
|
|
const w = this.widgets?.find((x) => x.name === name);
|
|
if (!w) return;
|
|
const original = w.callback;
|
|
const node = this;
|
|
w.callback = function () {
|
|
const r = original?.apply(this, arguments);
|
|
node.mpSyncTransform(true);
|
|
return r;
|
|
};
|
|
};
|
|
|
|
hook("rotation_method");
|
|
if (config.toggle) hook(config.toggle);
|
|
|
|
this.mpSyncTransform(false);
|
|
};
|
|
|
|
// Re-apply after a saved workflow restores widget values
|
|
const onConfigure = nodeType.prototype.onConfigure;
|
|
nodeType.prototype.onConfigure = function () {
|
|
onConfigure?.apply(this, arguments);
|
|
this.mpSyncTransform?.(false);
|
|
};
|
|
},
|
|
});
|