From de67252a87a2b394d091c788ecb22ae0e0bada7c Mon Sep 17 00:00:00 2001 From: Dariusz L Date: Mon, 27 Oct 2025 17:20:53 +0100 Subject: [PATCH] Add grab icon for layer movement Implemented grab icon feature in transform mode to move selected layers without changing selection, even when behind other layers. Added hover detection, cursor updates, and visual rendering in CanvasInteractions.ts and CanvasRenderer.ts. --- js/CanvasInteractions.js | 44 ++++++++++++++++++++++++++ js/CanvasRenderer.js | 53 +++++++++++++++++++++++++++++++ src/CanvasInteractions.ts | 53 +++++++++++++++++++++++++++++++ src/CanvasRenderer.ts | 65 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 215 insertions(+) diff --git a/js/CanvasInteractions.js b/js/CanvasInteractions.js index 290eb4c..7de76c9 100644 --- a/js/CanvasInteractions.js +++ b/js/CanvasInteractions.js @@ -41,6 +41,7 @@ export class CanvasInteractions { canvasMoveRect: null, outputAreaTransformHandle: null, outputAreaTransformAnchor: { x: 0, y: 0 }, + hoveringGrabIcon: false, }; this.originalLayerPositions = new Map(); } @@ -151,6 +152,29 @@ export class CanvasInteractions { } return false; } + /** + * Sprawdza czy punkt znajduje się w obszarze ikony "grab" (środek layera) + * Zwraca layer, jeśli kliknięto w ikonę grab + */ + getGrabIconAtPosition(worldX, worldY) { + // Rozmiar ikony grab w pikselach światowych + const grabIconRadius = 20 / this.canvas.viewport.zoom; + for (const layer of this.canvas.canvasSelection.selectedLayers) { + if (!layer.visible) + continue; + const centerX = layer.x + layer.width / 2; + const centerY = layer.y + layer.height / 2; + // Sprawdź czy punkt jest w obszarze ikony grab (okrąg wokół środka) + const dx = worldX - centerX; + const dy = worldY - centerY; + const distanceSquared = dx * dx + dy * dy; + const radiusSquared = grabIconRadius * grabIconRadius; + if (distanceSquared <= radiusSquared) { + return layer; + } + } + return null; + } resetInteractionState() { this.interaction.mode = 'none'; this.interaction.resizeHandle = null; @@ -227,6 +251,14 @@ export class CanvasInteractions { this.startLayerTransform(transformTarget.layer, transformTarget.handle, coords.world); return; } + // Check if clicking on grab icon of a selected layer + const grabIconLayer = this.getGrabIconAtPosition(coords.world.x, coords.world.y); + if (grabIconLayer) { + // Start dragging the selected layer(s) without changing selection + this.interaction.mode = 'potential-drag'; + this.interaction.dragStart = { ...coords.world }; + return; + } const clickedLayerResult = this.canvas.canvasLayers.getLayerAtPosition(coords.world.x, coords.world.y); if (clickedLayerResult) { this.prepareForDrag(clickedLayerResult.layer, coords.world); @@ -282,6 +314,13 @@ export class CanvasInteractions { } break; default: + // Check if hovering over grab icon + const wasHovering = this.interaction.hoveringGrabIcon; + this.interaction.hoveringGrabIcon = this.getGrabIconAtPosition(coords.world.x, coords.world.y) !== null; + // Re-render if hover state changed to show/hide grab icon + if (wasHovering !== this.interaction.hoveringGrabIcon) { + this.canvas.render(); + } this.updateCursor(coords.world); // Update brush cursor on overlay if mask tool is active if (this.canvas.maskTool.isActive) { @@ -617,6 +656,11 @@ export class CanvasInteractions { this.canvas.canvas.style.cursor = 'grabbing'; return; } + // Check if hovering over grab icon + if (this.interaction.hoveringGrabIcon) { + this.canvas.canvas.style.cursor = 'grab'; + return; + } const transformTarget = this.canvas.canvasLayers.getHandleAtPosition(worldCoords.x, worldCoords.y); if (transformTarget) { const handleName = transformTarget.handle; diff --git a/js/CanvasRenderer.js b/js/CanvasRenderer.js index 31c9f3e..6223d6d 100644 --- a/js/CanvasRenderer.js +++ b/js/CanvasRenderer.js @@ -141,6 +141,10 @@ export class CanvasRenderer { ctx.restore(); } }); + // Draw grab icons for selected layers when hovering + if (this.canvas.canvasInteractions.interaction.hoveringGrabIcon) { + this.drawGrabIcons(ctx); + } this.drawCanvasOutline(ctx); this.drawOutputAreaExtensionPreview(ctx); // Draw extension preview this.drawPendingGenerationAreas(ctx); // Draw snapshot outlines @@ -833,6 +837,55 @@ export class CanvasRenderer { // Just ensure it's the right size this.updateOverlaySize(); } + /** + * Draw grab icons in the center of selected layers + */ + drawGrabIcons(ctx) { + const selectedLayers = this.canvas.canvasSelection.selectedLayers; + if (selectedLayers.length === 0) + return; + const iconRadius = 20 / this.canvas.viewport.zoom; + const innerRadius = 12 / this.canvas.viewport.zoom; + selectedLayers.forEach((layer) => { + if (!layer.visible) + return; + const centerX = layer.x + layer.width / 2; + const centerY = layer.y + layer.height / 2; + ctx.save(); + // Draw outer circle (background) + ctx.beginPath(); + ctx.arc(centerX, centerY, iconRadius, 0, Math.PI * 2); + ctx.fillStyle = 'rgba(0, 150, 255, 0.7)'; + ctx.fill(); + ctx.strokeStyle = 'rgba(255, 255, 255, 0.9)'; + ctx.lineWidth = 2 / this.canvas.viewport.zoom; + ctx.stroke(); + // Draw hand/grab icon (simplified) + ctx.fillStyle = 'rgba(255, 255, 255, 0.95)'; + ctx.strokeStyle = 'rgba(255, 255, 255, 0.95)'; + ctx.lineWidth = 1.5 / this.canvas.viewport.zoom; + // Draw four dots representing grab points + const dotRadius = 2 / this.canvas.viewport.zoom; + const dotDistance = 6 / this.canvas.viewport.zoom; + // Top-left + ctx.beginPath(); + ctx.arc(centerX - dotDistance, centerY - dotDistance, dotRadius, 0, Math.PI * 2); + ctx.fill(); + // Top-right + ctx.beginPath(); + ctx.arc(centerX + dotDistance, centerY - dotDistance, dotRadius, 0, Math.PI * 2); + ctx.fill(); + // Bottom-left + ctx.beginPath(); + ctx.arc(centerX - dotDistance, centerY + dotDistance, dotRadius, 0, Math.PI * 2); + ctx.fill(); + // Bottom-right + ctx.beginPath(); + ctx.arc(centerX + dotDistance, centerY + dotDistance, dotRadius, 0, Math.PI * 2); + ctx.fill(); + ctx.restore(); + }); + } /** * Draw transform handles for output area when in transform mode */ diff --git a/src/CanvasInteractions.ts b/src/CanvasInteractions.ts index 3cdeaae..0381d46 100644 --- a/src/CanvasInteractions.ts +++ b/src/CanvasInteractions.ts @@ -51,6 +51,7 @@ interface InteractionState { canvasMoveRect: { x: number, y: number, width: number, height: number } | null; outputAreaTransformHandle: string | null; outputAreaTransformAnchor: Point; + hoveringGrabIcon: boolean; } export class CanvasInteractions { @@ -98,6 +99,7 @@ export class CanvasInteractions { canvasMoveRect: null, outputAreaTransformHandle: null, outputAreaTransformAnchor: { x: 0, y: 0 }, + hoveringGrabIcon: false, }; this.originalLayerPositions = new Map(); } @@ -234,6 +236,33 @@ export class CanvasInteractions { return false; } + /** + * Sprawdza czy punkt znajduje się w obszarze ikony "grab" (środek layera) + * Zwraca layer, jeśli kliknięto w ikonę grab + */ + getGrabIconAtPosition(worldX: number, worldY: number): Layer | null { + // Rozmiar ikony grab w pikselach światowych + const grabIconRadius = 20 / this.canvas.viewport.zoom; + + for (const layer of this.canvas.canvasSelection.selectedLayers) { + if (!layer.visible) continue; + + const centerX = layer.x + layer.width / 2; + const centerY = layer.y + layer.height / 2; + + // Sprawdź czy punkt jest w obszarze ikony grab (okrąg wokół środka) + const dx = worldX - centerX; + const dy = worldY - centerY; + const distanceSquared = dx * dx + dy * dy; + const radiusSquared = grabIconRadius * grabIconRadius; + + if (distanceSquared <= radiusSquared) { + return layer; + } + } + return null; + } + resetInteractionState(): void { this.interaction.mode = 'none'; this.interaction.resizeHandle = null; @@ -320,6 +349,15 @@ export class CanvasInteractions { return; } + // Check if clicking on grab icon of a selected layer + const grabIconLayer = this.getGrabIconAtPosition(coords.world.x, coords.world.y); + if (grabIconLayer) { + // Start dragging the selected layer(s) without changing selection + this.interaction.mode = 'potential-drag'; + this.interaction.dragStart = {...coords.world}; + return; + } + const clickedLayerResult = this.canvas.canvasLayers.getLayerAtPosition(coords.world.x, coords.world.y); if (clickedLayerResult) { this.prepareForDrag(clickedLayerResult.layer, coords.world); @@ -378,6 +416,15 @@ export class CanvasInteractions { } break; default: + // Check if hovering over grab icon + const wasHovering = this.interaction.hoveringGrabIcon; + this.interaction.hoveringGrabIcon = this.getGrabIconAtPosition(coords.world.x, coords.world.y) !== null; + + // Re-render if hover state changed to show/hide grab icon + if (wasHovering !== this.interaction.hoveringGrabIcon) { + this.canvas.render(); + } + this.updateCursor(coords.world); // Update brush cursor on overlay if mask tool is active if (this.canvas.maskTool.isActive) { @@ -738,6 +785,12 @@ export class CanvasInteractions { return; } + // Check if hovering over grab icon + if (this.interaction.hoveringGrabIcon) { + this.canvas.canvas.style.cursor = 'grab'; + return; + } + const transformTarget = this.canvas.canvasLayers.getHandleAtPosition(worldCoords.x, worldCoords.y); if (transformTarget) { diff --git a/src/CanvasRenderer.ts b/src/CanvasRenderer.ts index a4844ae..1ba81b7 100644 --- a/src/CanvasRenderer.ts +++ b/src/CanvasRenderer.ts @@ -188,6 +188,11 @@ export class CanvasRenderer { } }); + // Draw grab icons for selected layers when hovering + if (this.canvas.canvasInteractions.interaction.hoveringGrabIcon) { + this.drawGrabIcons(ctx); + } + this.drawCanvasOutline(ctx); this.drawOutputAreaExtensionPreview(ctx); // Draw extension preview this.drawPendingGenerationAreas(ctx); // Draw snapshot outlines @@ -1013,6 +1018,66 @@ export class CanvasRenderer { this.updateOverlaySize(); } + /** + * Draw grab icons in the center of selected layers + */ + drawGrabIcons(ctx: any): void { + const selectedLayers = this.canvas.canvasSelection.selectedLayers; + if (selectedLayers.length === 0) return; + + const iconRadius = 20 / this.canvas.viewport.zoom; + const innerRadius = 12 / this.canvas.viewport.zoom; + + selectedLayers.forEach((layer: any) => { + if (!layer.visible) return; + + const centerX = layer.x + layer.width / 2; + const centerY = layer.y + layer.height / 2; + + ctx.save(); + + // Draw outer circle (background) + ctx.beginPath(); + ctx.arc(centerX, centerY, iconRadius, 0, Math.PI * 2); + ctx.fillStyle = 'rgba(0, 150, 255, 0.7)'; + ctx.fill(); + ctx.strokeStyle = 'rgba(255, 255, 255, 0.9)'; + ctx.lineWidth = 2 / this.canvas.viewport.zoom; + ctx.stroke(); + + // Draw hand/grab icon (simplified) + ctx.fillStyle = 'rgba(255, 255, 255, 0.95)'; + ctx.strokeStyle = 'rgba(255, 255, 255, 0.95)'; + ctx.lineWidth = 1.5 / this.canvas.viewport.zoom; + + // Draw four dots representing grab points + const dotRadius = 2 / this.canvas.viewport.zoom; + const dotDistance = 6 / this.canvas.viewport.zoom; + + // Top-left + ctx.beginPath(); + ctx.arc(centerX - dotDistance, centerY - dotDistance, dotRadius, 0, Math.PI * 2); + ctx.fill(); + + // Top-right + ctx.beginPath(); + ctx.arc(centerX + dotDistance, centerY - dotDistance, dotRadius, 0, Math.PI * 2); + ctx.fill(); + + // Bottom-left + ctx.beginPath(); + ctx.arc(centerX - dotDistance, centerY + dotDistance, dotRadius, 0, Math.PI * 2); + ctx.fill(); + + // Bottom-right + ctx.beginPath(); + ctx.arc(centerX + dotDistance, centerY + dotDistance, dotRadius, 0, Math.PI * 2); + ctx.fill(); + + ctx.restore(); + }); + } + /** * Draw transform handles for output area when in transform mode */