1bcfaffdd4
- Fix frontend Network error: API_BASE now defaults to empty string (same-origin) - Fix 3 files: api.ts, AuthContext.tsx, Dashboard.tsx - Add Canvas 2D context mock in tests/setup.ts for jsdom - Fix -0 vs +0 in ZoomPanController.getViewport() - Add IntegrationWorkflow.test.ts (35 tests, full CAD workflow)
100 lines
3.0 KiB
TypeScript
100 lines
3.0 KiB
TypeScript
import type { Transform, Viewport } from '../types/cad.types';
|
|
|
|
export class ZoomPanController {
|
|
private scale = 1;
|
|
private offsetX = 0;
|
|
private offsetY = 0;
|
|
private canvas: HTMLCanvasElement;
|
|
private isPanning = false;
|
|
private lastX = 0;
|
|
private lastY = 0;
|
|
|
|
constructor(canvas: HTMLCanvasElement) {
|
|
this.canvas = canvas;
|
|
}
|
|
|
|
getTransform(): Transform {
|
|
return {
|
|
a: this.scale, b: 0, c: 0,
|
|
d: this.scale, e: this.offsetX, f: this.offsetY,
|
|
};
|
|
}
|
|
|
|
getViewport(): Viewport {
|
|
const w = this.canvas.width / this.scale;
|
|
const h = this.canvas.height / this.scale;
|
|
const minX = -this.offsetX / this.scale || 0;
|
|
const minY = -this.offsetY / this.scale || 0;
|
|
return {
|
|
minX,
|
|
minY,
|
|
maxX: minX + w,
|
|
maxY: minY + h,
|
|
};
|
|
}
|
|
|
|
getScale(): number { return this.scale; }
|
|
|
|
zoomAt(cx: number, cy: number, factor: number): void {
|
|
const newScale = Math.max(0.01, Math.min(100, this.scale * factor));
|
|
const ratio = newScale / this.scale;
|
|
this.offsetX = cx - (cx - this.offsetX) * ratio;
|
|
this.offsetY = cy - (cy - this.offsetY) * ratio;
|
|
this.scale = newScale;
|
|
}
|
|
|
|
zoomFit(elements: Array<{x:number;y:number;width:number;height:number}>): void {
|
|
if (elements.length === 0) return;
|
|
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
for (const el of elements) {
|
|
minX = Math.min(minX, el.x - el.width/2);
|
|
minY = Math.min(minY, el.y - el.height/2);
|
|
maxX = Math.max(maxX, el.x + el.width/2);
|
|
maxY = Math.max(maxY, el.y + el.height/2);
|
|
}
|
|
const padding = 40;
|
|
const scaleX = (this.canvas.width - padding*2) / (maxX - minX);
|
|
const scaleY = (this.canvas.height - padding*2) / (maxY - minY);
|
|
this.scale = Math.min(scaleX, scaleY);
|
|
this.offsetX = -minX * this.scale + padding;
|
|
this.offsetY = -minY * this.scale + padding;
|
|
}
|
|
|
|
zoomToRect(rect: { minX: number; minY: number; maxX: number; maxY: number }): void {
|
|
const padding = 20;
|
|
const w = rect.maxX - rect.minX;
|
|
const h = rect.maxY - rect.minY;
|
|
if (w <= 0 || h <= 0) return;
|
|
const scaleX = (this.canvas.width - padding * 2) / w;
|
|
const scaleY = (this.canvas.height - padding * 2) / h;
|
|
this.scale = Math.max(0.01, Math.min(100, Math.min(scaleX, scaleY)));
|
|
this.offsetX = -rect.minX * this.scale + padding;
|
|
this.offsetY = -rect.minY * this.scale + padding;
|
|
}
|
|
|
|
pan(dx: number, dy: number): void {
|
|
this.offsetX += dx;
|
|
this.offsetY += dy;
|
|
}
|
|
|
|
screenToWorld(sx: number, sy: number): { x: number; y: number } {
|
|
const rect = this.canvas.getBoundingClientRect();
|
|
const x = (sx - rect.left - this.offsetX) / this.scale;
|
|
const y = (sy - rect.top - this.offsetY) / this.scale;
|
|
return { x, y };
|
|
}
|
|
|
|
worldToScreen(wx: number, wy: number): { x: number; y: number } {
|
|
return {
|
|
x: wx * this.scale + this.offsetX,
|
|
y: wy * this.scale + this.offsetY,
|
|
};
|
|
}
|
|
|
|
reset(): void {
|
|
this.scale = 1;
|
|
this.offsetX = 0;
|
|
this.offsetY = 0;
|
|
}
|
|
}
|