Files
ttstd-infinite-canvas/web/test/image-operation-plan.test.ts

65 lines
4.0 KiB
TypeScript

import { describe, expect, test } from "bun:test";
import { planImageTiles, resolveCropPixels, resolveUpscaleSize } from "../src/lib/canvas/image-operation-plan";
import { resizeSelection, selectAspect, translateSelection, type CropGrip } from "../src/lib/canvas/image-crop-selection";
import { invertSelectionAlpha } from "../src/lib/canvas/image-mask-document";
describe("图片工具像素合同", () => {
test("奇数尺寸网格完整覆盖原图且像素不重叠", () => {
const size = { width: 17, height: 13 };
const tiles = planImageTiles(size, { rows: 3, columns: 4 });
const coverage = new Uint8Array(size.width * size.height);
for (const tile of tiles) {
for (let y = tile.y; y < tile.y + tile.height; y++) {
for (let x = tile.x; x < tile.x + tile.width; x++) coverage[y * size.width + x]++;
}
}
expect(tiles).toHaveLength(12);
expect(coverage.every((count) => count === 1)).toBe(true);
expect(tiles.map(({ row, column }) => [row, column])).toEqual(Array.from({ length: 12 }, (_, i) => [Math.floor(i / 4), i % 4]));
});
test("拒绝零面积、非有限参数及超像素网格", () => {
for (const value of [0, NaN, Infinity, -1, 13, 1.5]) expect(() => planImageTiles({ width: 20, height: 20 }, { rows: value, columns: 2 })).toThrow();
expect(() => planImageTiles({ width: 1, height: 1 }, { rows: 2, columns: 1 })).toThrow();
expect(() => resolveUpscaleSize(0, 100, 1000)).toThrow();
expect(() => resolveUpscaleSize(100, 100, Infinity)).toThrow();
});
test("裁剪夹取图内像素,默认居中方形", () => {
expect(resolveCropPixels({ width: 9, height: 4 })).toEqual({ x: 2, y: 0, width: 4, height: 4 });
expect(resolveCropPixels({ width: 10, height: 10 }, { x: -0.1, y: 0.8, width: 0.3, height: 0.5 })).toEqual({ x: 0, y: 8, width: 2, height: 2 });
expect(() => resolveCropPixels({ width: 10, height: 10 }, { x: 2, y: 0, width: 0.1, height: 1 })).toThrow();
});
test("缩放保持宽高比并限制长边", () => {
expect(resolveUpscaleSize(300, 200, 9000)).toEqual({ width: 4096, height: 2731 });
expect(resolveUpscaleSize(200, 300, 1024)).toEqual({ width: 683, height: 1024 });
});
test("锁定比例时八个手柄在各种拖动下都保持比例与图内边界", () => {
const box = { x: 0.1, y: 0.2, width: 0.6, height: 0.4 };
for (const x of [-1, 0, 1] as const) for (const y of [-1, 0, 1] as const) {
if (!x && !y) continue;
for (const dx of [-3, -0.1, 0, 0.1, 3]) for (const dy of [-3, -0.1, 0, 0.1, 3]) {
const next = resizeSelection(box, [x, y] as CropGrip, dx, dy, true);
expect(next.width / next.height).toBeCloseTo(1.5, 8);
expect(next.x).toBeGreaterThanOrEqual(-1e-12);
expect(next.y).toBeGreaterThanOrEqual(-1e-12);
expect(next.x + next.width).toBeLessThanOrEqual(1 + 1e-12);
expect(next.y + next.height).toBeLessThanOrEqual(1 + 1e-12);
}
}
});
test("极窄比例切换后仍可自由拖动手柄", () => {
const box = selectAspect({ x: 0.1, y: 0.1, width: 0.8, height: 0.8 }, 0.02);
expect(box.width).toBeLessThan(0.06);
const resized = resizeSelection(box, [-1, 0], 0, 0, false);
expect(resized.x).toBeGreaterThanOrEqual(0);
expect(resized.width).toBeGreaterThan(0);
expect(resized.x + resized.width).toBeLessThanOrEqual(1);
expect(translateSelection(box, -100, 100).y + box.height).toBeCloseTo(1);
});
test("遮罩选区转换为透明,未选区保持白色不透明,空选区拒绝", () => {
const input = new Uint8ClampedArray([0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 255]);
expect([...invertSelectionAlpha(input)]).toEqual([255, 255, 255, 255, 255, 255, 255, 0, 255, 255, 255, 0]);
expect(input[7]).toBe(12);
expect(() => invertSelectionAlpha(new Uint8ClampedArray(8))).toThrow();
});
});