Files
ausboss-ComfyUI-AusBoss/tests/transform_geometry.test.mjs
T
ausboss ac8cf4832c Attach rotate knob to the rotated image corner, feather QoL, live fade preview
The rotation knob previously anchored to axis-aligned bounding boxes, so
under rotation or padding it floated in space, visually attached to
nothing. It now rides the actual top-right corner of the rotated image
quad (short stem, offset outward along the corner's radial direction)
and orbits with the image as the angle changes - the grab-the-object's-
corner model design tools use. Verified at 0/30/-60/135 degrees: the
drawn knob sits exactly at the computed rotated-corner position.

Feather quality of life: default is now 24 (a no-op until padding or
rotation creates generated area), and the slider is paired with a number
box with single-step arrows for granular control; both stay in sync and
values above the slider range remain typeable up to 4096.

The sidebar Preview now renders the feather fade live: it builds the
generated-area mask (padding, rotation corners, source alpha), blurs it,
draws it twice to match the engine's edge-pinned ramp, tints it with the
fill color, and overlays - so tweaking feather shows the real blend as
you type. Verified: preview pixels near the crop edge move measurably
toward the fill color as feather increases.
2026-07-25 00:21:08 -05:00

77 lines
3.6 KiB
JavaScript

import assert from "node:assert/strict";
import test from "node:test";
import {
canvasLocalPoint,
cropHandleCenters,
nearestHandle,
paddingHandleCenters,
resetTransformValues,
resizeCrop,
resolveCrop,
resolvePadding,
rotatedSize,
sourceChanged,
zoomAround,
} from "../js/shared/transform_geometry.mjs";
test("rotated size handles positive and negative angles", () => {
assert.deepEqual(rotatedSize(100, 50, 90), { width: 50, height: 100 });
assert.deepEqual(rotatedSize(100, 50, -90), { width: 50, height: 100 });
// Mixed odd/even sides hit Pillow's exact transpose fast path.
assert.deepEqual(rotatedSize(2, 3, -90), { width: 3, height: 2 });
});
test("rotated size matches Pillow expand output exactly", () => {
// Expected values generated with Pillow's Image.rotate(expand=True); see
// the note on rotatedSize. The old width*cos+height*sin formula was 1px
// short on most free angles (e.g. 512@45 gave 725, Pillow produces 726).
assert.deepEqual(rotatedSize(512, 512, 45), { width: 726, height: 726 });
assert.deepEqual(rotatedSize(512, 512, -12.5), { width: 612, height: 612 });
assert.deepEqual(rotatedSize(512, 512, 179.9), { width: 514, height: 514 });
assert.deepEqual(rotatedSize(1920, 1080, 15), { width: 2136, height: 1542 });
assert.deepEqual(rotatedSize(1920, 1080, 60), { width: 1896, height: 2204 });
});
test("crop clamps and honors ratios", () => {
assert.deepEqual(resolveCrop({ crop_x: 95, crop_y: 45, crop_width: 99, crop_height: 99, crop_aspect_ratio: "free" }, { width: 100, height: 50 }), { x: 95, y: 45, width: 5, height: 5 });
assert.deepEqual(resolveCrop({ crop_x: 0, crop_y: 0, crop_width: 0, crop_height: 0, crop_aspect_ratio: "16:9" }, { width: 100, height: 100 }), { x: 0, y: 0, width: 100, height: 56 });
});
test("canvas multiple adds only right and bottom", () => {
assert.deepEqual(resolvePadding({ pad_left: 0, pad_top: 0, pad_right: 0, pad_bottom: 0, canvas_multiple: 8 }, { width: 101, height: 99 }), { left: 0, top: 0, right: 3, bottom: 5, outputWidth: 104, outputHeight: 104 });
});
test("handle priority and closest distance are deterministic", () => {
const selection = nearestHandle({ x: 20, y: 20 }, [
{ kind: "padding", priority: 1, radius: 30, handles: [{ name: "pad_left", x: 20, y: 20 }] },
{ kind: "crop", priority: 2, radius: 30, handles: [{ name: "nw", x: 20, y: 20 }] },
]);
assert.equal(selection.kind, "padding");
});
test("crop resize never leaves the source", () => {
const resized = resizeCrop({ x: 10, y: 10, width: 40, height: 30 }, "se", 1000, 1000, { width: 100, height: 80 });
assert.deepEqual(resized, { x: 10, y: 10, width: 90, height: 70 });
});
test("coordinate conversion accounts for CSS scaling", () => {
const canvas = { clientWidth: 400, clientHeight: 200, getBoundingClientRect: () => ({ left: 10, top: 20, width: 200, height: 100 }) };
assert.deepEqual(canvasLocalPoint(canvas, { clientX: 110, clientY: 70 }), { x: 200, y: 100 });
});
test("zoom remains anchored under the pointer", () => {
assert.deepEqual(zoomAround({ zoom: 1, panX: 0, panY: 0 }, 2, { x: 100, y: 50 }), { zoom: 2, panX: -100, panY: -50 });
});
test("reset and source change restore identity including timeline", () => {
const reset = resetTransformValues(true);
assert.equal(reset.canvas_multiple, 1);
assert.equal(reset.pad_bottom, 0);
assert.equal(reset.feather, 24); // feather defaults on; no-op until padding/rotation exists
assert.equal(reset.frame_index, 0);
assert.equal(sourceChanged("a", "b", true), true);
assert.equal(sourceChanged("a", "b", false), false);
assert.equal(sourceChanged("a", "a", true), false);
});