mirror of
https://github.com/igorski/bitmappery.git
synced 2026-07-26 16:57:43 +02:00
Added action to remove a tile and its layers from the Document
This commit is contained in:
102
src/store/actions/tile-delete.ts
Normal file
102
src/store/actions/tile-delete.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* The MIT License (MIT)
|
||||
*
|
||||
* Igor Zinken 2026 - 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
|
||||
* the Software without restriction, including without limitation the rights to
|
||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
* 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 Store } from "vuex";
|
||||
import type { Document, Layer } from "@/definitions/document";
|
||||
import { enqueueState } from "@/factories/history-state-factory";
|
||||
import { type BitMapperyState } from "@/store";
|
||||
import { getLayersByTile, getIndexOfFirstLayerInTileGroup } from "@/utils/timeline-util";
|
||||
|
||||
export const deleteTile = ( store: Store<BitMapperyState>, activeDocument: Document, tile: number ): void => {
|
||||
const layers = getLayersByTile( activeDocument, tile );
|
||||
const currentActiveSet = store.getters.activeSet;
|
||||
const currentTileAmount = activeDocument.sets.length;
|
||||
|
||||
// clone the layers of the current tile
|
||||
|
||||
const layerIndex = getIndexOfFirstLayerInTileGroup( activeDocument, tile );
|
||||
|
||||
// in case the next tile (and subsequent ones) already exist, gather them so we can decrement their tile offset
|
||||
|
||||
const shiftingGroups = new Map<number, string[]>(); // key == tile id, value is layer ids
|
||||
|
||||
let tileIndex = tile;
|
||||
|
||||
while ( ++tileIndex < currentTileAmount ) {
|
||||
const prevLayers = getLayersByTile( activeDocument, tileIndex );
|
||||
if ( prevLayers.length > 0 ) {
|
||||
shiftingGroups.set( tileIndex, prevLayers.map( layer => layer.id ));
|
||||
}
|
||||
}
|
||||
|
||||
const commit = (): void => {
|
||||
layers.forEach( layer => {
|
||||
store.commit( "removeLayer", activeDocument.layers.findIndex( compare => compare.id === layer.id ));
|
||||
});
|
||||
|
||||
// decrement the tile indices of all layers after the remove set
|
||||
for ( const [ tileIndex, layerIds ] of shiftingGroups ) {
|
||||
for ( const layerId of layerIds ) {
|
||||
store.commit( "updateLayer", {
|
||||
index: activeDocument.layers.findIndex( layer => layer.id === layerId ),
|
||||
opts: {
|
||||
rel: {
|
||||
type: "tile",
|
||||
id: tileIndex - 1,
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
if ( currentActiveSet === tile ) {
|
||||
store.commit( "setActiveSet", Math.max( 0, tile - 1 ));
|
||||
}
|
||||
};
|
||||
commit();
|
||||
|
||||
enqueueState( `tileDelete_${tile}`, {
|
||||
undo(): void {
|
||||
layers.forEach(( layer: Layer, index: number ) => {
|
||||
store.commit( "insertLayerAtIndex", { index: layerIndex + index, layer });
|
||||
});
|
||||
|
||||
// restore the tile indices of all layers after the inserted set
|
||||
for ( const [ tileIndex, layerIds ] of shiftingGroups ) {
|
||||
for ( const layerId of layerIds ) {
|
||||
store.commit( "updateLayer", {
|
||||
index: activeDocument.layers.findIndex( layer => layer.id === layerId ),
|
||||
opts: {
|
||||
rel: {
|
||||
type: "tile",
|
||||
id: tileIndex,
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
if ( currentActiveSet === tile ) {
|
||||
store.commit( "setActiveSet", currentActiveSet );
|
||||
}
|
||||
},
|
||||
redo: commit,
|
||||
});
|
||||
};
|
||||
@@ -61,7 +61,7 @@ describe( "Tile add action", () => {
|
||||
});
|
||||
|
||||
it( "should mark the added set index as the active set", () => {
|
||||
addTile( store, activeDocument, 2 );
|
||||
addTile( store, activeDocument );
|
||||
|
||||
expect( store.commit ).toHaveBeenCalledWith( "setActiveSet", 2 );
|
||||
});
|
||||
|
||||
142
tests/unit/store/actions/tile-delete.spec.ts
Normal file
142
tests/unit/store/actions/tile-delete.spec.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
import { type Store } from "vuex";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { createStore, mockZCanvas } from "../../mocks";
|
||||
|
||||
mockZCanvas();
|
||||
|
||||
import { type Document } from "@/definitions/document";
|
||||
import DocumentFactory from "@/factories/document-factory";
|
||||
import LayerFactory from "@/factories/layer-factory";
|
||||
import { type BitMapperyState } from "@/store";
|
||||
import { deleteTile } from "@/store/actions/tile-delete";
|
||||
|
||||
const mockEnqueueState = vi.fn();
|
||||
vi.mock( "@/factories/history-state-factory", () => ({
|
||||
enqueueState: ( ...args: any[] ) => mockEnqueueState( ...args ),
|
||||
}));
|
||||
|
||||
describe( "Tile delete action", () => {
|
||||
let store: Store<BitMapperyState>;
|
||||
let activeDocument: Document;
|
||||
|
||||
const tile1Layer1 = LayerFactory.create({ rel: { type: "tile", id: 0 }});
|
||||
const tile1Layer2 = LayerFactory.create({ rel: { type: "tile", id: 0 }});
|
||||
|
||||
const tile2Layer1 = LayerFactory.create({ rel: { type: "tile", id: 1 }});
|
||||
const tile2Layer2 = LayerFactory.create({ rel: { type: "tile", id: 1 }});
|
||||
const tile2Layer3 = LayerFactory.create({ rel: { type: "tile", id: 1 }});
|
||||
|
||||
const tile3Layer1 = LayerFactory.create({ rel: { type: "tile", id: 2 }});
|
||||
|
||||
const tile4Layer1 = LayerFactory.create({ rel: { type: "tile", id: 3 }});
|
||||
const tile4Layer2 = LayerFactory.create({ rel: { type: "tile", id: 3 }});
|
||||
|
||||
beforeEach(() => {
|
||||
store = createStore();
|
||||
store.getters.activeSet = 1;
|
||||
|
||||
activeDocument = DocumentFactory.create({
|
||||
layers: [
|
||||
tile1Layer1, tile1Layer2,
|
||||
tile2Layer1, tile2Layer2, tile2Layer3,
|
||||
tile3Layer1,
|
||||
tile4Layer1, tile4Layer2,
|
||||
],
|
||||
});
|
||||
activeDocument.sets = [ 0, 1, 2, 3 ];
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
describe( "when delete a tile", () => {
|
||||
it( "should be able to delete the tiles Layer contents", () => {
|
||||
deleteTile( store, activeDocument, 1 );
|
||||
|
||||
expect( store.commit ).toHaveBeenNthCalledWith( 1, "removeLayer", 2 );
|
||||
expect( store.commit ).toHaveBeenNthCalledWith( 2, "removeLayer", 3 );
|
||||
expect( store.commit ).toHaveBeenNthCalledWith( 3, "removeLayer", 4 );
|
||||
});
|
||||
|
||||
it( "should update the active set index to the previous one in case it is the deleted set", () => {
|
||||
deleteTile( store, activeDocument, 1 );
|
||||
|
||||
expect( store.commit ).toHaveBeenCalledWith( "setActiveSet", 0 );
|
||||
});
|
||||
|
||||
it( "should not update the active set index in case it is not equal to the deleted set", () => {
|
||||
deleteTile( store, activeDocument, 2 );
|
||||
|
||||
expect( store.commit ).not.toHaveBeenCalledWith( "setActiveSet", expect.any( Number ));
|
||||
});
|
||||
|
||||
it( "should be able to update the set indices of any subsequent tile layers", () => {
|
||||
deleteTile( store, activeDocument, 1 );
|
||||
|
||||
// validate existing sets with id 2 and 3 have moved to 1 and 2
|
||||
// NOTE: we don't check for exact index numbers as the mocked store function do not update the document
|
||||
// checking for rel id should cover the use case though
|
||||
|
||||
expect( store.commit ).toHaveBeenNthCalledWith( 4, "updateLayer", { index: expect.any( Number ), opts: { rel: { type: "tile", id: 1 }}});
|
||||
expect( store.commit ).toHaveBeenNthCalledWith( 5, "updateLayer", { index: expect.any( Number ), opts: { rel: { type: "tile", id: 2 }}});
|
||||
expect( store.commit ).toHaveBeenNthCalledWith( 6, "updateLayer", { index: expect.any( Number ), opts: { rel: { type: "tile", id: 2 }}});
|
||||
|
||||
expect( store.commit ).toHaveBeenNthCalledWith( 7, "setActiveSet", 0 );
|
||||
});
|
||||
});
|
||||
|
||||
describe( "when restoring a tile delete step", () => {
|
||||
it( "should restore the deleted layers", () => {
|
||||
deleteTile( store, activeDocument, 1 );
|
||||
|
||||
const { undo } = mockEnqueueState.mock.calls[ 0 ][ 1 ];
|
||||
vi.resetAllMocks();
|
||||
|
||||
undo();
|
||||
|
||||
expect( store.commit ).toHaveBeenNthCalledWith( 1, "insertLayerAtIndex", { index: 2, layer: tile2Layer1 });
|
||||
expect( store.commit ).toHaveBeenNthCalledWith( 2, "insertLayerAtIndex", { index: 3, layer: tile2Layer2 });
|
||||
expect( store.commit ).toHaveBeenNthCalledWith( 3, "insertLayerAtIndex", { index: 4, layer: tile2Layer3 });
|
||||
});
|
||||
|
||||
it( "should mark the originally active set as the active set in case it was the deleted one", () => {
|
||||
deleteTile( store, activeDocument, 1 );
|
||||
|
||||
const { undo } = mockEnqueueState.mock.calls[ 0 ][ 1 ];
|
||||
vi.resetAllMocks();
|
||||
|
||||
undo();
|
||||
|
||||
expect( store.commit ).toHaveBeenCalledWith( "setActiveSet", 1 );
|
||||
});
|
||||
|
||||
it( "should not update the active set when the originally active set was not the deleted one", () => {
|
||||
deleteTile( store, activeDocument, 2 );
|
||||
|
||||
const { undo } = mockEnqueueState.mock.calls[ 0 ][ 1 ];
|
||||
vi.resetAllMocks();
|
||||
|
||||
undo();
|
||||
|
||||
expect( store.commit ).not.toHaveBeenCalledWith( "setActiveSet", expect.any( Number ));
|
||||
});
|
||||
|
||||
it( "should restore the original tile set indices of all subsequent tile layers when restoring a delete action between tile sets", () => {
|
||||
deleteTile( store, activeDocument, 1 );
|
||||
|
||||
const { undo } = mockEnqueueState.mock.calls[ 0 ][ 1 ];
|
||||
vi.resetAllMocks();
|
||||
|
||||
undo();
|
||||
|
||||
// validate updated sets with ids 1 and 2 have moved back to 2 and 3
|
||||
|
||||
expect( store.commit ).toHaveBeenNthCalledWith( 4, "updateLayer", { index: expect.any( Number ), opts: { rel: { type: "tile", id: 2 }}});
|
||||
expect( store.commit ).toHaveBeenNthCalledWith( 5, "updateLayer", { index: expect.any( Number ), opts: { rel: { type: "tile", id: 3 }}});
|
||||
expect( store.commit ).toHaveBeenNthCalledWith( 6, "updateLayer", { index: expect.any( Number ), opts: { rel: { type: "tile", id: 3 }}});
|
||||
|
||||
expect( store.commit ).toHaveBeenNthCalledWith( 7, "setActiveSet", 1 );
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user