mirror of
https://github.com/igorski/bitmappery.git
synced 2026-06-17 03:34:56 +02:00
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:
@@ -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>
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 )]
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user