Added layer interaction to state history

This commit is contained in:
Igor Zinken
2021-01-12 17:15:24 +01:00
parent 060e3abea0
commit 3d343c63f4
7 changed files with 138 additions and 46 deletions

View File

@@ -202,6 +202,7 @@ import {
import { supportsFullscreen, setToggleButton } from "@/utils/environment-util";
import { getRectangleForSelection } from "@/math/selection-math";
import { getCanvasInstance, runSpriteFn, getSpriteForLayer } from "@/factories/sprite-factory";
import { enqueueState } from "@/factories/history-state-factory";
import messages from "./messages.json";
export default {
@@ -283,10 +284,25 @@ export default {
requestSelectionSave() {
this.openModal( SAVE_SELECTION );
},
async requestCropToSelection() {
requestCropToSelection() {
const store = this.$store;
const currentSize = {
width: this.activeDocument.width,
height: this.activeDocument.height
};
const { left, top, width, height } = getRectangleForSelection( this.activeLayer.selection );
await this.cropActiveDocumentContent({ left, top });
this.setActiveDocumentSize({ width, height });
const commit = async () => {
await store.commit( "cropActiveDocumentContent", { left, top });
store.commit( "setActiveDocumentSize", { width, height });
};
commit();
enqueueState("crop", {
async undo() {
await store.commit( "cropActiveDocumentContent", { left: -left, top: -top });
store.commit( "setActiveDocumentSize", currentSize );
},
redo: commit
});
},
requestDropboxLoad() {
this.openModal( DROPBOX_FILE_SELECTOR );

View File

@@ -1,7 +1,7 @@
/**
* The MIT License (MIT)
*
* Igor Zinken 2020 - https://www.igorski.nl
* Igor Zinken 2020-2021 - 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
@@ -64,6 +64,7 @@
import { mapGetters, mapMutations, mapActions } from "vuex";
import Modal from "@/components/modal/modal";
import SelectBox from '@/components/ui/select-box/select-box';
import { enqueueState } from "@/factories/history-state-factory";
import { LAYER_GRAPHIC, LAYER_TEXT } from "@/definitions/layer-types";
import messages from "./messages.json";
@@ -80,6 +81,7 @@ export default {
computed: {
...mapGetters([
"activeDocument",
"activeLayerIndex",
"layers",
]),
layerTypes() {
@@ -98,18 +100,28 @@ export default {
},
methods: {
...mapMutations([
"addLayer",
"closeModal",
]),
requestLayerAdd() {
if ( !this.isValid ) {
return;
}
this.addLayer({
name: this.name,
type: this.type,
width: this.activeDocument.width,
height: this.activeDocument.height
const newLayer = {
name : this.name,
type : this.type,
width : this.activeDocument.width,
height : this.activeDocument.height
};
const store = this.$store;
const commit = () => store.commit( "addLayer", newLayer );
commit();
const addedLayerIndex = this.activeLayerIndex;
enqueueState( "layerAdd", {
undo() {
store.commit( "removeLayer", addedLayerIndex );
},
redo: commit,
});
this.closeModal();
},

View File

@@ -116,6 +116,7 @@ import { mapGetters, mapMutations } from "vuex";
import { ADD_LAYER, LAYER_FILTERS } from "@/definitions/modal-windows";
import { createCanvas } from "@/utils/canvas-util";
import { getSpriteForLayer } from "@/factories/sprite-factory";
import { enqueueState } from "@/factories/history-state-factory";
import KeyboardService from "@/services/keyboard-service";
import messages from "./messages.json";
@@ -153,7 +154,6 @@ export default {
...mapMutations([
"openModal",
"removeLayer",
"updateLayer",
"setActiveLayerIndex",
"setActiveLayerMask",
"openDialog",
@@ -161,31 +161,33 @@ export default {
requestLayerAdd() {
this.openModal( ADD_LAYER );
},
requestMaskAdd() {
this.updateLayer({
index: this.activeLayerIndex,
opts: {
mask: createCanvas( this.activeLayer.width, this.activeLayer.height ).cvs
}
});
},
handleLayerDoubleClick( index ) {
this.editable = true;
},
updateActiveLayerName({ target }) {
this.updateLayer({
index: this.activeLayerIndex,
opts: {
name: target.value
}
const newName = target.value;
const currentName = this.activeLayer.name;
const index = this.activeLayerIndex;
const store = this.$store;
const commit = () => store.commit( "updateLayer", { index, opts: { name: newName } });
commit();
enqueueState( `layerName_${index}`, {
undo() {
store.commit( "updateLayer", { index, opts: { name: currentName } });
},
redo: commit,
});
},
toggleLayerVisibility( index ) {
this.updateLayer({
index,
opts: {
visible: !this.layers[ index ].visible
}
const originalVisibility = this.layers[ index ].visible;
const store = this.$store;
const commit = () => store.commit( "updateLayer", { index, opts: { visible: !originalVisibility } });
commit();
enqueueState( `layerVisibility_${index}`, {
undo() {
store.commit( "updateLayer", { index, opts: { visible: originalVisibility } });
},
redo: commit,
});
},
handleFiltersClick( index ) {
@@ -201,22 +203,53 @@ export default {
}
},
requestLayerRemove( index ) {
const layer = this.layers[ index ];
this.openDialog({
type: "confirm",
title: this.$t( "areYouSure" ),
message: this.$t( "doYouWantToRemoveLayerName", { name: this.layers[ index ]?.name }),
message: this.$t( "doYouWantToRemoveLayerName", { name: layer.name }),
confirm: () => {
this.removeLayer( index );
const store = this.$store;
const commit = () => store.commit( "removeLayer", index );
commit();
enqueueState( `layerRemove_${index}`, {
undo() {
store.commit( "insertLayerAtIndex", { index, layer });
},
redo: commit,
});
}
});
},
requestMaskAdd() {
const index = this.activeLayerIndex;
const mask = createCanvas( this.activeLayer.width, this.activeLayer.height ).cvs;
const store = this.$store;
const commit = () => store.commit( "updateLayer", { index, opts: { mask } });
commit();
enqueueState( `maskAdd_${index}`, {
undo() {
store.commit( "updateLayer", { index, opts: { mask: null } });
},
redo: commit,
});
},
requestMaskRemove( index ) {
const mask = this.activeLayer.mask;
this.openDialog({
type: "confirm",
title: this.$t( "areYouSure" ),
message: this.$t( "doYouWantToRemoveMaskName", { name: this.layers[ index ]?.name }),
confirm: () => {
this.updateLayer({ index, opts: { mask: null } });
const store = this.$store;
const commit = () => store.commit( "updateLayer", { index, opts: { mask: null } });
commit();
enqueueState( `maskRemove_${index}`, {
undo() {
store.commit( "updateLayer", { index, opts: { mask } });
},
redo: commit,
});
}
});
},

View File

@@ -22,7 +22,8 @@
*/
import KeyboardService from "@/services/keyboard-service";
import DocumentFactory from "@/factories/document-factory";
import { initHistory } from "@/factories/history-state-factory";
import LayerFactory from "@/factories/layer-factory";
import { initHistory, enqueueState } from "@/factories/history-state-factory";
import { LAYER_IMAGE } from "@/definitions/layer-types";
import { runSpriteFn } from "@/factories/sprite-factory";
import canvasModule from "./modules/canvas-module";
@@ -193,15 +194,28 @@ export default {
runSpriteFn( sprite => sprite.resetSelection(), getters.activeDocument );
},
pasteSelection({ commit, getters, dispatch, state }) {
const { image, size } = state.selectionContent;
commit( "addLayer", {
const selection = state.selectionContent;
const { image, size } = selection;
const layer = LayerFactory.create({
type: LAYER_IMAGE,
source: image,
...size,
x: getters.activeDocument.width / 2 - size.width / 2,
y: getters.activeDocument.height / 2 - size.height / 2,
});
dispatch( "clearSelection" );
const index = getters.activeDocument.layers.length;
const paste = () => {
commit( "insertLayerAtIndex", { index, layer });
dispatch( "clearSelection" );
};
paste();
enqueueState( `paste_${selection.length}`, {
undo() {
commit( "setSelectionContent", selection );
commit( "removeLayer", index );
},
redo: paste
});
},
/**
* Install the services that will listen to global hardware events

View File

@@ -84,6 +84,11 @@ export default {
layers.push( LayerFactory.create( opts ));
state.activeLayerIndex = layers.length - 1;
},
insertLayerAtIndex( state, { index, layer }) {
const document = state.documents[ state.activeIndex ];
document.layers.splice( index, 0, layer );
state.activeLayerIndex = index;
},
removeLayer( state, index ) {
const layer = state.documents[ state.activeIndex ]?.layers[ index ];
if ( !layer ) {

View File

@@ -233,6 +233,20 @@ describe( "Vuex document module", () => {
{ name: "layer1" }, expect.any( Object )
]);
});
it( "should be able to add layers at specific indices in the layer list", () => {
const state = {
documents: [ { name: "foo", layers: [{ name: "layer1" }, { name: "layer2" }] }],
activeIndex: 0,
activeLayerIndex: 0,
};
const layer = { name: "layer3" };
mutations.insertLayerAtIndex( state, { index: 1, layer });
expect( state.documents[0].layers).toEqual([
{ name: "layer1" }, layer, { name: "layer2" }
]);
expect( state.activeLayerIndex ).toEqual( 1 );
});
});
describe( "when removing layers", () => {

View File

@@ -16,7 +16,12 @@ jest.mock( "@/factories/document-factory", () => ({
jest.mock( "@/utils/file-util", () => ({
selectFile: (...args) => mockUpdateFn?.( "selectFile", ...args ),
saveBlobAsFile: (...args) => mockUpdateFn?.( "saveBlobAsFile", ...args ),
}))
}));
jest.mock("@/utils/canvas-util", () => ({
createCanvas: jest.fn(),
imageToBase64: jest.fn(),
base64ToLayerImage: jest.fn()
}));
describe( "Vuex store", () => {
describe( "getters", () => {
@@ -270,18 +275,11 @@ describe( "Vuex store", () => {
const state = {
selectionContent: { image: { src: "foo" }, size: { width: 40, height: 30 } },
};
const mockedGetters = { activeDocument: { width: 200, height: 150 } };
const mockedGetters = { activeDocument: { width: 200, height: 150, layers: [] } };
const commit = jest.fn();
const dispatch = jest.fn();
await actions.pasteSelection({ state, getters: mockedGetters, commit, dispatch });
expect( commit ).toHaveBeenCalledWith( "addLayer", {
type: LAYER_IMAGE,
source: state.selectionContent.image,
width: state.selectionContent.size.width,
height: state.selectionContent.size.height,
x: 80,
y: 60
});
expect( commit ).toHaveBeenCalledWith( "insertLayerAtIndex", { index: 0, layer: expect.any( Object ) });
expect( dispatch ).toHaveBeenCalledWith( "clearSelection" );
});
});