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.
This commit is contained in:
Dariusz L
2025-10-27 17:20:53 +01:00
parent 4acece1602
commit de67252a87
4 changed files with 215 additions and 0 deletions

View File

@@ -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;

View File

@@ -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
*/

View File

@@ -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) {

View File

@@ -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
*/