Added action to remove a tile and its layers from the Document

This commit is contained in:
Igor Zinken
2026-03-22 12:07:36 +01:00
parent 1742b7681a
commit b28003e6a4
3 changed files with 245 additions and 1 deletions

View 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,
});
};

View File

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

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