From df6979a59bed9bb1a8538d61fe60b5fa03a95633 Mon Sep 17 00:00:00 2001 From: Dariusz L Date: Mon, 4 Aug 2025 00:46:14 +0200 Subject: [PATCH] Fix selection border points for vertical/horizontal flip --- js/CanvasInteractions.js | 42 +++++++++++++++++++++++++++++++-------- js/CanvasLayers.js | 11 ++++++++-- js/CanvasRenderer.js | 28 ++++++++++++++++++-------- src/CanvasInteractions.ts | 42 +++++++++++++++++++++++++++++---------- src/CanvasLayers.ts | 26 +++++++++++++++--------- src/CanvasRenderer.ts | 30 ++++++++++++++++++++-------- 6 files changed, 134 insertions(+), 45 deletions(-) diff --git a/js/CanvasInteractions.js b/js/CanvasInteractions.js index cba88bc..56156c7 100644 --- a/js/CanvasInteractions.js +++ b/js/CanvasInteractions.js @@ -742,8 +742,14 @@ export class CanvasInteractions { // Rotate mouse delta into the layer's unrotated frame const deltaX_world = mouseX_local - dragStartX_local; const deltaY_world = mouseY_local - dragStartY_local; - const mouseDeltaX_local = deltaX_world * cos + deltaY_world * sin; - const mouseDeltaY_local = deltaY_world * cos - deltaX_world * sin; + let mouseDeltaX_local = deltaX_world * cos + deltaY_world * sin; + let mouseDeltaY_local = deltaY_world * cos - deltaX_world * sin; + if (layer.flipH) { + mouseDeltaX_local *= -1; + } + if (layer.flipV) { + mouseDeltaY_local *= -1; + } // Convert the on-screen mouse delta to an image-space delta. const screenToImageScaleX = o.originalWidth / o.width; const screenToImageScaleY = o.originalHeight / o.height; @@ -751,19 +757,39 @@ export class CanvasInteractions { const delta_image_y = mouseDeltaY_local * screenToImageScaleY; let newCropBounds = { ...o.cropBounds }; // Start with the bounds from the beginning of the drag // Apply the image-space delta to the appropriate edges of the crop bounds + const isFlippedH = layer.flipH; + const isFlippedV = layer.flipV; if (handle?.includes('w')) { - newCropBounds.x += delta_image_x; - newCropBounds.width -= delta_image_x; + if (isFlippedH) + newCropBounds.width += delta_image_x; + else { + newCropBounds.x += delta_image_x; + newCropBounds.width -= delta_image_x; + } } if (handle?.includes('e')) { - newCropBounds.width += delta_image_x; + if (isFlippedH) { + newCropBounds.x += delta_image_x; + newCropBounds.width -= delta_image_x; + } + else + newCropBounds.width += delta_image_x; } if (handle?.includes('n')) { - newCropBounds.y += delta_image_y; - newCropBounds.height -= delta_image_y; + if (isFlippedV) + newCropBounds.height += delta_image_y; + else { + newCropBounds.y += delta_image_y; + newCropBounds.height -= delta_image_y; + } } if (handle?.includes('s')) { - newCropBounds.height += delta_image_y; + if (isFlippedV) { + newCropBounds.y += delta_image_y; + newCropBounds.height -= delta_image_y; + } + else + newCropBounds.height += delta_image_y; } // Clamp crop bounds to stay within the original image and maintain minimum size if (newCropBounds.width < 1) { diff --git a/js/CanvasLayers.js b/js/CanvasLayers.js index 425913e..8a3f2d8 100644 --- a/js/CanvasLayers.js +++ b/js/CanvasLayers.js @@ -1036,9 +1036,16 @@ export class CanvasLayers { const layerScaleY = layer.height / layer.originalHeight; const cropRectW = layer.cropBounds.width * layerScaleX; const cropRectH = layer.cropBounds.height * layerScaleY; + // Effective crop bounds start position, accounting for flips. + const effectiveCropX = layer.flipH + ? layer.originalWidth - (layer.cropBounds.x + layer.cropBounds.width) + : layer.cropBounds.x; + const effectiveCropY = layer.flipV + ? layer.originalHeight - (layer.cropBounds.y + layer.cropBounds.height) + : layer.cropBounds.y; // Center of the CROP rectangle in the layer's local, un-rotated space - const cropCenterX_local = (-layer.width / 2) + ((layer.cropBounds.x + layer.cropBounds.width / 2) * layerScaleX); - const cropCenterY_local = (-layer.height / 2) + ((layer.cropBounds.y + layer.cropBounds.height / 2) * layerScaleY); + const cropCenterX_local = (-layer.width / 2) + ((effectiveCropX + layer.cropBounds.width / 2) * layerScaleX); + const cropCenterY_local = (-layer.height / 2) + ((effectiveCropY + layer.cropBounds.height / 2) * layerScaleY); // Rotate this local center to find the world-space center of the crop rect handleCenterX = layerCenterX + (cropCenterX_local * cos - cropCenterY_local * sin); handleCenterY = layerCenterY + (cropCenterX_local * sin + cropCenterY_local * cos); diff --git a/js/CanvasRenderer.js b/js/CanvasRenderer.js index 9070099..874b57d 100644 --- a/js/CanvasRenderer.js +++ b/js/CanvasRenderer.js @@ -467,8 +467,10 @@ export class CanvasRenderer { // Draw line to rotation handle ctx.setLineDash([]); ctx.beginPath(); - ctx.moveTo(0, -halfH); - ctx.lineTo(0, -halfH - 20 / this.canvas.viewport.zoom); + const startY = layer.flipV ? halfH : -halfH; + const endY = startY + (layer.flipV ? 1 : -1) * (20 / this.canvas.viewport.zoom); + ctx.moveTo(0, startY); + ctx.lineTo(0, endY); ctx.stroke(); } // --- DRAW HANDLES (Unified Logic) --- @@ -476,19 +478,29 @@ export class CanvasRenderer { ctx.fillStyle = '#ffffff'; ctx.strokeStyle = '#000000'; ctx.lineWidth = 1 / this.canvas.viewport.zoom; + const centerX = layer.x + layer.width / 2; + const centerY = layer.y + layer.height / 2; for (const key in handles) { // Skip rotation handle in crop mode if (layer.cropMode && key === 'rot') continue; const point = handles[key]; - // The handle position is already in world space, we need it in the layer's rotated space - const localX = point.x - (layer.x + layer.width / 2); - const localY = point.y - (layer.y + layer.height / 2); + // The handle position is already in world space. + // We need to convert it to the layer's local, un-rotated space. + const dx = point.x - centerX; + const dy = point.y - centerY; + // "Un-rotate" the position to get it in the layer's local, un-rotated space const rad = -layer.rotation * Math.PI / 180; - const rotatedX = localX * Math.cos(rad) - localY * Math.sin(rad); - const rotatedY = localX * Math.sin(rad) + localY * Math.cos(rad); + const cos = Math.cos(rad); + const sin = Math.sin(rad); + const localX = dx * cos - dy * sin; + const localY = dx * sin + dy * cos; + // The context is already flipped. We need to flip the coordinates + // to match the visual transformation, so the arc is drawn in the correct place. + const finalX = localX * (layer.flipH ? -1 : 1); + const finalY = localY * (layer.flipV ? -1 : 1); ctx.beginPath(); - ctx.arc(rotatedX, rotatedY, handleRadius, 0, Math.PI * 2); + ctx.arc(finalX, finalY, handleRadius, 0, Math.PI * 2); ctx.fill(); ctx.stroke(); } diff --git a/src/CanvasInteractions.ts b/src/CanvasInteractions.ts index 33d5c8e..6fb273f 100644 --- a/src/CanvasInteractions.ts +++ b/src/CanvasInteractions.ts @@ -857,8 +857,15 @@ export class CanvasInteractions { // Rotate mouse delta into the layer's unrotated frame const deltaX_world = mouseX_local - dragStartX_local; const deltaY_world = mouseY_local - dragStartY_local; - const mouseDeltaX_local = deltaX_world * cos + deltaY_world * sin; - const mouseDeltaY_local = deltaY_world * cos - deltaX_world * sin; + let mouseDeltaX_local = deltaX_world * cos + deltaY_world * sin; + let mouseDeltaY_local = deltaY_world * cos - deltaX_world * sin; + + if (layer.flipH) { + mouseDeltaX_local *= -1; + } + if (layer.flipV) { + mouseDeltaY_local *= -1; + } // Convert the on-screen mouse delta to an image-space delta. const screenToImageScaleX = o.originalWidth / o.width; @@ -870,22 +877,37 @@ export class CanvasInteractions { let newCropBounds = { ...o.cropBounds }; // Start with the bounds from the beginning of the drag // Apply the image-space delta to the appropriate edges of the crop bounds + const isFlippedH = layer.flipH; + const isFlippedV = layer.flipV; + if (handle?.includes('w')) { - newCropBounds.x += delta_image_x; - newCropBounds.width -= delta_image_x; + if (isFlippedH) newCropBounds.width += delta_image_x; + else { + newCropBounds.x += delta_image_x; + newCropBounds.width -= delta_image_x; + } } if (handle?.includes('e')) { - newCropBounds.width += delta_image_x; + if (isFlippedH) { + newCropBounds.x += delta_image_x; + newCropBounds.width -= delta_image_x; + } else newCropBounds.width += delta_image_x; } if (handle?.includes('n')) { - newCropBounds.y += delta_image_y; - newCropBounds.height -= delta_image_y; + if (isFlippedV) newCropBounds.height += delta_image_y; + else { + newCropBounds.y += delta_image_y; + newCropBounds.height -= delta_image_y; + } } if (handle?.includes('s')) { - newCropBounds.height += delta_image_y; + if (isFlippedV) { + newCropBounds.y += delta_image_y; + newCropBounds.height -= delta_image_y; + } else newCropBounds.height += delta_image_y; } - - // Clamp crop bounds to stay within the original image and maintain minimum size + + // Clamp crop bounds to stay within the original image and maintain minimum size if (newCropBounds.width < 1) { if (handle?.includes('w')) newCropBounds.x = o.cropBounds.x + o.cropBounds.width -1; newCropBounds.width = 1; diff --git a/src/CanvasLayers.ts b/src/CanvasLayers.ts index dee496a..fc91633 100644 --- a/src/CanvasLayers.ts +++ b/src/CanvasLayers.ts @@ -1188,9 +1188,17 @@ export class CanvasLayers { const cropRectW = layer.cropBounds.width * layerScaleX; const cropRectH = layer.cropBounds.height * layerScaleY; + // Effective crop bounds start position, accounting for flips. + const effectiveCropX = layer.flipH + ? layer.originalWidth - (layer.cropBounds.x + layer.cropBounds.width) + : layer.cropBounds.x; + const effectiveCropY = layer.flipV + ? layer.originalHeight - (layer.cropBounds.y + layer.cropBounds.height) + : layer.cropBounds.y; + // Center of the CROP rectangle in the layer's local, un-rotated space - const cropCenterX_local = (-layer.width / 2) + ((layer.cropBounds.x + layer.cropBounds.width / 2) * layerScaleX); - const cropCenterY_local = (-layer.height / 2) + ((layer.cropBounds.y + layer.cropBounds.height / 2) * layerScaleY); + const cropCenterX_local = (-layer.width / 2) + ((effectiveCropX + layer.cropBounds.width / 2) * layerScaleX); + const cropCenterY_local = (-layer.height / 2) + ((effectiveCropY + layer.cropBounds.height / 2) * layerScaleY); // Rotate this local center to find the world-space center of the crop rect handleCenterX = layerCenterX + (cropCenterX_local * cos - cropCenterY_local * sin); @@ -1206,13 +1214,13 @@ export class CanvasLayers { halfH = layer.height / 2; } - const localHandles: Record = { - 'n': { x: 0, y: -halfH }, 'ne': { x: halfW, y: -halfH }, - 'e': { x: halfW, y: 0 }, 'se': { x: halfW, y: halfH }, - 's': { x: 0, y: halfH }, 'sw': { x: -halfW, y: halfH }, - 'w': { x: -halfW, y: 0 }, 'nw': { x: -halfW, y: -halfH }, - 'rot': { x: 0, y: -halfH - 20 / this.canvas.viewport.zoom } - }; + const localHandles: Record = { + 'n': { x: 0, y: -halfH }, 'ne': { x: halfW, y: -halfH }, + 'e': { x: halfW, y: 0 }, 'se': { x: halfW, y: halfH }, + 's': { x: 0, y: halfH }, 'sw': { x: -halfW, y: halfH }, + 'w': { x: -halfW, y: 0 }, 'nw': { x: -halfW, y: -halfH }, + 'rot': { x: 0, y: -halfH - 20 / this.canvas.viewport.zoom } + }; const worldHandles: Record = {}; for (const key in localHandles) { diff --git a/src/CanvasRenderer.ts b/src/CanvasRenderer.ts index e47a71d..beec414 100644 --- a/src/CanvasRenderer.ts +++ b/src/CanvasRenderer.ts @@ -575,8 +575,10 @@ export class CanvasRenderer { // Draw line to rotation handle ctx.setLineDash([]); ctx.beginPath(); - ctx.moveTo(0, -halfH); - ctx.lineTo(0, -halfH - 20 / this.canvas.viewport.zoom); + const startY = layer.flipV ? halfH : -halfH; + const endY = startY + (layer.flipV ? 1 : -1) * (20 / this.canvas.viewport.zoom); + ctx.moveTo(0, startY); + ctx.lineTo(0, endY); ctx.stroke(); } @@ -586,21 +588,33 @@ export class CanvasRenderer { ctx.strokeStyle = '#000000'; ctx.lineWidth = 1 / this.canvas.viewport.zoom; + const centerX = layer.x + layer.width / 2; + const centerY = layer.y + layer.height / 2; + for (const key in handles) { // Skip rotation handle in crop mode if (layer.cropMode && key === 'rot') continue; const point = handles[key]; - // The handle position is already in world space, we need it in the layer's rotated space - const localX = point.x - (layer.x + layer.width / 2); - const localY = point.y - (layer.y + layer.height / 2); + // The handle position is already in world space. + // We need to convert it to the layer's local, un-rotated space. + const dx = point.x - centerX; + const dy = point.y - centerY; + // "Un-rotate" the position to get it in the layer's local, un-rotated space const rad = -layer.rotation * Math.PI / 180; - const rotatedX = localX * Math.cos(rad) - localY * Math.sin(rad); - const rotatedY = localX * Math.sin(rad) + localY * Math.cos(rad); + const cos = Math.cos(rad); + const sin = Math.sin(rad); + const localX = dx * cos - dy * sin; + const localY = dx * sin + dy * cos; + + // The context is already flipped. We need to flip the coordinates + // to match the visual transformation, so the arc is drawn in the correct place. + const finalX = localX * (layer.flipH ? -1 : 1); + const finalY = localY * (layer.flipV ? -1 : 1); ctx.beginPath(); - ctx.arc(rotatedX, rotatedY, handleRadius, 0, Math.PI * 2); + ctx.arc(finalX, finalY, handleRadius, 0, Math.PI * 2); ctx.fill(); ctx.stroke(); }