Select all now selects full Document size more logically. Alt + clicking on a Layer selects the Layer outline, respecting transformations

This commit is contained in:
Igor Zinken
2025-03-15 11:06:05 +01:00
parent 83d8e7af4d
commit 201ddbd9e5
8 changed files with 116 additions and 59 deletions

View File

@@ -151,7 +151,7 @@
v-tooltip.right="$t('copyTooltip')"
type="button"
:disabled="!hasSelection"
@click="requestSelectionCopy( false )"
@click="requestSelectionCopy({ merged: false })"
></button>
</li>
<li>
@@ -159,7 +159,7 @@
v-t="'copyMerged'"
type="button"
:disabled="!hasSelection"
@click="requestSelectionCopy( true )"
@click="requestSelectionCopy({ merged: true })"
></button>
</li>
<li>
@@ -414,9 +414,10 @@
</nav>
</template>
<script>
<script lang="ts">
import { mapState, mapGetters, mapMutations, mapActions } from "vuex";
import cloneDeep from "lodash.clonedeep";
import { type Layer } from "@/definitions/document";
import { MAX_SPRITESHEET_WIDTH } from "@/definitions/editor-properties";
import {
CREATE_DOCUMENT, RESIZE_DOCUMENT, SAVE_DOCUMENT, EXPORT_WINDOW, LOAD_SELECTION, SAVE_SELECTION,
@@ -466,66 +467,66 @@ export default {
"pixelGrid",
]),
supportsFullscreen,
noDocumentsAvailable() {
noDocumentsAvailable(): boolean {
return !this.activeDocument;
},
hasSavedSelections() {
hasSavedSelections(): boolean {
return Object.keys( this.activeDocument?.selections || {} ).length > 0;
},
hasClipboard() {
hasClipboard(): boolean {
return !!this.selectionContent;
},
activeLayerHasFilters() {
activeLayerHasFilters(): boolean {
return this.activeLayer?.filters?.enabled;
},
canSnapAndAlign: {
get() {
get(): boolean {
return this.snapAlign;
},
async set( value ) {
async set( value: boolean ): Promise<void> {
this.setSnapAlign( value );
this.setPreferences({ snapAlign: value });
await this.storePreferences();
}
},
useAntiAlias: {
get() {
get(): boolean {
return this.antiAlias;
},
async set( value ) {
async set( value: boolean ): Promise<void> {
this.setAntiAlias( value );
this.setPreferences({ antiAlias: value });
await this.storePreferences();
}
},
usePixelGrid: {
get() {
get(): boolean {
return this.pixelGrid;
},
set( value ) {
set( value: boolean ): void {
this.setPixelGrid( value );
},
},
fullscreenTooltip() {
fullscreenTooltip(): string {
return `${this.isFullscreen ? this.$t( "minimize" ) : this.$t( "maximize" )} (Shift + F)`;
},
canUsePixelGrid() {
canUsePixelGrid(): boolean {
return this.activeDocument?.width <= MAX_SPRITESHEET_WIDTH;
},
},
watch: {
blindActive( isOpen, wasOpen ) {
blindActive( isOpen: boolean, wasOpen?: boolean ): void {
if ( !isOpen && wasOpen === true ) {
this.setMenuOpened( false );
}
},
canUsePixelGrid( value ) {
canUsePixelGrid( value: boolean ): void {
if ( !value && this.usePixelGrid ) {
this.usePixelGrid = false;
}
},
},
mounted() {
mounted(): void {
if ( this.$refs.fullscreenBtn ) {
setToggleButton( this.$refs.fullscreenBtn, isFullscreen => {
this.isFullscreen = isFullscreen;
@@ -562,43 +563,43 @@ export default {
"loadDocument",
"storePreferences",
]),
openSubMenu( name ) {
openSubMenu( name: string ): void {
this.activeSubMenu = this.activeSubMenu === name ? null : name;
},
requestNewDocument() {
requestNewDocument(): void {
this.openModal( CREATE_DOCUMENT );
},
openFileSelector() {
openFileSelector(): void {
this.$refs.fileSelector?.click();
},
requestImageExport() {
requestImageExport(): void {
this.openModal( EXPORT_WINDOW );
},
requestDocumentResize() {
requestDocumentResize(): void {
this.openModal( RESIZE_DOCUMENT );
},
requestCanvasResize() {
requestCanvasResize(): void {
this.openModal( RESIZE_CANVAS );
},
requestGridToLayers() {
requestGridToLayers(): void {
this.openModal( GRID_TO_LAYERS );
},
requestDocumentExport() {
requestDocumentExport(): void {
this.openModal( SAVE_DOCUMENT );
},
requestSelectionLoad() {
requestSelectionLoad(): void {
this.openModal( LOAD_SELECTION );
},
requestSelectionSave() {
requestSelectionSave(): void {
this.openModal( SAVE_SELECTION );
},
openPreferences() {
openPreferences(): void {
this.openModal( PREFERENCES );
},
strokeSelection() {
strokeSelection(): void {
this.openModal( STROKE_SELECTION );
},
requestCropToSelection() {
requestCropToSelection(): void {
const store = this.$store;
const currentSize = {
width : this.activeDocument.width,
@@ -624,10 +625,10 @@ export default {
redo: commit
});
},
navigateHistory( action = "undo" ) {
navigateHistory( action = "undo" ): void {
this.$store.dispatch( action );
},
duplicateLayer() {
duplicateLayer(): void {
const indexToAdd = this.activeLayerIndex + 1;
let layer = {
...cloneDeep( this.activeLayer ),
@@ -648,9 +649,9 @@ export default {
redo: commit,
});
},
async mergeLayerDown( allLayers = false ) {
let layers = [];
let layerIndices = [];
async mergeLayerDown( allLayers = false ): Promise<void> {
let layers: Layer[] = [];
let layerIndices: number[] = [];
// collect the layers in ascending order
if ( allLayers ) {
this.activeDocument.layers.forEach(( layer, index ) => {
@@ -686,11 +687,11 @@ export default {
redo: commit,
});
},
copyLayerFilters() {
copyLayerFilters(): void {
this.clonedFilters = { ...this.activeLayer.filters };
this.showNotification({ message: this.$t( "filtersCopied" ) });
},
pasteLayerFilters() {
pasteLayerFilters(): void {
const orgFilters = { ...this.activeLayer.filters };
const filters = { ...this.clonedFilters };
const index = this.activeLayerIndex;
@@ -704,7 +705,7 @@ export default {
redo: commit,
});
},
toggleLayerFilters() {
toggleLayerFilters(): void {
const enabled = this.activeLayerHasFilters;
const filters = this.activeLayer.filters;
this.updateLayer({
@@ -712,13 +713,13 @@ export default {
opts: { filters: { ...filters, enabled: !enabled} }
});
},
selectAll() {
getCanvasInstance()?.interactionPane.selectAll( this.activeLayer );
selectAll(): void {
getCanvasInstance()?.interactionPane.selectAll();
},
close() {
close(): void {
this.setMenuOpened( false );
this.activeSubMenu = null;
}
},
}
};
</script>

View File

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

View File

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

View File

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

View File

@@ -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",

View File

@@ -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 )]
);
}

View File

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

View File

@@ -262,14 +262,14 @@ export default {
message: translate( "savedFileSuccessfully" , { file: truncate( name, 35 ) })
});
},
async requestSelectionCopy({ commit, getters }: ActionContext<BitMapperyState, any>, copyMerged = false ): Promise<void> {
const selectionImage = await copySelection( getters.activeDocument, getters.activeLayer, copyMerged );
async requestSelectionCopy({ commit, getters }: ActionContext<BitMapperyState, any>, { merged = false, isCut = false }): Promise<void> {
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<BitMapperyState, any> ): Promise<void> {
dispatch( "requestSelectionCopy" );
dispatch( "requestSelectionCopy", { merged: false, isCut: true });
dispatch( "deleteInSelection" );
},
clearSelection(): void {