resolve TypeScript errors and memory leaks

Fixed all TypeScript compilation errors by defining a dedicated TransformOrigin type and adding proper null checks. Implemented comprehensive event handler cleanup to prevent memory leaks and improved cross-platform support with Meta key handling for macOS users.
This commit is contained in:
Dariusz L
2025-08-08 13:15:21 +02:00
parent 11dd554204
commit e4f44c10e8
2 changed files with 161 additions and 85 deletions

View File

@@ -3,16 +3,33 @@ import { snapToGrid, getSnapAdjustment } from "./utils/CommonUtils.js";
const log = createModuleLogger('CanvasInteractions'); const log = createModuleLogger('CanvasInteractions');
export class CanvasInteractions { export class CanvasInteractions {
constructor(canvas) { constructor(canvas) {
// Bound event handlers to enable proper removeEventListener and avoid leaks
this.onMouseDown = (e) => this.handleMouseDown(e);
this.onMouseMove = (e) => this.handleMouseMove(e);
this.onMouseUp = (e) => this.handleMouseUp(e);
this.onMouseEnter = (e) => { this.canvas.isMouseOver = true; this.handleMouseEnter(e); };
this.onMouseLeave = (e) => { this.canvas.isMouseOver = false; this.handleMouseLeave(e); };
this.onWheel = (e) => this.handleWheel(e);
this.onKeyDown = (e) => this.handleKeyDown(e);
this.onKeyUp = (e) => this.handleKeyUp(e);
this.onDragOver = (e) => this.handleDragOver(e);
this.onDragEnter = (e) => this.handleDragEnter(e);
this.onDragLeave = (e) => this.handleDragLeave(e);
this.onDrop = (e) => { this.handleDrop(e); };
this.onContextMenu = (e) => this.handleContextMenu(e);
this.onBlur = () => this.handleBlur();
this.onPaste = (e) => this.handlePasteEvent(e);
this.canvas = canvas; this.canvas = canvas;
this.interaction = { this.interaction = {
mode: 'none', mode: 'none',
panStart: { x: 0, y: 0 }, panStart: { x: 0, y: 0 },
dragStart: { x: 0, y: 0 }, dragStart: { x: 0, y: 0 },
transformOrigin: {}, transformOrigin: null,
resizeHandle: null, resizeHandle: null,
resizeAnchor: { x: 0, y: 0 }, resizeAnchor: { x: 0, y: 0 },
canvasResizeStart: { x: 0, y: 0 }, canvasResizeStart: { x: 0, y: 0 },
isCtrlPressed: false, isCtrlPressed: false,
isMetaPressed: false,
isAltPressed: false, isAltPressed: false,
isShiftPressed: false, isShiftPressed: false,
isSPressed: false, isSPressed: false,
@@ -37,7 +54,6 @@ export class CanvasInteractions {
e.stopPropagation(); e.stopPropagation();
} }
performZoomOperation(worldCoords, zoomFactor) { performZoomOperation(worldCoords, zoomFactor) {
const rect = this.canvas.canvas.getBoundingClientRect();
const mouseBufferX = (worldCoords.x - this.canvas.viewport.x) * this.canvas.viewport.zoom; const mouseBufferX = (worldCoords.x - this.canvas.viewport.x) * this.canvas.viewport.zoom;
const mouseBufferY = (worldCoords.y - this.canvas.viewport.y) * this.canvas.viewport.zoom; const mouseBufferY = (worldCoords.y - this.canvas.viewport.y) * this.canvas.viewport.zoom;
const newZoom = Math.max(0.1, Math.min(10, this.canvas.viewport.zoom * zoomFactor)); const newZoom = Math.max(0.1, Math.min(10, this.canvas.viewport.zoom * zoomFactor));
@@ -64,29 +80,39 @@ export class CanvasInteractions {
} }
} }
setupEventListeners() { setupEventListeners() {
this.canvas.canvas.addEventListener('mousedown', this.handleMouseDown.bind(this)); this.canvas.canvas.addEventListener('mousedown', this.onMouseDown);
this.canvas.canvas.addEventListener('mousemove', this.handleMouseMove.bind(this)); this.canvas.canvas.addEventListener('mousemove', this.onMouseMove);
this.canvas.canvas.addEventListener('mouseup', this.handleMouseUp.bind(this)); this.canvas.canvas.addEventListener('mouseup', this.onMouseUp);
this.canvas.canvas.addEventListener('mouseleave', this.handleMouseLeave.bind(this)); this.canvas.canvas.addEventListener('wheel', this.onWheel, { passive: false });
this.canvas.canvas.addEventListener('wheel', this.handleWheel.bind(this), { passive: false }); this.canvas.canvas.addEventListener('keydown', this.onKeyDown);
this.canvas.canvas.addEventListener('keydown', this.handleKeyDown.bind(this)); this.canvas.canvas.addEventListener('keyup', this.onKeyUp);
this.canvas.canvas.addEventListener('keyup', this.handleKeyUp.bind(this));
// Add a blur event listener to the window to reset key states // Add a blur event listener to the window to reset key states
window.addEventListener('blur', this.handleBlur.bind(this)); window.addEventListener('blur', this.onBlur);
document.addEventListener('paste', this.handlePasteEvent.bind(this)); document.addEventListener('paste', this.onPaste);
this.canvas.canvas.addEventListener('mouseenter', (e) => { this.canvas.canvas.addEventListener('mouseenter', this.onMouseEnter);
this.canvas.isMouseOver = true; this.canvas.canvas.addEventListener('mouseleave', this.onMouseLeave);
this.handleMouseEnter(e); this.canvas.canvas.addEventListener('dragover', this.onDragOver);
}); this.canvas.canvas.addEventListener('dragenter', this.onDragEnter);
this.canvas.canvas.addEventListener('mouseleave', (e) => { this.canvas.canvas.addEventListener('dragleave', this.onDragLeave);
this.canvas.isMouseOver = false; this.canvas.canvas.addEventListener('drop', this.onDrop);
this.handleMouseLeave(e); this.canvas.canvas.addEventListener('contextmenu', this.onContextMenu);
}); }
this.canvas.canvas.addEventListener('dragover', this.handleDragOver.bind(this)); teardownEventListeners() {
this.canvas.canvas.addEventListener('dragenter', this.handleDragEnter.bind(this)); this.canvas.canvas.removeEventListener('mousedown', this.onMouseDown);
this.canvas.canvas.addEventListener('dragleave', this.handleDragLeave.bind(this)); this.canvas.canvas.removeEventListener('mousemove', this.onMouseMove);
this.canvas.canvas.addEventListener('drop', this.handleDrop.bind(this)); this.canvas.canvas.removeEventListener('mouseup', this.onMouseUp);
this.canvas.canvas.addEventListener('contextmenu', this.handleContextMenu.bind(this)); this.canvas.canvas.removeEventListener('wheel', this.onWheel);
this.canvas.canvas.removeEventListener('keydown', this.onKeyDown);
this.canvas.canvas.removeEventListener('keyup', this.onKeyUp);
window.removeEventListener('blur', this.onBlur);
document.removeEventListener('paste', this.onPaste);
this.canvas.canvas.removeEventListener('mouseenter', this.onMouseEnter);
this.canvas.canvas.removeEventListener('mouseleave', this.onMouseLeave);
this.canvas.canvas.removeEventListener('dragover', this.onDragOver);
this.canvas.canvas.removeEventListener('dragenter', this.onDragEnter);
this.canvas.canvas.removeEventListener('dragleave', this.onDragLeave);
this.canvas.canvas.removeEventListener('drop', this.onDrop);
this.canvas.canvas.removeEventListener('contextmenu', this.onContextMenu);
} }
/** /**
* Sprawdza czy punkt znajduje się w obszarze któregokolwiek z zaznaczonych layerów * Sprawdza czy punkt znajduje się w obszarze któregokolwiek z zaznaczonych layerów
@@ -163,7 +189,7 @@ export class CanvasInteractions {
} }
return; return;
} }
if (e.button !== 0) { // Środkowy przycisk if (e.button === 1) { // Środkowy przycisk
this.startPanning(e); this.startPanning(e);
return; return;
} }
@@ -179,7 +205,7 @@ export class CanvasInteractions {
return; return;
} }
// 4. Domyślna akcja na tle (lewy przycisk bez modyfikatorów) // 4. Domyślna akcja na tle (lewy przycisk bez modyfikatorów)
this.startPanningOrClearSelection(e); this.startPanning(e, true); // clearSelection = true
} }
handleMouseMove(e) { handleMouseMove(e) {
const coords = this.getMouseCoordinates(e); const coords = this.getMouseCoordinates(e);
@@ -376,7 +402,7 @@ export class CanvasInteractions {
} }
} }
calculateGridBasedScaling(oldHeight, deltaY) { calculateGridBasedScaling(oldHeight, deltaY) {
const gridSize = 64; const gridSize = 64; // Grid size - could be made configurable in the future
const direction = deltaY > 0 ? -1 : 1; const direction = deltaY > 0 ? -1 : 1;
let targetHeight; let targetHeight;
if (direction > 0) { if (direction > 0) {
@@ -401,6 +427,8 @@ export class CanvasInteractions {
handleKeyDown(e) { handleKeyDown(e) {
if (e.key === 'Control') if (e.key === 'Control')
this.interaction.isCtrlPressed = true; this.interaction.isCtrlPressed = true;
if (e.key === 'Meta')
this.interaction.isMetaPressed = true;
if (e.key === 'Shift') if (e.key === 'Shift')
this.interaction.isShiftPressed = true; this.interaction.isShiftPressed = true;
if (e.key === 'Alt') { if (e.key === 'Alt') {
@@ -485,6 +513,8 @@ export class CanvasInteractions {
handleKeyUp(e) { handleKeyUp(e) {
if (e.key === 'Control') if (e.key === 'Control')
this.interaction.isCtrlPressed = false; this.interaction.isCtrlPressed = false;
if (e.key === 'Meta')
this.interaction.isMetaPressed = false;
if (e.key === 'Shift') if (e.key === 'Shift')
this.interaction.isShiftPressed = false; this.interaction.isShiftPressed = false;
if (e.key === 'Alt') if (e.key === 'Alt')
@@ -504,6 +534,7 @@ export class CanvasInteractions {
handleBlur() { handleBlur() {
log.debug('Window lost focus, resetting key states.'); log.debug('Window lost focus, resetting key states.');
this.interaction.isCtrlPressed = false; this.interaction.isCtrlPressed = false;
this.interaction.isMetaPressed = false;
this.interaction.isAltPressed = false; this.interaction.isAltPressed = false;
this.interaction.isShiftPressed = false; this.interaction.isShiftPressed = false;
this.interaction.isSPressed = false; this.interaction.isSPressed = false;
@@ -525,6 +556,11 @@ export class CanvasInteractions {
} }
} }
updateCursor(worldCoords) { updateCursor(worldCoords) {
// If actively rotating, show grabbing cursor
if (this.interaction.mode === 'rotating') {
this.canvas.canvas.style.cursor = 'grabbing';
return;
}
const transformTarget = this.canvas.canvasLayers.getHandleAtPosition(worldCoords.x, worldCoords.y); const transformTarget = this.canvas.canvasLayers.getHandleAtPosition(worldCoords.x, worldCoords.y);
if (transformTarget) { if (transformTarget) {
const handleName = transformTarget.handle; const handleName = transformTarget.handle;
@@ -572,7 +608,8 @@ export class CanvasInteractions {
} }
prepareForDrag(layer, worldCoords) { prepareForDrag(layer, worldCoords) {
// Zaktualizuj zaznaczenie, ale nie zapisuj stanu // Zaktualizuj zaznaczenie, ale nie zapisuj stanu
if (this.interaction.isCtrlPressed) { // Support both Ctrl (Windows/Linux) and Cmd (macOS) for multi-selection
if (this.interaction.isCtrlPressed || this.interaction.isMetaPressed) {
const index = this.canvas.canvasSelection.selectedLayers.indexOf(layer); const index = this.canvas.canvasSelection.selectedLayers.indexOf(layer);
if (index === -1) { if (index === -1) {
this.canvas.canvasSelection.updateSelection([...this.canvas.canvasSelection.selectedLayers, layer]); this.canvas.canvasSelection.updateSelection([...this.canvas.canvasSelection.selectedLayers, layer]);
@@ -590,10 +627,9 @@ export class CanvasInteractions {
this.interaction.mode = 'potential-drag'; this.interaction.mode = 'potential-drag';
this.interaction.dragStart = { ...worldCoords }; this.interaction.dragStart = { ...worldCoords };
} }
startPanningOrClearSelection(e) { startPanning(e, clearSelection = true) {
// Ta funkcja jest teraz wywoływana tylko gdy kliknięto na tło bez modyfikatorów. // Unified panning method - can optionally clear selection
// Domyślna akcja: wyczyść zaznaczenie i rozpocznij panoramowanie. if (clearSelection && !this.interaction.isCtrlPressed) {
if (!this.interaction.isCtrlPressed) {
this.canvas.canvasSelection.updateSelection([]); this.canvas.canvasSelection.updateSelection([]);
} }
this.interaction.mode = 'panning'; this.interaction.mode = 'panning';
@@ -642,13 +678,6 @@ export class CanvasInteractions {
this.canvas.render(); this.canvas.render();
this.canvas.saveState(); this.canvas.saveState();
} }
startPanning(e) {
if (!this.interaction.isCtrlPressed) {
this.canvas.canvasSelection.updateSelection([]);
}
this.interaction.mode = 'panning';
this.interaction.panStart = { x: e.clientX, y: e.clientY };
}
panViewport(e) { panViewport(e) {
const dx = e.clientX - this.interaction.panStart.x; const dx = e.clientX - this.interaction.panStart.x;
const dy = e.clientY - this.interaction.panStart.y; const dy = e.clientY - this.interaction.panStart.y;
@@ -709,7 +738,7 @@ export class CanvasInteractions {
mouseY = Math.abs(mouseY - snapToGrid(mouseY)) < snapThreshold ? snapToGrid(mouseY) : mouseY; mouseY = Math.abs(mouseY - snapToGrid(mouseY)) < snapThreshold ? snapToGrid(mouseY) : mouseY;
} }
const o = this.interaction.transformOrigin; const o = this.interaction.transformOrigin;
if (o.rotation === undefined || o.width === undefined || o.height === undefined || o.centerX === undefined || o.centerY === undefined) if (!o)
return; return;
const handle = this.interaction.resizeHandle; const handle = this.interaction.resizeHandle;
const anchor = this.interaction.resizeAnchor; const anchor = this.interaction.resizeAnchor;
@@ -856,7 +885,7 @@ export class CanvasInteractions {
if (!layer) if (!layer)
return; return;
const o = this.interaction.transformOrigin; const o = this.interaction.transformOrigin;
if (o.rotation === undefined || o.centerX === undefined || o.centerY === undefined) if (!o)
return; return;
const startAngle = Math.atan2(this.interaction.dragStart.y - o.centerY, this.interaction.dragStart.x - o.centerX); const startAngle = Math.atan2(this.interaction.dragStart.y - o.centerY, this.interaction.dragStart.x - o.centerX);
const currentAngle = Math.atan2(worldCoords.y - o.centerY, worldCoords.x - o.centerX); const currentAngle = Math.atan2(worldCoords.y - o.centerY, worldCoords.x - o.centerX);

View File

@@ -10,15 +10,29 @@ interface MouseCoordinates {
view: Point; view: Point;
} }
interface TransformOrigin {
x: number;
y: number;
width: number;
height: number;
rotation: number;
centerX: number;
centerY: number;
originalWidth?: number;
originalHeight?: number;
cropBounds?: { x: number; y: number; width: number; height: number };
}
interface InteractionState { interface InteractionState {
mode: 'none' | 'panning' | 'dragging' | 'resizing' | 'rotating' | 'drawingMask' | 'resizingCanvas' | 'movingCanvas' | 'potential-drag' | 'drawingShape'; mode: 'none' | 'panning' | 'dragging' | 'resizing' | 'rotating' | 'drawingMask' | 'resizingCanvas' | 'movingCanvas' | 'potential-drag' | 'drawingShape';
panStart: Point; panStart: Point;
dragStart: Point; dragStart: Point;
transformOrigin: Partial<Layer> & { centerX?: number, centerY?: number }; transformOrigin: TransformOrigin | null;
resizeHandle: string | null; resizeHandle: string | null;
resizeAnchor: Point; resizeAnchor: Point;
canvasResizeStart: Point; canvasResizeStart: Point;
isCtrlPressed: boolean; isCtrlPressed: boolean;
isMetaPressed: boolean;
isAltPressed: boolean; isAltPressed: boolean;
isShiftPressed: boolean; isShiftPressed: boolean;
isSPressed: boolean; isSPressed: boolean;
@@ -35,17 +49,35 @@ export class CanvasInteractions {
public interaction: InteractionState; public interaction: InteractionState;
private originalLayerPositions: Map<Layer, Point>; private originalLayerPositions: Map<Layer, Point>;
// Bound event handlers to enable proper removeEventListener and avoid leaks
private onMouseDown = (e: MouseEvent) => this.handleMouseDown(e);
private onMouseMove = (e: MouseEvent) => this.handleMouseMove(e);
private onMouseUp = (e: MouseEvent) => this.handleMouseUp(e);
private onMouseEnter = (e: MouseEvent) => { this.canvas.isMouseOver = true; this.handleMouseEnter(e); };
private onMouseLeave = (e: MouseEvent) => { this.canvas.isMouseOver = false; this.handleMouseLeave(e); };
private onWheel = (e: WheelEvent) => this.handleWheel(e);
private onKeyDown = (e: KeyboardEvent) => this.handleKeyDown(e);
private onKeyUp = (e: KeyboardEvent) => this.handleKeyUp(e);
private onDragOver = (e: DragEvent) => this.handleDragOver(e);
private onDragEnter = (e: DragEvent) => this.handleDragEnter(e);
private onDragLeave = (e: DragEvent) => this.handleDragLeave(e);
private onDrop = (e: DragEvent) => { this.handleDrop(e); };
private onContextMenu = (e: MouseEvent) => this.handleContextMenu(e);
private onBlur = () => this.handleBlur();
private onPaste = (e: ClipboardEvent) => this.handlePasteEvent(e);
constructor(canvas: Canvas) { constructor(canvas: Canvas) {
this.canvas = canvas; this.canvas = canvas;
this.interaction = { this.interaction = {
mode: 'none', mode: 'none',
panStart: { x: 0, y: 0 }, panStart: { x: 0, y: 0 },
dragStart: { x: 0, y: 0 }, dragStart: { x: 0, y: 0 },
transformOrigin: {}, transformOrigin: null,
resizeHandle: null, resizeHandle: null,
resizeAnchor: { x: 0, y: 0 }, resizeAnchor: { x: 0, y: 0 },
canvasResizeStart: { x: 0, y: 0 }, canvasResizeStart: { x: 0, y: 0 },
isCtrlPressed: false, isCtrlPressed: false,
isMetaPressed: false,
isAltPressed: false, isAltPressed: false,
isShiftPressed: false, isShiftPressed: false,
isSPressed: false, isSPressed: false,
@@ -74,7 +106,6 @@ export class CanvasInteractions {
} }
private performZoomOperation(worldCoords: Point, zoomFactor: number): void { private performZoomOperation(worldCoords: Point, zoomFactor: number): void {
const rect = this.canvas.canvas.getBoundingClientRect();
const mouseBufferX = (worldCoords.x - this.canvas.viewport.x) * this.canvas.viewport.zoom; const mouseBufferX = (worldCoords.x - this.canvas.viewport.x) * this.canvas.viewport.zoom;
const mouseBufferY = (worldCoords.y - this.canvas.viewport.y) * this.canvas.viewport.zoom; const mouseBufferY = (worldCoords.y - this.canvas.viewport.y) * this.canvas.viewport.zoom;
@@ -106,34 +137,49 @@ export class CanvasInteractions {
} }
setupEventListeners(): void { setupEventListeners(): void {
this.canvas.canvas.addEventListener('mousedown', this.handleMouseDown.bind(this) as EventListener); this.canvas.canvas.addEventListener('mousedown', this.onMouseDown as EventListener);
this.canvas.canvas.addEventListener('mousemove', this.handleMouseMove.bind(this) as EventListener); this.canvas.canvas.addEventListener('mousemove', this.onMouseMove as EventListener);
this.canvas.canvas.addEventListener('mouseup', this.handleMouseUp.bind(this) as EventListener); this.canvas.canvas.addEventListener('mouseup', this.onMouseUp as EventListener);
this.canvas.canvas.addEventListener('mouseleave', this.handleMouseLeave.bind(this) as EventListener); this.canvas.canvas.addEventListener('wheel', this.onWheel as EventListener, { passive: false });
this.canvas.canvas.addEventListener('wheel', this.handleWheel.bind(this) as EventListener, { passive: false }); this.canvas.canvas.addEventListener('keydown', this.onKeyDown as EventListener);
this.canvas.canvas.addEventListener('keydown', this.handleKeyDown.bind(this) as EventListener); this.canvas.canvas.addEventListener('keyup', this.onKeyUp as EventListener);
this.canvas.canvas.addEventListener('keyup', this.handleKeyUp.bind(this) as EventListener);
// Add a blur event listener to the window to reset key states // Add a blur event listener to the window to reset key states
window.addEventListener('blur', this.handleBlur.bind(this)); window.addEventListener('blur', this.onBlur);
document.addEventListener('paste', this.handlePasteEvent.bind(this)); document.addEventListener('paste', this.onPaste as unknown as EventListener);
this.canvas.canvas.addEventListener('mouseenter', (e: MouseEvent) => { this.canvas.canvas.addEventListener('mouseenter', this.onMouseEnter as EventListener);
this.canvas.isMouseOver = true; this.canvas.canvas.addEventListener('mouseleave', this.onMouseLeave as EventListener);
this.handleMouseEnter(e);
});
this.canvas.canvas.addEventListener('mouseleave', (e: MouseEvent) => {
this.canvas.isMouseOver = false;
this.handleMouseLeave(e);
});
this.canvas.canvas.addEventListener('dragover', this.handleDragOver.bind(this) as EventListener); this.canvas.canvas.addEventListener('dragover', this.onDragOver as EventListener);
this.canvas.canvas.addEventListener('dragenter', this.handleDragEnter.bind(this) as EventListener); this.canvas.canvas.addEventListener('dragenter', this.onDragEnter as EventListener);
this.canvas.canvas.addEventListener('dragleave', this.handleDragLeave.bind(this) as EventListener); this.canvas.canvas.addEventListener('dragleave', this.onDragLeave as EventListener);
this.canvas.canvas.addEventListener('drop', this.handleDrop.bind(this) as unknown as EventListener); this.canvas.canvas.addEventListener('drop', this.onDrop as unknown as EventListener);
this.canvas.canvas.addEventListener('contextmenu', this.handleContextMenu.bind(this) as EventListener); this.canvas.canvas.addEventListener('contextmenu', this.onContextMenu as EventListener);
}
teardownEventListeners(): void {
this.canvas.canvas.removeEventListener('mousedown', this.onMouseDown as EventListener);
this.canvas.canvas.removeEventListener('mousemove', this.onMouseMove as EventListener);
this.canvas.canvas.removeEventListener('mouseup', this.onMouseUp as EventListener);
this.canvas.canvas.removeEventListener('wheel', this.onWheel as EventListener);
this.canvas.canvas.removeEventListener('keydown', this.onKeyDown as EventListener);
this.canvas.canvas.removeEventListener('keyup', this.onKeyUp as EventListener);
window.removeEventListener('blur', this.onBlur);
document.removeEventListener('paste', this.onPaste as unknown as EventListener);
this.canvas.canvas.removeEventListener('mouseenter', this.onMouseEnter as EventListener);
this.canvas.canvas.removeEventListener('mouseleave', this.onMouseLeave as EventListener);
this.canvas.canvas.removeEventListener('dragover', this.onDragOver as EventListener);
this.canvas.canvas.removeEventListener('dragenter', this.onDragEnter as EventListener);
this.canvas.canvas.removeEventListener('dragleave', this.onDragLeave as EventListener);
this.canvas.canvas.removeEventListener('drop', this.onDrop as unknown as EventListener);
this.canvas.canvas.removeEventListener('contextmenu', this.onContextMenu as EventListener);
} }
/** /**
@@ -222,7 +268,7 @@ export class CanvasInteractions {
} }
return; return;
} }
if (e.button !== 0) { // Środkowy przycisk if (e.button === 1) { // Środkowy przycisk
this.startPanning(e); this.startPanning(e);
return; return;
} }
@@ -241,7 +287,7 @@ export class CanvasInteractions {
} }
// 4. Domyślna akcja na tle (lewy przycisk bez modyfikatorów) // 4. Domyślna akcja na tle (lewy przycisk bez modyfikatorów)
this.startPanningOrClearSelection(e); this.startPanning(e, true); // clearSelection = true
} }
handleMouseMove(e: MouseEvent): void { handleMouseMove(e: MouseEvent): void {
@@ -462,7 +508,7 @@ export class CanvasInteractions {
} }
private calculateGridBasedScaling(oldHeight: number, deltaY: number): number { private calculateGridBasedScaling(oldHeight: number, deltaY: number): number {
const gridSize = 64; const gridSize = 64; // Grid size - could be made configurable in the future
const direction = deltaY > 0 ? -1 : 1; const direction = deltaY > 0 ? -1 : 1;
let targetHeight; let targetHeight;
@@ -487,6 +533,7 @@ export class CanvasInteractions {
handleKeyDown(e: KeyboardEvent): void { handleKeyDown(e: KeyboardEvent): void {
if (e.key === 'Control') this.interaction.isCtrlPressed = true; if (e.key === 'Control') this.interaction.isCtrlPressed = true;
if (e.key === 'Meta') this.interaction.isMetaPressed = true;
if (e.key === 'Shift') this.interaction.isShiftPressed = true; if (e.key === 'Shift') this.interaction.isShiftPressed = true;
if (e.key === 'Alt') { if (e.key === 'Alt') {
this.interaction.isAltPressed = true; this.interaction.isAltPressed = true;
@@ -571,6 +618,7 @@ export class CanvasInteractions {
handleKeyUp(e: KeyboardEvent): void { handleKeyUp(e: KeyboardEvent): void {
if (e.key === 'Control') this.interaction.isCtrlPressed = false; if (e.key === 'Control') this.interaction.isCtrlPressed = false;
if (e.key === 'Meta') this.interaction.isMetaPressed = false;
if (e.key === 'Shift') this.interaction.isShiftPressed = false; if (e.key === 'Shift') this.interaction.isShiftPressed = false;
if (e.key === 'Alt') this.interaction.isAltPressed = false; if (e.key === 'Alt') this.interaction.isAltPressed = false;
if (e.key.toLowerCase() === 's') this.interaction.isSPressed = false; if (e.key.toLowerCase() === 's') this.interaction.isSPressed = false;
@@ -590,6 +638,7 @@ export class CanvasInteractions {
handleBlur(): void { handleBlur(): void {
log.debug('Window lost focus, resetting key states.'); log.debug('Window lost focus, resetting key states.');
this.interaction.isCtrlPressed = false; this.interaction.isCtrlPressed = false;
this.interaction.isMetaPressed = false;
this.interaction.isAltPressed = false; this.interaction.isAltPressed = false;
this.interaction.isShiftPressed = false; this.interaction.isShiftPressed = false;
this.interaction.isSPressed = false; this.interaction.isSPressed = false;
@@ -615,6 +664,12 @@ export class CanvasInteractions {
} }
updateCursor(worldCoords: Point): void { updateCursor(worldCoords: Point): void {
// If actively rotating, show grabbing cursor
if (this.interaction.mode === 'rotating') {
this.canvas.canvas.style.cursor = 'grabbing';
return;
}
const transformTarget = this.canvas.canvasLayers.getHandleAtPosition(worldCoords.x, worldCoords.y); const transformTarget = this.canvas.canvasLayers.getHandleAtPosition(worldCoords.x, worldCoords.y);
if (transformTarget) { if (transformTarget) {
@@ -663,7 +718,8 @@ export class CanvasInteractions {
prepareForDrag(layer: Layer, worldCoords: Point): void { prepareForDrag(layer: Layer, worldCoords: Point): void {
// Zaktualizuj zaznaczenie, ale nie zapisuj stanu // Zaktualizuj zaznaczenie, ale nie zapisuj stanu
if (this.interaction.isCtrlPressed) { // Support both Ctrl (Windows/Linux) and Cmd (macOS) for multi-selection
if (this.interaction.isCtrlPressed || this.interaction.isMetaPressed) {
const index = this.canvas.canvasSelection.selectedLayers.indexOf(layer); const index = this.canvas.canvasSelection.selectedLayers.indexOf(layer);
if (index === -1) { if (index === -1) {
this.canvas.canvasSelection.updateSelection([...this.canvas.canvasSelection.selectedLayers, layer]); this.canvas.canvasSelection.updateSelection([...this.canvas.canvasSelection.selectedLayers, layer]);
@@ -681,14 +737,13 @@ export class CanvasInteractions {
this.interaction.dragStart = {...worldCoords}; this.interaction.dragStart = {...worldCoords};
} }
startPanningOrClearSelection(e: MouseEvent): void { startPanning(e: MouseEvent, clearSelection: boolean = true): void {
// Ta funkcja jest teraz wywoływana tylko gdy kliknięto na tło bez modyfikatorów. // Unified panning method - can optionally clear selection
// Domyślna akcja: wyczyść zaznaczenie i rozpocznij panoramowanie. if (clearSelection && !this.interaction.isCtrlPressed) {
if (!this.interaction.isCtrlPressed) {
this.canvas.canvasSelection.updateSelection([]); this.canvas.canvasSelection.updateSelection([]);
} }
this.interaction.mode = 'panning'; this.interaction.mode = 'panning';
this.interaction.panStart = {x: e.clientX, y: e.clientY}; this.interaction.panStart = { x: e.clientX, y: e.clientY };
} }
startCanvasResize(worldCoords: Point): void { startCanvasResize(worldCoords: Point): void {
@@ -743,14 +798,6 @@ export class CanvasInteractions {
this.canvas.saveState(); this.canvas.saveState();
} }
startPanning(e: MouseEvent): void {
if (!this.interaction.isCtrlPressed) {
this.canvas.canvasSelection.updateSelection([]);
}
this.interaction.mode = 'panning';
this.interaction.panStart = { x: e.clientX, y: e.clientY };
}
panViewport(e: MouseEvent): void { panViewport(e: MouseEvent): void {
const dx = e.clientX - this.interaction.panStart.x; const dx = e.clientX - this.interaction.panStart.x;
const dy = e.clientY - this.interaction.panStart.y; const dy = e.clientY - this.interaction.panStart.y;
@@ -818,7 +865,7 @@ export class CanvasInteractions {
} }
const o = this.interaction.transformOrigin; const o = this.interaction.transformOrigin;
if (o.rotation === undefined || o.width === undefined || o.height === undefined || o.centerX === undefined || o.centerY === undefined) return; if (!o) return;
const handle = this.interaction.resizeHandle; const handle = this.interaction.resizeHandle;
const anchor = this.interaction.resizeAnchor; const anchor = this.interaction.resizeAnchor;
@@ -974,7 +1021,7 @@ export class CanvasInteractions {
if (!layer) return; if (!layer) return;
const o = this.interaction.transformOrigin; const o = this.interaction.transformOrigin;
if (o.rotation === undefined || o.centerX === undefined || o.centerY === undefined) return; if (!o) return;
const startAngle = Math.atan2(this.interaction.dragStart.y - o.centerY, this.interaction.dragStart.x - o.centerX); const startAngle = Math.atan2(this.interaction.dragStart.y - o.centerY, this.interaction.dragStart.x - o.centerX);
const currentAngle = Math.atan2(worldCoords.y - o.centerY, worldCoords.x - o.centerX); const currentAngle = Math.atan2(worldCoords.y - o.centerY, worldCoords.x - o.centerX);
let angleDiff = (currentAngle - startAngle) * 180 / Math.PI; let angleDiff = (currentAngle - startAngle) * 180 / Math.PI;