181 lines
5.0 KiB
TypeScript
181 lines
5.0 KiB
TypeScript
|
|
/**
|
|||
|
|
* ZoomPanController Tests – Zoom & Pan Transformation
|
|||
|
|
*/
|
|||
|
|
import { describe, it, expect, beforeEach } from 'vitest';
|
|||
|
|
import { ZoomPanController } from '../src/canvas/ZoomPanController';
|
|||
|
|
|
|||
|
|
function createMockCanvas(w = 800, h = 600): HTMLCanvasElement {
|
|||
|
|
const canvas = document.createElement('canvas');
|
|||
|
|
canvas.width = w;
|
|||
|
|
canvas.height = h;
|
|||
|
|
const ctx = canvas.getContext('2d');
|
|||
|
|
if (ctx) {
|
|||
|
|
ctx.fillRect = (() => {}) as any;
|
|||
|
|
ctx.strokeRect = (() => {}) as any;
|
|||
|
|
ctx.beginPath = (() => {}) as any;
|
|||
|
|
ctx.moveTo = (() => {}) as any;
|
|||
|
|
ctx.lineTo = (() => {}) as any;
|
|||
|
|
ctx.stroke = (() => {}) as any;
|
|||
|
|
ctx.fill = (() => {}) as any;
|
|||
|
|
ctx.save = (() => {}) as any;
|
|||
|
|
ctx.restore = (() => {}) as any;
|
|||
|
|
ctx.scale = (() => {}) as any;
|
|||
|
|
ctx.arc = (() => {}) as any;
|
|||
|
|
ctx.setLineDash = (() => {}) as any;
|
|||
|
|
ctx.clearRect = (() => {}) as any;
|
|||
|
|
ctx.translate = (() => {}) as any;
|
|||
|
|
ctx.rotate = (() => {}) as any;
|
|||
|
|
}
|
|||
|
|
return canvas;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
describe('ZoomPanController', () => {
|
|||
|
|
let canvas: HTMLCanvasElement;
|
|||
|
|
let zpc: ZoomPanController;
|
|||
|
|
|
|||
|
|
beforeEach(() => {
|
|||
|
|
canvas = createMockCanvas(800, 600);
|
|||
|
|
zpc = new ZoomPanController(canvas);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
describe('initial state', () => {
|
|||
|
|
it('should have scale=1, offsetX=0, offsetY=0', () => {
|
|||
|
|
const t = zpc.getTransform();
|
|||
|
|
expect(t.a).toBe(1);
|
|||
|
|
expect(t.d).toBe(1);
|
|||
|
|
expect(t.e).toBe(0);
|
|||
|
|
expect(t.f).toBe(0);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('getScale should return 1 initially', () => {
|
|||
|
|
expect(zpc.getScale()).toBe(1);
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
describe('getViewport', () => {
|
|||
|
|
it('should return viewport covering full canvas at scale=1', () => {
|
|||
|
|
const vp = zpc.getViewport();
|
|||
|
|
expect(vp.minX).toBe(0);
|
|||
|
|
expect(vp.minY).toBe(0);
|
|||
|
|
expect(vp.maxX).toBe(800);
|
|||
|
|
expect(vp.maxY).toBe(600);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('should shrink visible area when zoomed in', () => {
|
|||
|
|
zpc.zoomAt(400, 300, 2);
|
|||
|
|
const vp = zpc.getViewport();
|
|||
|
|
const w = vp.maxX - vp.minX;
|
|||
|
|
const h = vp.maxY - vp.minY;
|
|||
|
|
expect(w).toBeCloseTo(400, 1);
|
|||
|
|
expect(h).toBeCloseTo(300, 1);
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
describe('zoomAt', () => {
|
|||
|
|
it('should increase scale with factor > 1', () => {
|
|||
|
|
zpc.zoomAt(100, 100, 2);
|
|||
|
|
expect(zpc.getScale()).toBe(2);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('should decrease scale with factor < 1', () => {
|
|||
|
|
zpc.zoomAt(100, 100, 0.5);
|
|||
|
|
expect(zpc.getScale()).toBe(0.5);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('should keep the zoom center point stable', () => {
|
|||
|
|
zpc.zoomAt(400, 300, 2);
|
|||
|
|
const screen = zpc.worldToScreen(400, 300);
|
|||
|
|
expect(screen.x).toBeCloseTo(400, 0);
|
|||
|
|
expect(screen.y).toBeCloseTo(300, 0);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('should clamp scale to minimum 0.01', () => {
|
|||
|
|
zpc.zoomAt(0, 0, 0.001);
|
|||
|
|
expect(zpc.getScale()).toBeGreaterThanOrEqual(0.01);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('should clamp scale to maximum 100', () => {
|
|||
|
|
for (let i = 0; i < 20; i++) {
|
|||
|
|
zpc.zoomAt(400, 300, 10);
|
|||
|
|
}
|
|||
|
|
expect(zpc.getScale()).toBeLessThanOrEqual(100);
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
describe('pan', () => {
|
|||
|
|
it('should update offsetX and offsetY', () => {
|
|||
|
|
zpc.pan(50, 30);
|
|||
|
|
const t = zpc.getTransform();
|
|||
|
|
expect(t.e).toBe(50);
|
|||
|
|
expect(t.f).toBe(30);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('should accumulate pan calls', () => {
|
|||
|
|
zpc.pan(10, 20);
|
|||
|
|
zpc.pan(30, 40);
|
|||
|
|
const t = zpc.getTransform();
|
|||
|
|
expect(t.e).toBe(40);
|
|||
|
|
expect(t.f).toBe(60);
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
describe('reset', () => {
|
|||
|
|
it('should restore scale=1 and offset=0,0', () => {
|
|||
|
|
zpc.zoomAt(100, 100, 3);
|
|||
|
|
zpc.pan(50, 50);
|
|||
|
|
zpc.reset();
|
|||
|
|
const t = zpc.getTransform();
|
|||
|
|
expect(t.a).toBe(1);
|
|||
|
|
expect(t.d).toBe(1);
|
|||
|
|
expect(t.e).toBe(0);
|
|||
|
|
expect(t.f).toBe(0);
|
|||
|
|
expect(zpc.getScale()).toBe(1);
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
describe('worldToScreen & screenToWorld', () => {
|
|||
|
|
it('should convert world to screen at identity transform', () => {
|
|||
|
|
const s = zpc.worldToScreen(100, 200);
|
|||
|
|
expect(s.x).toBe(100);
|
|||
|
|
expect(s.y).toBe(200);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('should convert world to screen with scale and offset', () => {
|
|||
|
|
zpc.zoomAt(0, 0, 2);
|
|||
|
|
zpc.pan(50, 50);
|
|||
|
|
const s = zpc.worldToScreen(100, 100);
|
|||
|
|
expect(s.x).toBe(250);
|
|||
|
|
expect(s.y).toBe(250);
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
describe('zoomFit', () => {
|
|||
|
|
it('should not crash with empty elements', () => {
|
|||
|
|
zpc.zoomFit([]);
|
|||
|
|
expect(zpc.getScale()).toBe(1);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('should fit elements within canvas', () => {
|
|||
|
|
zpc.zoomFit([
|
|||
|
|
{ x: 0, y: 0, width: 200, height: 200 },
|
|||
|
|
{ x: 400, y: 400, width: 200, height: 200 },
|
|||
|
|
]);
|
|||
|
|
expect(zpc.getScale()).toBeGreaterThan(0);
|
|||
|
|
expect(zpc.getScale()).toBeLessThan(100);
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
describe('zoomToRect', () => {
|
|||
|
|
it('should zoom to a given rect', () => {
|
|||
|
|
zpc.zoomToRect({ minX: 0, minY: 0, maxX: 400, maxY: 300 });
|
|||
|
|
expect(zpc.getScale()).toBeGreaterThan(0);
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
it('should do nothing for zero-size rect', () => {
|
|||
|
|
const before = zpc.getScale();
|
|||
|
|
zpc.zoomToRect({ minX: 0, minY: 0, maxX: 0, maxY: 0 });
|
|||
|
|
expect(zpc.getScale()).toBe(before);
|
|||
|
|
});
|
|||
|
|
});
|
|||
|
|
});
|