Files
Azornes-Comfyui-LayerForge/tests/test_layer_geometry_characterization.mjs

225 lines
6.5 KiB
JavaScript

import assert from 'node:assert/strict';
import { readFile } from 'node:fs/promises';
import test from 'node:test';
import { CanvasRenderer } from '../js/canvas/canvas_renderer.js';
import { ShapeTool } from '../js/canvas/shape_tool.js';
import {
getBoundsFromPoints,
getLayerWorldBounds,
getLayerWorldCorners,
isPointInRotatedLayer,
localToWorld,
worldToLocal,
} from '../js/utils/common_utils.js';
const canvasLayersSource = await readFile(
new URL('../src/canvas/canvas_layers.ts', import.meta.url),
'utf8'
);
function assertAlmostEqual(actual, expected, epsilon = 1e-9) {
assert.ok(
Math.abs(actual - expected) <= epsilon,
`expected ${actual} to be within ${epsilon} of ${expected}`
);
}
function assertPointAlmostEqual(actual, expected, epsilon = 1e-9) {
assertAlmostEqual(actual.x, expected.x, epsilon);
assertAlmostEqual(actual.y, expected.y, epsilon);
}
function createRendererContext(arcs = []) {
return {
beginPath() {},
arc(x, y, radius, startAngle, endAngle) {
arcs.push({ x, y, radius, startAngle, endAngle });
},
fill() {},
lineTo() {},
moveTo() {},
setLineDash() {},
stroke() {},
strokeRect() {},
};
}
function createRenderer() {
const renderer = Object.create(CanvasRenderer.prototype);
renderer.canvas = {
viewport: { zoom: 1 },
canvasLayers: { getHandles: () => ({}) },
};
renderer.isPointCoveredByHigherLayers = () => false;
return renderer;
}
test('rotated layer hit-test coordinates preserve current boundary behavior', () => {
const layer = {
x: 10,
y: 20,
width: 100,
height: 50,
rotation: 45,
};
const transform = {
centerX: layer.x + layer.width / 2,
centerY: layer.y + layer.height / 2,
rotation: layer.rotation,
};
const insideWorld = localToWorld(layer.width / 2, 0, transform);
const insideLocal = worldToLocal(insideWorld.x, insideWorld.y, transform);
assertPointAlmostEqual(insideLocal, { x: layer.width / 2, y: 0 });
assert.equal(isPointInRotatedLayer(insideWorld.x, insideWorld.y, layer), true);
const outsideWorld = localToWorld(layer.width / 2 + 0.001, 0, transform);
const outsideLocal = worldToLocal(outsideWorld.x, outsideWorld.y, transform);
assert.ok(Math.abs(outsideLocal.x) > layer.width / 2);
assert.equal(isPointInRotatedLayer(outsideWorld.x, outsideWorld.y, layer), false);
});
test('rotated full-layer bounds preserve current dimensions', () => {
const layer = {
x: 10,
y: 20,
width: 100,
height: 50,
rotation: 45,
};
const corners = getLayerWorldCorners(layer);
const bounds = getBoundsFromPoints(corners);
const layerBounds = getLayerWorldBounds(layer);
assertAlmostEqual(bounds.x, 6.966991411008934);
assertAlmostEqual(bounds.x + bounds.width, 113.03300858899107);
assertAlmostEqual(bounds.y, -8.033008588991066);
assertAlmostEqual(bounds.y + bounds.height, 98.03300858899107);
assert.deepEqual(layerBounds, bounds);
assert.equal(Math.ceil(bounds.width), 107);
assert.equal(Math.ceil(bounds.height), 107);
});
test('crop bounds preserve flip-aware layer-local coordinates', () => {
const layer = {
x: 0,
y: 0,
width: 200,
height: 100,
rotation: 0,
cropMode: true,
originalWidth: 400,
originalHeight: 200,
flipH: true,
flipV: false,
cropBounds: { x: 50, y: 20, width: 100, height: 60 },
};
assert.deepEqual(getLayerWorldCorners(layer, { cropAware: true }), [
{ x: 125, y: 10 },
{ x: 175, y: 10 },
{ x: 175, y: 40 },
{ x: 125, y: 40 },
]);
assert.deepEqual(getLayerWorldBounds(layer, { cropAware: true }), {
x: 125,
y: 10,
width: 50,
height: 30,
});
});
test('shape and renderer bounding boxes preserve local and world-space behavior', () => {
const shapeTool = Object.create(ShapeTool.prototype);
shapeTool.shape = {
points: [
{ x: -10, y: 5 },
{ x: 20, y: -15 },
{ x: 4, y: 25 },
],
isClosed: true,
};
assert.deepEqual(shapeTool.getBoundingBox(), {
x: -10,
y: -15,
width: 30,
height: 40,
});
shapeTool.shape = { points: [], isClosed: false };
assert.equal(shapeTool.getBoundingBox(), null);
const renderer = createRenderer();
renderer.canvas.outputAreaBounds = { x: 100, y: 200 };
renderer.canvas.outputAreaExtensionEnabled = true;
renderer.canvas.outputAreaExtensions = { left: 5, top: -2, right: 0, bottom: 0 };
renderer.canvas.outputAreaShape = {
points: [{ x: 0, y: 0 }, { x: 10, y: 5 }],
};
renderer.canvas.batchPreviewManagers = [
{ generationArea: { x: 104, y: 198, width: 2, height: 2 } },
];
assert.equal(renderer.isCustomShapeOverlappingWithBatchAreas(), true);
renderer.canvas.batchPreviewManagers = [
{ generationArea: { x: 200, y: 300, width: 2, height: 2 } },
];
assert.equal(renderer.isCustomShapeOverlappingWithBatchAreas(), false);
});
test('renderer adaptive lines use the layer center and rotation contract', () => {
const renderer = createRenderer();
const observedWorldPoints = [];
renderer.isPointCoveredByHigherLayers = (worldX, worldY) => {
observedWorldPoints.push({ x: worldX, y: worldY });
return false;
};
const layer = { x: 10, y: 20, width: 100, height: 50, rotation: 90 };
renderer.drawAdaptiveLine(createRendererContext(), 0, 0, 16, 0, layer);
const transform = {
centerX: layer.x + layer.width / 2,
centerY: layer.y + layer.height / 2,
rotation: layer.rotation,
};
assertPointAlmostEqual(observedWorldPoints[0], localToWorld(0, 0, transform));
assertPointAlmostEqual(observedWorldPoints.at(-1), localToWorld(16, 0, transform));
});
test('renderer selection handles convert world positions back to local coordinates with flips', () => {
const renderer = createRenderer();
const layer = {
x: 100,
y: 200,
width: 100,
height: 50,
rotation: 90,
flipH: true,
flipV: false,
};
const transform = {
centerX: layer.x + layer.width / 2,
centerY: layer.y + layer.height / 2,
rotation: layer.rotation,
};
const worldHandle = localToWorld(50, 0, transform);
renderer.canvas.canvasLayers.getHandles = () => ({ e: worldHandle });
const arcs = [];
renderer.drawSelectionFrame(createRendererContext(arcs), layer);
assert.equal(arcs.length, 1);
assertAlmostEqual(arcs[0].x, -50);
assertAlmostEqual(arcs[0].y, 0);
});
test('CanvasLayers handle construction uses the canonical point transform', () => {
assert.match(canvasLayersSource, /localToWorld\(/);
assert.doesNotMatch(canvasLayersSource, /const rad = layer\.rotation \* Math\.PI \/ 180/);
assert.match(canvasLayersSource, /worldHandles\[key\] = localToWorld/);
});