mirror of
https://github.com/igorski/bitmappery.git
synced 2026-06-17 03:34:56 +02:00
Selections can now be stored and reloaded at Document level
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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" );
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
101
src/components/selection-menu/load-selection/load-selection.vue
Normal file
101
src/components/selection-menu/load-selection/load-selection.vue
Normal 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>
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"en-US": {
|
||||
"loadSelection": "Load selection",
|
||||
"availableSelections": "Available selections",
|
||||
"load": "Load",
|
||||
"cancel": "Cancel"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"en-US": {
|
||||
"saveSelection": "Save selection",
|
||||
"name": "Name",
|
||||
"save": "Save",
|
||||
"cancel": "Cancel"
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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 }) {
|
||||
|
||||
@@ -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 );
|
||||
|
||||
Reference in New Issue
Block a user