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 {