diff --git a/README.md b/README.md index f3ceb08..65920a3 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/src/bitmappery.vue b/src/bitmappery.vue index b963b80..147f643 100644 --- a/src/bitmappery.vue +++ b/src/bitmappery.vue @@ -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" ); } }, }, diff --git a/src/components/application-menu/application-menu.vue b/src/components/application-menu/application-menu.vue index be2fa5d..2ed4a7d 100644 --- a/src/components/application-menu/application-menu.vue +++ b/src/components/application-menu/application-menu.vue @@ -106,6 +106,20 @@ @click="clearSelection()" > +
  • + +
  • +
  • + +
  • @@ -134,7 +148,9 @@ diff --git a/src/components/application-menu/messages.json b/src/components/application-menu/messages.json index d90a070..93e476f 100644 --- a/src/components/application-menu/messages.json +++ b/src/components/application-menu/messages.json @@ -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", diff --git a/src/components/options-panel/options-panel.vue b/src/components/options-panel/options-panel.vue index 58edcf2..07c4f8b 100644 --- a/src/components/options-panel/options-panel.vue +++ b/src/components/options-panel/options-panel.vue @@ -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; diff --git a/src/components/selection-menu/load-selection/load-selection.vue b/src/components/selection-menu/load-selection/load-selection.vue new file mode 100644 index 0000000..91b20e0 --- /dev/null +++ b/src/components/selection-menu/load-selection/load-selection.vue @@ -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. + */ + + + diff --git a/src/components/selection-menu/load-selection/messages.json b/src/components/selection-menu/load-selection/messages.json new file mode 100644 index 0000000..8738d80 --- /dev/null +++ b/src/components/selection-menu/load-selection/messages.json @@ -0,0 +1,8 @@ +{ + "en-US": { + "loadSelection": "Load selection", + "availableSelections": "Available selections", + "load": "Load", + "cancel": "Cancel" + } +} diff --git a/src/components/selection-menu/save-selection/messages.json b/src/components/selection-menu/save-selection/messages.json new file mode 100644 index 0000000..8179a6a --- /dev/null +++ b/src/components/selection-menu/save-selection/messages.json @@ -0,0 +1,8 @@ +{ + "en-US": { + "saveSelection": "Save selection", + "name": "Name", + "save": "Save", + "cancel": "Cancel" + } +} diff --git a/src/components/selection-menu/save-selection/save-selection.vue b/src/components/selection-menu/save-selection/save-selection.vue new file mode 100644 index 0000000..1a10beb --- /dev/null +++ b/src/components/selection-menu/save-selection/save-selection.vue @@ -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. + */ + + + diff --git a/src/components/toolbox/toolbox.vue b/src/components/toolbox/toolbox.vue index 136864c..e4e5a14 100644 --- a/src/components/toolbox/toolbox.vue +++ b/src/components/toolbox/toolbox.vue @@ -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; diff --git a/src/components/ui/zcanvas/layer-sprite.js b/src/components/ui/zcanvas/layer-sprite.js index 2e6433d..bb6d0be 100644 --- a/src/components/ui/zcanvas/layer-sprite.js +++ b/src/components/ui/zcanvas/layer-sprite.js @@ -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; diff --git a/src/definitions/modal-windows.js b/src/definitions/modal-windows.js index 3b3e0a9..30bc088 100644 --- a/src/definitions/modal-windows.js +++ b/src/definitions/modal-windows.js @@ -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; diff --git a/src/factories/document-factory.js b/src/factories/document-factory.js index 618f200..912b837 100644 --- a/src/factories/document-factory.js +++ b/src/factories/document-factory.js @@ -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, }); } }; diff --git a/src/store/modules/document-module.js b/src/store/modules/document-module.js index feff6f1..c548310 100644 --- a/src/store/modules/document-module.js +++ b/src/store/modules/document-module.js @@ -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 }) { diff --git a/tests/unit/factories/document-factory.spec.js b/tests/unit/factories/document-factory.spec.js index caf5bfc..459e101 100644 --- a/tests/unit/factories/document-factory.spec.js +++ b/tests/unit/factories/document-factory.spec.js @@ -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 );