From 60b87f66265b9c91c275293c63e7c119d6cc86d3 Mon Sep 17 00:00:00 2001 From: Igor Zinken Date: Sun, 10 Jan 2021 22:16:45 +0100 Subject: [PATCH] Separated math utility into area specific functions. Decluttered LayerSprite selection logic --- .../application-menu/application-menu.vue | 2 +- .../document-canvas/document-canvas.vue | 2 +- .../tool-options-rotate.vue | 2 +- src/components/ui/zcanvas/layer-sprite.js | 20 +++---- src/components/ui/zcanvas/zoomable-canvas.js | 2 +- src/definitions/tool-types.js | 2 +- src/{utils => math}/image-math.js | 20 ------- src/math/selection-math.js | 52 +++++++++++++++++++ src/services/keyboard-service.js | 2 +- src/services/render-service.js | 3 +- tests/unit/{utils => math}/image-math.spec.js | 22 +------- tests/unit/math/selection-math.spec.js | 49 +++++++++++++++++ 12 files changed, 118 insertions(+), 60 deletions(-) rename src/{utils => math}/image-math.js (93%) create mode 100644 src/math/selection-math.js rename tests/unit/{utils => math}/image-math.spec.js (83%) create mode 100644 tests/unit/math/selection-math.spec.js diff --git a/src/components/application-menu/application-menu.vue b/src/components/application-menu/application-menu.vue index 829ef6d..9d64c35 100644 --- a/src/components/application-menu/application-menu.vue +++ b/src/components/application-menu/application-menu.vue @@ -186,7 +186,7 @@ import { DROPBOX_FILE_SELECTOR, SAVE_DROPBOX_DOCUMENT } from "@/definitions/modal-windows"; import { supportsFullscreen, setToggleButton } from "@/utils/environment-util"; -import { getRectangleForSelection } from "@/utils/image-math"; +import { getRectangleForSelection } from "@/math/selection-math"; import { getCanvasInstance, runSpriteFn, getSpriteForLayer } from "@/factories/sprite-factory"; import messages from "./messages.json"; diff --git a/src/components/document-canvas/document-canvas.vue b/src/components/document-canvas/document-canvas.vue index 73f1c51..da433de 100644 --- a/src/components/document-canvas/document-canvas.vue +++ b/src/components/document-canvas/document-canvas.vue @@ -56,7 +56,7 @@ import ZoomableCanvas from "@/components/ui/zcanvas/zoomable-canvas"; import InteractionPane, { MODE_PAN, MODE_LAYER_SELECT } from "@/components/ui/zcanvas/interaction-pane"; import Scrollbars from "./scrollbars/scrollbars"; import ToolTypes, { MAX_ZOOM, calculateMaxScaling } from "@/definitions/tool-types"; -import { scaleToRatio, scaleValue } from "@/utils/image-math"; +import { scaleToRatio, scaleValue } from "@/math/image-math"; import { isMobile } from "@/utils/environment-util"; import { getCanvasInstance, setCanvasInstance, diff --git a/src/components/options-panel/tool-options-rotate/tool-options-rotate.vue b/src/components/options-panel/tool-options-rotate/tool-options-rotate.vue index 7df8194..96ce262 100644 --- a/src/components/options-panel/tool-options-rotate/tool-options-rotate.vue +++ b/src/components/options-panel/tool-options-rotate/tool-options-rotate.vue @@ -37,7 +37,7 @@ import { mapGetters, mapMutations } from "vuex"; import ToolTypes, { MIN_ZOOM, MAX_ZOOM } from "@/definitions/tool-types"; import Slider from "@/components/ui/slider/slider"; import messages from "./messages.json"; -import { degreesToRadians, radiansToDegrees } from "@/utils/image-math"; +import { degreesToRadians, radiansToDegrees } from "@/math/image-math"; export default { i18n: { messages }, diff --git a/src/components/ui/zcanvas/layer-sprite.js b/src/components/ui/zcanvas/layer-sprite.js index de4d328..a8a1eeb 100644 --- a/src/components/ui/zcanvas/layer-sprite.js +++ b/src/components/ui/zcanvas/layer-sprite.js @@ -26,9 +26,9 @@ import { createCanvas, resizeImage, globalToLocal } from "@/utils/canvas-util"; import { renderCross, renderMasked } from "@/utils/render-util"; import { LAYER_GRAPHIC, LAYER_MASK, LAYER_TEXT } from "@/definitions/layer-types"; import { - isPointInRange, translatePointerRotation, getRectangleForSelection, - rotatePoints, rectangleToCoordinates -} from "@/utils/image-math"; + isPointInRange, translatePointerRotation, rotatePoints, rectangleToCoordinates +} from "@/math/image-math"; +import { getRectangleForSelection, isSelectionClosed } from "@/math/selection-math"; import { renderEffectsForLayer } from "@/services/render-service"; import { flushLayerCache, clearCacheProperty } from "@/services/caches/bitmap-cache"; import { getSpriteForLayer } from "@/factories/sprite-factory"; @@ -186,15 +186,9 @@ class LayerSprite extends sprite { // drawable tools can work alongside an existing selection const selection = activeLayer.selection; - if ( selection?.length > 2 && canDrawOnSelection( activeLayer )) { - const firstPoint = selection[ 0 ]; - const lastPoint = selection[ selection.length - 1 ]; - if ( firstPoint.x === lastPoint.x && firstPoint.y === lastPoint.y ) { - this._hasSelection = true; - this._selectionClosed = true; - } else { - this._hasSelection = false; - } + if ( isSelectionClosed( selection ) && canDrawOnSelection( activeLayer )) { + this._hasSelection = true; + this._selectionClosed = true; } else { this._hasSelection = false; this.resetSelection(); @@ -465,7 +459,7 @@ class LayerSprite extends sprite { documentContext.restore(); } // render selection outline - if ( this._isSelectMode || this._hasSelection ) { + if (( this._isSelectMode || this._hasSelection ) && this.layer.selection ) { documentContext.save(); documentContext.beginPath(); documentContext.lineWidth = 2 / this.canvas.zoomFactor; diff --git a/src/components/ui/zcanvas/zoomable-canvas.js b/src/components/ui/zcanvas/zoomable-canvas.js index 54eaeec..f20616c 100644 --- a/src/components/ui/zcanvas/zoomable-canvas.js +++ b/src/components/ui/zcanvas/zoomable-canvas.js @@ -22,7 +22,7 @@ */ import Vue from "vue"; import { canvas } from "zcanvas"; -import { fastRound } from "@/utils/image-math"; +import { fastRound } from "@/math/image-math"; class ZoomableCanvas extends canvas { constructor( opts, store, rescaleFn ) { diff --git a/src/definitions/tool-types.js b/src/definitions/tool-types.js index 2d8c0cd..eb40767 100644 --- a/src/definitions/tool-types.js +++ b/src/definitions/tool-types.js @@ -20,7 +20,7 @@ * 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 { constrain, isPortrait } from "@/utils/image-math"; +import { constrain, isPortrait } from "@/math/image-math"; import { MAX_IMAGE_SIZE, MAX_MEGAPIXEL } from "@/definitions/image-types"; import { LAYER_GRAPHIC } from "@/definitions/layer-types"; diff --git a/src/utils/image-math.js b/src/math/image-math.js similarity index 93% rename from src/utils/image-math.js rename to src/math/image-math.js index 4599bb0..bff8d1a 100644 --- a/src/utils/image-math.js +++ b/src/math/image-math.js @@ -91,26 +91,6 @@ export const isPointInRange = ( point1x, point1y, point2x, point2y, margin = 5 ) return point1x >= left && point1x <= right && point1y >= top && point1y <= bottom; }; -export const getRectangleForSelection = selection => { - let minX = Infinity; - let minY = Infinity; - let maxX = 0; - let maxY = 0; - - selection.forEach(({ x, y }) => { - minX = Math.min( minX, x ); - maxX = Math.max( maxX, x ); - minY = Math.min( minY, y ); - maxY = Math.max( maxY, y ); - }); - return { - left : minX, - top : minY, - width : maxX - minX, - height : maxY - minY - }; -}; - export const translatePointerRotation = ( x, y, rotationCenterX, rotationCenterY, angleInRadians ) => { const x2 = x - rotationCenterX; const y2 = y - rotationCenterY; diff --git a/src/math/selection-math.js b/src/math/selection-math.js new file mode 100644 index 0000000..1081e8f --- /dev/null +++ b/src/math/selection-math.js @@ -0,0 +1,52 @@ +/** + * The MIT License (MIT) + * + * Igor Zinken 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 + * 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. + */ +export const getRectangleForSelection = selection => { + let minX = Infinity; + let minY = Infinity; + let maxX = 0; + let maxY = 0; + + selection.forEach(({ x, y }) => { + minX = Math.min( minX, x ); + maxX = Math.max( maxX, x ); + minY = Math.min( minY, y ); + maxY = Math.max( maxY, y ); + }); + return { + left : minX, + top : minY, + width : maxX - minX, + height : maxY - minY + }; +}; + +export const isSelectionClosed = selection => { + // smallest selection is four point polygon + if ( !selection || selection.length < 3 ) { + return false; + } + const firstPoint = selection[ 0 ]; + const lastPoint = selection[ selection.length - 1 ]; + + return firstPoint.x === lastPoint.x && firstPoint.y === lastPoint.y; +}; diff --git a/src/services/keyboard-service.js b/src/services/keyboard-service.js index 463886d..71abe00 100644 --- a/src/services/keyboard-service.js +++ b/src/services/keyboard-service.js @@ -26,7 +26,7 @@ import { CREATE_DOCUMENT, ADD_LAYER, EXPORT_DOCUMENT, DROPBOX_FILE_SELECTOR, SAVE_DROPBOX_DOCUMENT } from "@/definitions/modal-windows"; import { getCanvasInstance, getSpriteForLayer } from "@/factories/sprite-factory"; -import { translatePoints } from "@/utils/image-math"; +import { translatePoints } from "@/math/image-math"; let state, getters, commit, dispatch, listener, suspended = false, blockDefaults = true, optionDown = false, shiftDown = false; diff --git a/src/services/render-service.js b/src/services/render-service.js index 9de78d5..4d56d0f 100644 --- a/src/services/render-service.js +++ b/src/services/render-service.js @@ -29,7 +29,8 @@ import { hasFilters, isEqual as isFiltersEqual } from "@/factories/filters-facto import { isEqual as isTextEqual } from "@/factories/text-factory"; import { createCanvas, cloneCanvas, resizeToBase64 } from "@/utils/canvas-util"; import { replaceLayerSource } from "@/utils/layer-util"; -import { fastRound, getRotatedSize, getRotationCenter, getRectangleForSelection } from "@/utils/image-math"; +import { fastRound, getRotatedSize, getRotationCenter } from "@/math/image-math"; +import { getRectangleForSelection } from "@/math/selection-math"; import { hasLayerCache, getLayerCache, setLayerCache } from "@/services/caches/bitmap-cache"; import { loadGoogleFont } from "@/services/font-service"; import FilterWorker from "@/workers/filter.worker"; diff --git a/tests/unit/utils/image-math.spec.js b/tests/unit/math/image-math.spec.js similarity index 83% rename from tests/unit/utils/image-math.spec.js rename to tests/unit/math/image-math.spec.js index 48997e6..06d8ed2 100644 --- a/tests/unit/utils/image-math.spec.js +++ b/tests/unit/math/image-math.spec.js @@ -1,7 +1,6 @@ import { - fastRound, scaleToRatio, constrain, isPortrait, isLandscape, isSquare, - getRectangleForSelection, translatePoints, -} from "@/utils/image-math"; + fastRound, scaleToRatio, constrain, isPortrait, isLandscape, isSquare, translatePoints, +} from "@/math/image-math"; describe( "Image math utilities", () => { describe( "When rounding numbers", () => { @@ -60,23 +59,6 @@ describe( "Image math utilities", () => { }); }); - describe( "when given a polygon selection", () => { - it( "should be able to calculate the bounding box of the selection", () => { - const selection = [ - { x: 100, y: 150 }, - { x: 50, y: 899 }, - { x: 50, y: 100 }, - { x: 101, y: 100 } - ]; - expect( getRectangleForSelection( selection )).toEqual({ - left: 50, - top: 100, - width: 51, - height: 799 - }); - }); - }); - it( "should be able to translate the coordinates within a list", () => { const list = [ { x: 10, y: 10 }, { x: 15, y: 15 } diff --git a/tests/unit/math/selection-math.spec.js b/tests/unit/math/selection-math.spec.js new file mode 100644 index 0000000..351a21e --- /dev/null +++ b/tests/unit/math/selection-math.spec.js @@ -0,0 +1,49 @@ +import { getRectangleForSelection, isSelectionClosed } from "@/math/selection-math"; + +describe( "selection math", () => { + it( "should be able to calculate the bounding box of the selection", () => { + const selection = [ + { x: 100, y: 150 }, + { x: 50, y: 899 }, + { x: 50, y: 100 }, + { x: 101, y: 100 } + ]; + expect( getRectangleForSelection( selection )).toEqual({ + left: 50, + top: 100, + width: 51, + height: 799 + }); + }); + + describe( "when determining whether a selection is closed", () => { + it( "should not consider a selection with less than four points closable", () => { + const selection = [{ x: 100, y: 150 }]; + expect( isSelectionClosed( selection )).toBe( false ); + selection.push({ x: 150, y: 150 }); + expect( isSelectionClosed( selection )).toBe( false ); + selection.push({ x: 100, y: 200 }); + expect( isSelectionClosed( selection )).toBe( false ); + }); + + it( "should not consider a selection where the first and last point are not at the same coordinate closed", () => { + const selection = [ + { x: 100, y: 150 }, + { x: 150, y: 150 }, + { x: 100, y: 200 }, + { x: 100, y: 160 } + ]; + expect( isSelectionClosed( selection )).toBe( false ); + }); + + it( "should consider a selection where the first and last point are at the same coordinate closed", () => { + const selection = [ + { x: 100, y: 150 }, + { x: 150, y: 150 }, + { x: 100, y: 200 }, + { x: 100, y: 150 } + ]; + expect( isSelectionClosed( selection )).toBe( true ); + }); + }); +});