Copying selection content onto new Layers reuses the original selections Layer type

This commit is contained in:
Igor Zinken
2025-03-15 09:53:07 +01:00
parent 1d37781855
commit 9ca9bbb732
6 changed files with 84 additions and 34 deletions

View File

@@ -463,6 +463,10 @@ export default {
padding: variables.$spacing-small variables.$spacing-small 0;
margin-left: variables.$spacing-small;
}
&__name-input {
padding-top: 0;
}
&__actions {
margin-right: variables.$spacing-small;

View File

@@ -1,7 +1,7 @@
/**
* The MIT License (MIT)
*
* Igor Zinken 2020-2023 - https://www.igorski.nl
* Igor Zinken 2020-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
@@ -20,8 +20,9 @@
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import type { Point } from "zcanvas";
import type { Point, SizedImage } from "zcanvas";
import type BrushTypes from "@/definitions/brush-types";
import type { LayerTypes } from "@/definitions/layer-types";
import type { Selection } from "@/definitions/document";
export type Notification = {
@@ -140,3 +141,5 @@ export type WandToolOptions = {
threshold: number;
sampleMerged: boolean;
};
export type CopiedSelection = SizedImage & { type: LayerTypes };

View File

@@ -21,8 +21,8 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { ActionContext } from "vuex";
import type { Size, SizedImage } from "zcanvas";
import type { Notification, Dialog } from "@/definitions/editor";
import type { Size } from "zcanvas";
import type { Notification, Dialog, CopiedSelection } from "@/definitions/editor";
import KeyboardService from "@/services/keyboard-service";
import DocumentFactory from "@/factories/document-factory";
import LayerFactory from "@/factories/layer-factory";
@@ -49,7 +49,7 @@ export interface BitMapperyState {
menuOpened: boolean;
toolboxOpened: boolean;
openedPanels: string[];
selectionContent: SizedImage | null; // clipboard content of copied images ({ image, size })
selectionContent: CopiedSelection | null; // clipboard content of copied images
blindActive: boolean;
panMode: boolean; // whether drag interactions with the document will pan its viewport
selectMode: boolean; // whether the currently active tool is a selection type (works across layers)
@@ -133,7 +133,7 @@ export default {
closeOpenedPanels( state: BitMapperyState ): void {
state.openedPanels = [];
},
setSelectionContent( state: BitMapperyState, image: SizedImage ): void {
setSelectionContent( state: BitMapperyState, image: CopiedSelection ): void {
state.selectionContent = image;
},
setBlindActive( state: BitMapperyState, active: boolean ): void {
@@ -281,9 +281,9 @@ export default {
},
pasteSelection({ commit, getters, dispatch, state }: ActionContext<BitMapperyState, any> ): void {
const selection = state.selectionContent;
const { image, size } = selection;
const { image, size, type } = selection;
const layer = LayerFactory.create({
type: LayerTypes.LAYER_GRAPHIC,
type: ( !type || type === LayerTypes.LAYER_TEXT ) ? LayerTypes.LAYER_GRAPHIC : type,
source: imageToCanvas( image, size.width, size.height ),
...size,
left: getters.activeDocument.width / 2 - size.width / 2,

View File

@@ -21,9 +21,10 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { canvas, loader } from "zcanvas";
import type { Rectangle, SizedImage } from "zcanvas";
import type { Rectangle } from "zcanvas";
import { PNG } from "@/definitions/image-types";
import type { Document, Shape, Layer } from "@/definitions/document";
import type { CopiedSelection } from "@/definitions/editor";
import { renderEffectsForLayer } from "@/services/render-service";
import { createSpriteForLayer, getSpriteForLayer } from "@/factories/sprite-factory";
import { rotateRectangle, areEqual } from "@/math/rectangle-math";
@@ -162,7 +163,7 @@ export const tilesToSingle = ( tiles: HTMLCanvasElement[], tileWidth: number, ti
/**
* Copy the selection defined in activeLayer into a separate Image
*/
export const copySelection = async ( activeDocument: Document, activeLayer: Layer, copyMerged = false ): Promise<SizedImage> => {
export const copySelection = async ( activeDocument: Document, activeLayer: Layer, copyMerged = false ): Promise<CopiedSelection> => {
const { zcvs, cvs, ctx } = createFullSizeZCanvas( activeDocument );
ctx.save();
@@ -200,7 +201,13 @@ export const copySelection = async ( activeDocument: Document, activeLayer: Laye
0, 0, selectionRectangle.width, selectionRectangle.height
);
zcvs.dispose();
return await loader.loadImage( selectionCanvas.cvs.toDataURL( PNG.mime ));
const output = await loader.loadImage( selectionCanvas.cvs.toDataURL( PNG.mime ));
return {
...output,
type: activeLayer.type,
};
};
/**

View File

@@ -22,14 +22,14 @@ describe( "tool types", () => {
LayerTypes.LAYER_IMAGE, LayerTypes.LAYER_TEXT,
])( `should not consider a "%s"-layer to be drawable`, ( type: LayerTypes ) => {
const layer = LayerFactory.create({ type });
expect( canDraw( document, layer )).toBe( false );
expect( canDraw( document, layer, null )).toBe( false );
});
it.each([
LayerTypes.LAYER_IMAGE, LayerTypes.LAYER_TEXT,
])( `should not consider a "%s"-layer with an unselected mask to be drawable`, ( type: LayerTypes ) => {
const layer = LayerFactory.create({ type, mask: createMockCanvasElement() });
expect( canDraw( document, layer )).toBe( false );
expect( canDraw( document, layer, null )).toBe( false );
});
it.each([
@@ -41,7 +41,7 @@ describe( "tool types", () => {
it( `should consider a "${LayerTypes.LAYER_GRAPHIC}"-layer to be drawable`, () => {
const layer = LayerFactory.create({ type: LayerTypes.LAYER_GRAPHIC });
expect( canDraw( document, layer )).toBe( true );
expect( canDraw( document, layer, null )).toBe( true );
});
});
});

View File

@@ -1,4 +1,4 @@
import { it, describe, expect, afterAll, vi } from "vitest";
import { it, beforeEach, describe, expect, afterAll, vi } from "vitest";
import { mockZCanvas } from "../mocks";
import { type Layer } from "@/definitions/document";
import { type Dialog } from "@/definitions/editor";
@@ -9,7 +9,7 @@ import { LayerTypes } from "@/definitions/layer-types";
import DocumentFactory from "@/factories/document-factory";
import LayerFactory from "@/factories/layer-factory";
import KeyboardService from "@/services/keyboard-service";
import store from "@/store";
import store, { type BitMapperyState } from "@/store";
import { createState, createMockImageElement } from "../mocks";
const { getters, mutations, actions } = store;
@@ -83,7 +83,7 @@ describe( "Vuex store", () => {
it( "should be able to set the current selection content", () => {
const state = createState({ selectionContent: null });
const selection = { image: createMockImageElement(), size: { width: 100, height: 50 } };
const selection = { image: createMockImageElement(), size: { width: 100, height: 50 }, type: LayerTypes.LAYER_GRAPHIC };
mutations.setSelectionContent( state, selection );
expect( state.selectionContent ).toEqual( selection );
});
@@ -385,25 +385,61 @@ describe( "Vuex store", () => {
expect( commit ).toHaveBeenCalledWith( "showNotification", expect.any( Object ));
});
it( "should be able to paste the current in-memory image selection at the center of the Document", async () => {
const state = createState({
selectionContent: {
image: createMockImageElement(),
size: {
width: 40,
height: 30
}
},
});
describe( "when pasting the current in-memory image selection", () => {
const mockedGetters = { activeDocument: { width: 200, height: 150, layers: [] as Layer[] } };
const commit = vi.fn();
const dispatch = vi.fn();
let state: BitMapperyState;
// @ts-expect-error not assignable to parameter of type 'ActionContext<BitMapperyState, any>'
await actions.pasteSelection({ state, getters: mockedGetters, commit, dispatch });
expect( commit ).toHaveBeenCalledWith( "insertLayerAtIndex", { index: 0, layer: expect.any( Object ) });
expect( dispatch ).toHaveBeenCalledWith( "clearSelection" );
beforeEach(() => {
state = createState({
selectionContent: {
image: createMockImageElement(),
size: {
width: 40,
height: 30
},
type: LayerTypes.LAYER_GRAPHIC,
},
});
});
it( "should be able to paste at the center of the Document", async () => {
const commit = vi.fn();
const dispatch = vi.fn();
// @ts-expect-error not assignable to parameter of type 'ActionContext<BitMapperyState, any>'
await actions.pasteSelection({ state, getters: mockedGetters, commit, dispatch });
expect( commit ).toHaveBeenCalledWith( "insertLayerAtIndex", { index: 0, layer: expect.any( Object ) });
expect( dispatch ).toHaveBeenCalledWith( "clearSelection" );
});
it.each(
[ LayerTypes.LAYER_GRAPHIC, LayerTypes.LAYER_IMAGE ]
)( `should keep the original type when the selection was made from a "$%s"-Layer`, async ( type: LayerTypes ) => {
const commit = vi.fn();
state.selectionContent.type = type;
// @ts-expect-error not assignable to parameter of type 'ActionContext<BitMapperyState, any>'
await actions.pasteSelection({ state, getters: mockedGetters, commit, dispatch: vi.fn() });
const createdLayer = commit.mock.calls.find(([ cmd ]) => cmd === "insertLayerAtIndex" )![ 1 ].layer;
expect( createdLayer.type ).toEqual( type );
});
it( `should convert a selection made from a "${LayerTypes.LAYER_TEXT}"-Layer to the "${LayerTypes.LAYER_GRAPHIC} type`, async () => {
const commit = vi.fn();
state.selectionContent.type = LayerTypes.LAYER_TEXT;;
// @ts-expect-error not assignable to parameter of type 'ActionContext<BitMapperyState, any>'
await actions.pasteSelection({ state, getters: mockedGetters, commit, dispatch: vi.fn() });
const createdLayer = commit.mock.calls.find(([ cmd ]) => cmd === "insertLayerAtIndex" )![ 1 ].layer;
expect( createdLayer.type ).toEqual( LayerTypes.LAYER_GRAPHIC );
});
});
});
});