diff --git a/src/components/layer-panel/layer-panel.vue b/src/components/layer-panel/layer-panel.vue index c8f9b8f..05f2fb0 100644 --- a/src/components/layer-panel/layer-panel.vue +++ b/src/components/layer-panel/layer-panel.vue @@ -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; diff --git a/src/definitions/editor.ts b/src/definitions/editor.ts index 811b60b..70c4362 100644 --- a/src/definitions/editor.ts +++ b/src/definitions/editor.ts @@ -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 }; \ No newline at end of file diff --git a/src/store/index.ts b/src/store/index.ts index b149e49..e4b616a 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -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 ): 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, diff --git a/src/utils/document-util.ts b/src/utils/document-util.ts index 34a8c3e..cc10cb3 100644 --- a/src/utils/document-util.ts +++ b/src/utils/document-util.ts @@ -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 => { +export const copySelection = async ( activeDocument: Document, activeLayer: Layer, copyMerged = false ): Promise => { 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, + }; }; /** diff --git a/tests/unit/definitions/tool-types.spec.ts b/tests/unit/definitions/tool-types.spec.ts index 9173fb6..055b06f 100644 --- a/tests/unit/definitions/tool-types.spec.ts +++ b/tests/unit/definitions/tool-types.spec.ts @@ -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 ); }); }); }); \ No newline at end of file diff --git a/tests/unit/store/store.spec.ts b/tests/unit/store/store.spec.ts index 83432fa..2f55f26 100644 --- a/tests/unit/store/store.spec.ts +++ b/tests/unit/store/store.spec.ts @@ -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' - 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' + 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' + 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' + 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 ); + }); }); }); });