diff --git a/src/components/application-menu/application-menu.vue b/src/components/application-menu/application-menu.vue
index 3ff27b2..3d31983 100644
--- a/src/components/application-menu/application-menu.vue
+++ b/src/components/application-menu/application-menu.vue
@@ -151,7 +151,7 @@
v-tooltip.right="$t('copyTooltip')"
type="button"
:disabled="!hasSelection"
- @click="requestSelectionCopy( false )"
+ @click="requestSelectionCopy({ merged: false })"
>
@@ -159,7 +159,7 @@
v-t="'copyMerged'"
type="button"
:disabled="!hasSelection"
- @click="requestSelectionCopy( true )"
+ @click="requestSelectionCopy({ merged: true })"
>
@@ -414,9 +414,10 @@
-
diff --git a/src/components/layer-panel/layer-panel.vue b/src/components/layer-panel/layer-panel.vue
index 05f2fb0..9f6f7a5 100644
--- a/src/components/layer-panel/layer-panel.vue
+++ b/src/components/layer-panel/layer-panel.vue
@@ -152,7 +152,7 @@ import ToolTypes from "@/definitions/tool-types";
import type { Layer } from "@/definitions/types/document";
import { createCanvas } from "@/utils/canvas-util";
import { toggleLayerVisibility } from "@/factories/action-factory";
-import { getSpriteForLayer } from "@/factories/sprite-factory";
+import { getCanvasInstance, getSpriteForLayer } from "@/factories/sprite-factory";
import { enqueueState } from "@/factories/history-state-factory";
import KeyboardService from "@/services/keyboard-service";
import { focus } from "@/utils/environment-util";
@@ -333,6 +333,11 @@ export default {
handleLayerClick( layer: IndexedLayer ): void {
this.setActiveLayerIndex( layer.index );
getSpriteForLayer( layer )?.setActionTarget( "source" );
+ if ( KeyboardService.hasAlt() ) {
+ this.$nextTick(() => {
+ getCanvasInstance()?.interactionPane.selectAll( this.activeLayer );
+ });
+ }
/*
if ( layer.type === LAYER_TEXT ) {
this.setActiveTool({ tool: ToolTypes.TEXT });
diff --git a/src/math/point-math.ts b/src/math/point-math.ts
index 1c6b5a0..bdf9036 100644
--- a/src/math/point-math.ts
+++ b/src/math/point-math.ts
@@ -162,6 +162,21 @@ export const pointerToCanvasCoordinates = ( pointerX: number, pointerY: number,
};
};
+export const rotatePoint = ( point: Point, angleInRadians: number, cx: number, cy: number ): Point => {
+ if ( angleInRadians === 0 ) {
+ return point;
+ }
+ const cosA = Math.cos( angleInRadians );
+ const sinA = Math.sin( angleInRadians );
+ const dx = point.x - cx;
+ const dy = point.y - cy;
+
+ return {
+ x: cx + dx * cosA - dy * sinA,
+ y: cy + dx * sinA + dy * cosA,
+ };
+};
+
/**
* Utility to rotate a list of pointers (touch/mouse coordinates performed when drawing on a LayerSprite) relative
* to the sprite's associated Layers transformations
diff --git a/src/math/rectangle-math.ts b/src/math/rectangle-math.ts
index b4cdbdf..dcceaf4 100644
--- a/src/math/rectangle-math.ts
+++ b/src/math/rectangle-math.ts
@@ -1,7 +1,7 @@
/**
* The MIT License (MIT)
*
- * Igor Zinken 2021-2023 - https://www.igorski.nl
+ * Igor Zinken 2021-2025 - https://www.igorski.nl
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
@@ -21,6 +21,7 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import type { Point, Rectangle } from "zcanvas";
+import { rotatePoint } from "@/math/point-math";
import { fastRound } from "@/math/unit-math";
const HALF = 0.5;
@@ -34,6 +35,10 @@ export const getRotationCenter = ({ left, top, width, height }: Rectangle, round
};
};
+/**
+ * Rotate a Rectangle using provided angle. This returns a BOUNDING BOX of the area occupied by the
+ * rotated Rectangle.
+ */
export const rotateRectangle = ( rectangle: Rectangle, angleInRadians = 0, rounded = false ): Rectangle => {
if ( angleInRadians === 0 ) {
return rectangle;
@@ -83,6 +88,24 @@ export const rotateRectangle = ( rectangle: Rectangle, angleInRadians = 0, round
return out;
};
+/**
+ * Rotate a Rectangle using provided angle. This returns a list of coordinates of the corners of the Rectangle.
+ */
+export const rotateRectangleToCoordinates = ( rect: Rectangle, angleInRadians: number ): Point[] => {
+ const cx = rect.left + rect.width / 2;
+ const cy = rect.top + rect.height / 2;
+
+ const corners: Point[] = [
+ { x: rect.left, y: rect.top },
+ { x: rect.left + rect.width, y: rect.top },
+ { x: rect.left + rect.width, y: rect.top + rect.height },
+ { x: rect.left, y: rect.top + rect.height },
+ ];
+ const rotatedCorners = corners.map( point => rotatePoint( point, angleInRadians, cx, cy ));
+ rotatedCorners.push( rotatedCorners[ 0 ]); // close path
+ return rotatedCorners;
+};
+
export const scaleRectangle = ({ left, top, width, height }: Rectangle, scale = 1, rounded = false ): Rectangle => {
const scaledWidth = width * scale;
const scaledHeight = height * scale;
diff --git a/src/messages.json b/src/messages.json
index 9e4bdc7..41132db 100644
--- a/src/messages.json
+++ b/src/messages.json
@@ -7,6 +7,7 @@
"savedFileSuccessfully": "Saved document \"{file}\"",
"errorLoadingFile": "An error occured during the loading of \"{file}\"",
"selectionCopied": "Selection copied",
+ "selectionCut": "Selection cut",
"warningUnload": "You are about to close BitMappery. Confirmation means you have either saved your pending changes or are aware these will otherwise be lost.",
"selectionInverted": "Selection inverted",
"corsError": "Could not open file '{file}' as the owner does not allow cross-origin sharing",
diff --git a/src/rendering/canvas-elements/interaction-pane.ts b/src/rendering/canvas-elements/interaction-pane.ts
index 270c564..6b9cf56 100644
--- a/src/rendering/canvas-elements/interaction-pane.ts
+++ b/src/rendering/canvas-elements/interaction-pane.ts
@@ -27,7 +27,7 @@ import ToolTypes from "@/definitions/tool-types";
import { enqueueState } from "@/factories/history-state-factory";
import { getCanvasInstance, getSpriteForLayer } from "@/factories/sprite-factory";
import { isPointInRange, translatePoints, snapToAngle, rectToCoordinateList } from "@/math/point-math";
-import { scaleRectangle } from "@/math/rectangle-math";
+import { rotateRectangleToCoordinates, scaleRectangle } from "@/math/rectangle-math";
import { selectByColor } from "@/math/selection-math";
import { fastRound } from "@/math/unit-math";
import LayerSprite from "@/rendering/canvas-elements/layer-sprite";
@@ -204,9 +204,14 @@ class InteractionPane extends sprite {
}
selectAll( targetLayer: Layer = null ): void {
- const bounds = targetLayer ? getSpriteForLayer( targetLayer ).getBounds() : this._bounds;
+ if ( targetLayer ) {
+ const { scale, rotation } = targetLayer.effects;
+ const bounds = scaleRectangle( getSpriteForLayer( targetLayer ).getBounds(), scale );
+ this.setSelection( [ rotateRectangleToCoordinates( bounds, rotation ) ]);
+ return;
+ }
this.setSelection(
- [ rectToCoordinateList( bounds.left, bounds.top, bounds.width, bounds.height )]
+ [ rectToCoordinateList( this._bounds.left, this._bounds.top, this._bounds.width, this._bounds.height )]
);
}
diff --git a/src/services/keyboard-service.ts b/src/services/keyboard-service.ts
index 7b4a48b..27b4f44 100644
--- a/src/services/keyboard-service.ts
+++ b/src/services/keyboard-service.ts
@@ -74,16 +74,22 @@ const KeyboardService =
},
/**
* whether the Apple option or a control key is
- * currently held down for the given event
+ * currently held down for either the given event or a still held key
*/
- hasOption( aEvent: KeyboardEvent ): boolean {
- return ( optionDown === true ) || aEvent.metaKey || aEvent.ctrlKey;
+ hasOption( event?: KeyboardEvent ): boolean {
+ return optionDown || !!event?.metaKey || !!event?.ctrlKey;
+ },
+ /**
+ * whether the alt key is currently held down
+ */
+ hasAlt(): boolean {
+ return altDown;
},
/**
* whether the shift key is currently held down
*/
hasShift(): boolean {
- return ( shiftDown === true );
+ return shiftDown;
},
/**
* attach a listener to receive updates whenever a key
@@ -252,8 +258,8 @@ function handleKeyDown( event: KeyboardEvent ): void {
case 65: // A
// select all
- if ( nativeModifier && getters.activeLayer ) {
- getCanvasInstance()?.interactionPane.selectAll( getters.activeLayer );
+ if ( nativeModifier && getters.activeDocument ) {
+ getCanvasInstance()?.interactionPane.selectAll();
}
break;
@@ -267,7 +273,7 @@ function handleKeyDown( event: KeyboardEvent ): void {
// copy current selection
if ( nativeModifier ) {
if ( getters.activeDocument?.activeSelection?.length > 0 ) {
- dispatch( "requestSelectionCopy", shiftDown );
+ dispatch( "requestSelectionCopy", { merged: shiftDown });
preventDefault( event );
}
} else {
@@ -482,6 +488,7 @@ function handleKeyDown( event: KeyboardEvent ): void {
function handleKeyUp( event: KeyboardEvent ): void {
shiftDown = false;
+ altDown = false;
switch ( event.keyCode ) {
default:
diff --git a/src/store/index.ts b/src/store/index.ts
index e4b616a..8b5945c 100644
--- a/src/store/index.ts
+++ b/src/store/index.ts
@@ -262,14 +262,14 @@ export default {
message: translate( "savedFileSuccessfully" , { file: truncate( name, 35 ) })
});
},
- async requestSelectionCopy({ commit, getters }: ActionContext, copyMerged = false ): Promise {
- const selectionImage = await copySelection( getters.activeDocument, getters.activeLayer, copyMerged );
+ async requestSelectionCopy({ commit, getters }: ActionContext, { merged = false, isCut = false }): Promise {
+ const selectionImage = await copySelection( getters.activeDocument, getters.activeLayer, merged );
commit( "setSelectionContent", selectionImage );
commit( "setActiveTool", { tool: null, activeLayer: getters.activeLayer });
- commit( "showNotification", { message: translate( "selectionCopied" ) });
+ commit( "showNotification", { message: translate( isCut ? "selectionCut" : "selectionCopied" ) });
},
async requestSelectionCut({ dispatch }: ActionContext ): Promise {
- dispatch( "requestSelectionCopy" );
+ dispatch( "requestSelectionCopy", { merged: false, isCut: true });
dispatch( "deleteInSelection" );
},
clearSelection(): void {