Selections can now be stored and reloaded at Document level

This commit is contained in:
Igor Zinken
2020-12-29 14:19:11 +01:00
parent 739f50dfb9
commit 6f825e3ed7
15 changed files with 280 additions and 12 deletions

View File

@@ -72,7 +72,6 @@ npm run lint
* Zoom set original size isn't that accurate (check also on mobile views), needs calculateMaxScaling ?
* Unload Blobs when images are no longer used in document (see sprite-factory disposeSprite, keep instance count of usages)
* Load/save documents directly from/to Dropbox
* Load/save selections (into document)
* Implement layer scaling
* Implement rectangular selection
* Implement merged layer selection

View File

@@ -78,7 +78,7 @@ import store from "./store";
import messages from "./messages.json";
import {
RESIZE_DOCUMENT, SAVE_DOCUMENT, EXPORT_IMAGE, DROPBOX_FILE_SELECTOR,
ADD_LAYER
ADD_LAYER, LOAD_SELECTION, SAVE_SELECTION
} from "@/definitions/modal-windows";
Vue.use( Vuex );
@@ -127,6 +127,10 @@ export default {
return () => import( "@/components/dropbox-file-selector/dropbox-file-selector" );
case ADD_LAYER:
return () => import( "@/components/options-panel/components/add-layer/add-layer" );
case LOAD_SELECTION:
return () => import( "@/components/selection-menu/load-selection/load-selection" );
case SAVE_SELECTION:
return () => import( "@/components/selection-menu/save-selection/save-selection" );
}
},
},

View File

@@ -106,6 +106,20 @@
@click="clearSelection()"
></button>
</li>
<li>
<button v-t="'loadSelection'"
type="button"
:disabled="!hasSavedSelections"
@click="requestSelectionLoad()"
></button>
</li>
<li>
<button v-t="'saveSelection'"
type="button"
:disabled="!hasSelection"
@click="requestSelectionSave()"
></button>
</li>
</ul>
</li>
<!-- window menu -->
@@ -134,7 +148,9 @@
<script>
import { mapState, mapGetters, mapMutations, mapActions } from "vuex";
import { RESIZE_DOCUMENT, SAVE_DOCUMENT, EXPORT_IMAGE } from "@/definitions/modal-windows";
import {
RESIZE_DOCUMENT, SAVE_DOCUMENT, EXPORT_IMAGE, LOAD_SELECTION, SAVE_SELECTION
} from "@/definitions/modal-windows";
import { supportsFullscreen, setToggleButton } from "@/utils/environment-util";
import { runSpriteFn } from "@/factories/sprite-factory";
import messages from "./messages.json";
@@ -159,6 +175,9 @@ export default {
hasSelection() {
return this.activeLayer?.selection?.length > 0;
},
hasSavedSelections() {
return Object.keys( this.activeDocument?.selections || {} ).length > 0;
},
hasClipboard() {
return !!this.selectionContent;
},
@@ -202,6 +221,12 @@ export default {
requestDocumentSave() {
this.openModal( SAVE_DOCUMENT );
},
requestSelectionLoad() {
this.openModal( LOAD_SELECTION );
},
requestSelectionSave() {
this.openModal( SAVE_SELECTION );
},
}
};
</script>

View File

@@ -10,6 +10,8 @@
"resizeDocument": "Resize document",
"selection": "Selection",
"deselectAll": "Deselect all",
"loadSelection": "Load selection",
"saveSelection": "Save selection",
"copySelection": "Copy selection",
"pasteAsNewLayer": "Paste as new layer",
"window": "Window",

View File

@@ -159,6 +159,8 @@ export default {
.close-button {
top: $spacing-small - $spacing-xxsmall;
right: $spacing-xxsmall;
width: 36px;
height: 29px;
img {
width: $spacing-medium + $spacing-small;

View File

@@ -0,0 +1,101 @@
/**
* The MIT License (MIT)
*
* Igor Zinken 2020 - 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.
*/
<template>
<modal>
<template #header>
<h2 v-t="'loadSelection'"></h2>
</template>
<template #content>
<div class="form" @keyup.enter="requestLoad()">
<div class="wrapper input">
<label v-t="'availableSelections'"></label>
<select-box :options="selections"
v-model="name"
/>
</div>
</div>
</template>
<template #actions>
<button
v-t="'load'"
type="button"
class="button"
:disabled="!isValid"
@click="requestLoad()"
></button>
<button
v-t="'cancel'"
type="button"
class="button"
@click="closeModal()"
></button>
</template>
</modal>
</template>
<script>
import { mapGetters, mapMutations } from "vuex";
import ToolTypes from "@/definitions/tool-types";
import SelectBox from '@/components/ui/select-box/select-box';
import { mapSelectOptions } from "@/utils/search-select-util";
import { getSpriteForLayer } from "@/factories/sprite-factory";
import Modal from "@/components/modal/modal";
import messages from "./messages.json";
export default {
i18n: { messages },
components: {
Modal,
SelectBox,
},
data: () => ({
name: "",
}),
computed: {
...mapGetters([
"activeDocument",
"activeLayer",
]),
selections() {
return mapSelectOptions( Object.keys( this.activeDocument.selections ));
},
isValid() {
return this.name.length > 0;
},
},
methods: {
...mapMutations([
"closeModal",
"setActiveTool",
]),
requestLoad() {
if ( !this.isValid ) {
return;
}
this.setActiveTool({ tool: ToolTypes.LASSO, activeLayer: this.activeLayer });
getSpriteForLayer( this.activeLayer )?.setSelection( this.activeDocument.selections[ this.name ]);
this.closeModal();
},
},
};
</script>

View File

@@ -0,0 +1,8 @@
{
"en-US": {
"loadSelection": "Load selection",
"availableSelections": "Available selections",
"load": "Load",
"cancel": "Cancel"
}
}

View File

@@ -0,0 +1,8 @@
{
"en-US": {
"saveSelection": "Save selection",
"name": "Name",
"save": "Save",
"cancel": "Cancel"
}
}

View File

@@ -0,0 +1,95 @@
/**
* The MIT License (MIT)
*
* Igor Zinken 2020 - 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.
*/
<template>
<modal>
<template #header>
<h2 v-t="'saveSelection'"></h2>
</template>
<template #content>
<div class="form" @keyup.enter="requestSave()">
<div class="wrapper input">
<label v-t="'name'"></label>
<input ref="nameInput"
type="text"
v-model="name"
/>
</div>
</div>
</template>
<template #actions>
<button
v-t="'save'"
type="button"
class="button"
:disabled="!isValid"
@click="requestSave()"
></button>
<button
v-t="'cancel'"
type="button"
class="button"
@click="closeModal()"
></button>
</template>
</modal>
</template>
<script>
import { mapGetters, mapMutations } from "vuex";
import Modal from "@/components/modal/modal";
import messages from "./messages.json";
export default {
i18n: { messages },
components: {
Modal,
},
data: () => ({
name: "",
}),
computed: {
...mapGetters([
"activeLayer",
]),
isValid() {
return this.name.length > 0;
},
},
mounted() {
this.$refs.nameInput.focus();
},
methods: {
...mapMutations([
"closeModal",
"saveSelection",
]),
requestSave() {
if ( !this.isValid ) {
return;
}
this.saveSelection({ name: this.name, selection: this.activeLayer?.selection });
this.closeModal();
},
},
};
</script>

View File

@@ -197,6 +197,8 @@ export default {
.close-button {
top: $spacing-small - $spacing-xxsmall;
right: $spacing-xxsmall;
width: 36px;
height: 29px;
img {
width: $spacing-medium + $spacing-small;
@@ -237,7 +239,7 @@ export default {
.color-panel {
vertical-align: middle;
display: inline-flex;
border-top: 1px solid $color-lines;
border-top: 1px solid #444;
margin-top: $spacing-small;
padding-top: $spacing-medium - $spacing-xsmall;

View File

@@ -167,13 +167,18 @@ class LayerSprite extends sprite {
resetSelection() {
if ( this._isSelectMode ) {
Vue.set( this.layer, "selection", [] );
this.setSelection( [] );
} else {
Vue.delete( this.layer, "selection" );
}
this._selectionClosed = false;
}
setSelection( value ) {
Vue.set( this.layer, "selection", value );
this._selectionClosed = true; // TODO: can we determine this from first and last point?
}
async resize( width, height ) {
const ratioX = width / this._bounds.width;
const ratioY = height / this._bounds.height;

View File

@@ -25,3 +25,5 @@ export const SAVE_DOCUMENT = 2;
export const EXPORT_IMAGE = 3;
export const DROPBOX_FILE_SELECTOR = 4;
export const ADD_LAYER = 5;
export const SAVE_SELECTION = 6;
export const LOAD_SELECTION = 7;

View File

@@ -30,7 +30,7 @@ const DocumentFactory = {
* all layers and image content)
*/
create({
name = "New document", width = 400, height = 300, layers = []
name = "New document", width = 400, height = 300, layers = [], selections = {}
} = {}) {
if ( !layers.length ) {
layers = [ LayerFactory.create({ width, height }) ];
@@ -41,6 +41,7 @@ const DocumentFactory = {
name,
width,
height,
selections,
};
},
@@ -54,7 +55,8 @@ const DocumentFactory = {
n: document.name,
w: document.width,
h: document.height,
l: layers
l: layers,
s: document.selections,
};
},
@@ -70,7 +72,8 @@ const DocumentFactory = {
name: document.n,
width: document.w,
height: document.h,
layers
layers,
selections: document.s,
});
}
};

View File

@@ -125,6 +125,10 @@ export default {
sprite.cacheEffects();
}
},
saveSelection( state, { name, selection }) {
const document = state.documents[ state.activeIndex ];
Vue.set( document.selections, name, selection );
},
},
actions: {
requestNewDocument({ commit, getters }) {

View File

@@ -17,7 +17,8 @@ describe( "Document factory", () => {
name: "New document",
width: 400,
height: 300,
layers: [ { layer: "1" } ]
layers: [ { layer: "1" } ],
selections: {}
});
});
@@ -27,14 +28,16 @@ describe( "Document factory", () => {
name: "foo",
width: 1200,
height: 900,
layers
layers,
selections: { foo: [{ x: 0, y: 0 }] }
});
expect( document ).toEqual({
id: expect.any( String ),
name: "foo",
width: 1200,
height: 900,
layers
layers,
selections: { foo: [{ x: 0, y: 0 }] },
});
});
});
@@ -42,11 +45,16 @@ describe( "Document factory", () => {
describe( "when serializing and deserializing a Document", () => {
it( "should do so without data loss", async () => {
const layers = [ { layer: "1" }, { layer: "2" } ];
const selections = {
foo: [{ x: 0, y: 0 }, { x: 10, y: 10 }, { x: 0, y: 10 }, { x: 0, y: 0 }],
bar: []
};
const document = DocumentFactory.create({
name: "foo",
width: 1200,
height: 900,
layers
layers,
selections
});
mockUpdateFn = jest.fn(( fn, data ) => JSON.stringify( data ));
const serialized = DocumentFactory.serialize( document );