diff --git a/src/containers/paper-canvas.css b/src/containers/paper-canvas.css index 16edbb7a..82e8e028 100644 --- a/src/containers/paper-canvas.css +++ b/src/containers/paper-canvas.css @@ -4,5 +4,7 @@ margin: auto; position: relative; background-color: #fff; + /* Turn off anti-aliasing for the drawing canvas. Each time it's updated it switches + back and forth from aliased to unaliased and that looks bad */ image-rendering: pixelated; } diff --git a/src/containers/selection-hov.jsx b/src/containers/selection-hoc.jsx similarity index 94% rename from src/containers/selection-hov.jsx rename to src/containers/selection-hoc.jsx index 81a4f7df..ca1179ab 100644 --- a/src/containers/selection-hov.jsx +++ b/src/containers/selection-hoc.jsx @@ -3,7 +3,7 @@ import React from 'react'; import {connect} from 'react-redux'; import paper from 'paper'; -const SelectionHOV = function (WrappedComponent) { +const SelectionHOC = function (WrappedComponent) { class SelectionComponent extends React.Component { componentDidMount () { if (this.props.hoveredItem) { @@ -43,4 +43,4 @@ const SelectionHOV = function (WrappedComponent) { )(SelectionComponent); }; -export default SelectionHOV; +export default SelectionHOC; diff --git a/src/helper/guides.js b/src/helper/guides.js index ffd65d76..c6f917b8 100644 --- a/src/helper/guides.js +++ b/src/helper/guides.js @@ -1,6 +1,6 @@ import paper from 'paper'; import {getGuideLayer} from './layer'; -import {removePaperItemsByTags, removePaperItemsByDataTags} from './helper'; +import {getAllPaperItems} from './selection'; const GUIDE_BLUE = '#009dec'; const GUIDE_GREY = '#aaaaaa'; @@ -65,12 +65,34 @@ const getGuideColor = function (colorName) { } }; +const _removePaperItemsByDataTags = function (tags) { + const allItems = getAllPaperItems(true); + for (const item of allItems) { + for (const tag of tags) { + if (item.data && item.data[tag]) { + item.remove(); + } + } + } +}; + +const _removePaperItemsByTags = function (tags) { + const allItems = getAllPaperItems(true); + for (const item of allItems) { + for (const tag of tags) { + if (item[tag]) { + item.remove(); + } + } + } +}; + const removeHelperItems = function () { - removePaperItemsByDataTags(['isHelperItem']); + _removePaperItemsByDataTags(['isHelperItem']); }; const removeAllGuides = function () { - removePaperItemsByTags(['guide']); + _removePaperItemsByTags(['guide']); }; export { diff --git a/src/helper/helper.js b/src/helper/helper.js deleted file mode 100644 index c7428106..00000000 --- a/src/helper/helper.js +++ /dev/null @@ -1,63 +0,0 @@ -import paper from 'paper'; - -/** - * @param {boolean} includeGuides True if guide layer items like the bounding box should - * be included in the returned items. - * @return {Array} all top-level (direct descendants of a paper.Layer) items - */ -const getAllPaperItems = function (includeGuides) { - includeGuides = includeGuides || false; - const allItems = []; - for (const layer of paper.project.layers) { - for (const child of layer.children) { - // don't give guides back - if (!includeGuides && child.guide) { - continue; - } - allItems.push(child); - } - } - return allItems; -}; - -const getPaperItemsByTags = function (tags) { - const allItems = getAllPaperItems(true); - const foundItems = []; - for (const item of allItems) { - for (const tag of tags) { - if (item[tag] && foundItems.indexOf(item) === -1) { - foundItems.push(item); - } - } - } - return foundItems; -}; - -const removePaperItemsByDataTags = function (tags) { - const allItems = getAllPaperItems(true); - for (const item of allItems) { - for (const tag of tags) { - if (item.data && item.data[tag]) { - item.remove(); - } - } - } -}; - -const removePaperItemsByTags = function (tags) { - const allItems = getAllPaperItems(true); - for (const item of allItems) { - for (const tag of tags) { - if (item[tag]) { - item.remove(); - } - } - } -}; - -export { - getAllPaperItems, - getPaperItemsByTags, - removePaperItemsByDataTags, - removePaperItemsByTags -}; diff --git a/src/helper/selection-tools/bounding-box-tool.js b/src/helper/selection-tools/bounding-box-tool.js index c77a5dcd..dca535fd 100644 --- a/src/helper/selection-tools/bounding-box-tool.js +++ b/src/helper/selection-tools/bounding-box-tool.js @@ -24,7 +24,7 @@ const Modes = keyMirror({ }); /** - * A paper.Tool that handles transforming the selection and drawing a bounding box with handles. + * Tool that handles transforming the selection and drawing a bounding box with handles. * On mouse down, the type of function (move, scale, rotate) is determined based on what is clicked * (scale handle, rotate handle, the object itself). This determines the mode of the tool, which then * delegates actions to the MoveTool, RotateTool or ScaleTool accordingly. diff --git a/src/helper/selection-tools/move-tool.js b/src/helper/selection-tools/move-tool.js index e6470a92..8853e631 100644 --- a/src/helper/selection-tools/move-tool.js +++ b/src/helper/selection-tools/move-tool.js @@ -3,6 +3,9 @@ import {isCompoundPathItem, getRootItem} from '../item'; import {snapDeltaToAngle} from '../math'; import {clearSelection, cloneSelection, getSelectedItems, setItemSelection} from '../selection'; +/** + * Tool to handle dragging an item to reposition it in a selection mode. + */ class MoveTool { /** * @param {!function} onUpdateSvg A callback to call when the image visibly changes diff --git a/src/helper/selection-tools/rotate-tool.js b/src/helper/selection-tools/rotate-tool.js index 4893ede1..2006cebf 100644 --- a/src/helper/selection-tools/rotate-tool.js +++ b/src/helper/selection-tools/rotate-tool.js @@ -1,5 +1,8 @@ import paper from 'paper'; +/** + * Tool to handle rotation when dragging the rotation handle in the bounding box tool. + */ class RotateTool { /** * @param {!function} onUpdateSvg A callback to call when the image visibly changes diff --git a/src/helper/selection-tools/scale-tool.js b/src/helper/selection-tools/scale-tool.js index 085395d8..8744ab72 100644 --- a/src/helper/selection-tools/scale-tool.js +++ b/src/helper/selection-tools/scale-tool.js @@ -1,5 +1,9 @@ import paper from 'paper'; +/** + * Tool to handle scaling items by pulling on the handles around the edges of the bounding + * box when in the bounding box tool. + */ class ScaleTool { /** * @param {!function} onUpdateSvg A callback to call when the image visibly changes @@ -34,9 +38,9 @@ class ScaleTool { this.boundsPath = boundsPath; this.boundsScaleHandles = boundsScaleHandles; this.boundsRotHandles = boundsRotHandles; - this.pivot = this.boundsPath.bounds[this.getOpposingRectCornerNameByIndex(index)].clone(); - this.origPivot = this.boundsPath.bounds[this.getOpposingRectCornerNameByIndex(index)].clone(); - this.corner = this.boundsPath.bounds[this.getRectCornerNameByIndex(index)].clone(); + this.pivot = this.boundsPath.bounds[this._getOpposingRectCornerNameByIndex(index)].clone(); + this.origPivot = this.boundsPath.bounds[this._getOpposingRectCornerNameByIndex(index)].clone(); + this.corner = this.boundsPath.bounds[this._getRectCornerNameByIndex(index)].clone(); this.origSize = this.corner.subtract(this.pivot); this.origCenter = this.boundsPath.bounds.center; for (const item of selectedItems) { @@ -105,14 +109,14 @@ class ScaleTool { for (let i = 0; i < this.boundsScaleHandles.length; i++) { const handle = this.boundsScaleHandles[i]; - handle.position = this.itemGroup.bounds[this.getRectCornerNameByIndex(i)]; + handle.position = this.itemGroup.bounds[this._getRectCornerNameByIndex(i)]; handle.bringToFront(); } for (let i = 0; i < this.boundsRotHandles.length; i++) { const handle = this.boundsRotHandles[i]; if (handle) { - handle.position = this.itemGroup.bounds[this.getRectCornerNameByIndex(i)] + handle.data.offset; + handle.position = this.itemGroup.bounds[this._getRectCornerNameByIndex(i)] + handle.data.offset; handle.bringToFront(); } } @@ -156,7 +160,7 @@ class ScaleTool { // @todo add back undo this.onUpdateSvg(); } - getRectCornerNameByIndex (index) { + _getRectCornerNameByIndex (index) { switch (index) { case 0: return 'bottomLeft'; @@ -176,7 +180,7 @@ class ScaleTool { return 'bottomCenter'; } } - getOpposingRectCornerNameByIndex (index) { + _getOpposingRectCornerNameByIndex (index) { switch (index) { case 0: return 'topRight'; diff --git a/src/helper/selection-tools/select-tool.js b/src/helper/selection-tools/select-tool.js index 2dbb015d..a7f0c775 100644 --- a/src/helper/selection-tools/select-tool.js +++ b/src/helper/selection-tools/select-tool.js @@ -6,10 +6,23 @@ import BoundingBoxTool from './bounding-box-tool'; import SelectionBoxTool from './selection-box-tool'; import paper from 'paper'; +/** + * paper.Tool that handles select mode. This is made up of 2 subtools. + * - The selection box tool is active when the user clicks an empty space and drags. + * It selects all items in the rectangle. + * - The bounding box tool is active if the user clicks on a non-empty space. It handles + * reshaping the item that was clicked. + */ class SelectTool extends paper.Tool { + /** The distance within which mouse events count as a hit against an item */ static get TOLERANCE () { return 6; } + /** + * @param {function} setHoveredItem Callback to set the hovered item + * @param {function} clearHoveredItem Callback to clear the hovered item + * @param {!function} onUpdateSvg A callback to call when the image visibly changes + */ constructor (setHoveredItem, clearHoveredItem, onUpdateSvg) { super(); this.setHoveredItem = setHoveredItem; @@ -18,13 +31,6 @@ class SelectTool extends paper.Tool { this.boundingBoxTool = new BoundingBoxTool(onUpdateSvg); this.selectionBoxTool = new SelectionBoxTool(Modes.SELECT); this.selectionBoxMode = false; - this._hitOptions = { - segments: true, - stroke: true, - curves: true, - fill: true, - guide: false - }; // We have to set these functions instead of just declaring them because // paper.js tools hook up the listeners in the setter functions. @@ -37,21 +43,42 @@ class SelectTool extends paper.Tool { selectRootItem(); this.boundingBoxTool.setSelectionBounds(); } + /** + * To be called when the hovered item changes. When the select tool hovers over a + * new item, it compares against this to see if a hover item change event needs to + * be fired. + * @param {paper.Item} prevHoveredItem The highlight that indicates the mouse is over + * a given item currently + */ setPrevHoveredItem (prevHoveredItem) { this.prevHoveredItem = prevHoveredItem; } + /** + * Returns the hit options to use when conducting hit tests. + * @param {boolean} preselectedOnly True if we should only return results that are already + * selected. + * @return {object} See paper.Item.hitTest for definition of options + */ getHitOptions (preselectedOnly) { - this._hitOptions.tolerance = SelectTool.TOLERANCE / paper.view.zoom; + // Tolerance needs to be scaled when the view is zoomed in in order to represent the same + // distance for the user to move the mouse. + const hitOptions = { + segments: true, + stroke: true, + curves: true, + fill: true, + guide: false, + tolerance: SelectTool.TOLERANCE / paper.view.zoom + }; if (preselectedOnly) { - this._hitOptions.selected = true; - } else { - delete this._hitOptions.selected; + hitOptions.selected = true; } - return this._hitOptions; + return hitOptions; } handleMouseDown (event) { if (event.event.button > 0) return; // only first mouse button + // If bounding box tool does not find an item that was hit, use selection box tool. this.clearHoveredItem(); if (!this.boundingBoxTool .onMouseDown( diff --git a/src/helper/selection-tools/selection-box-tool.js b/src/helper/selection-tools/selection-box-tool.js index f057200f..bc787c51 100644 --- a/src/helper/selection-tools/selection-box-tool.js +++ b/src/helper/selection-tools/selection-box-tool.js @@ -1,6 +1,7 @@ import {rectSelect} from '../guides'; import {clearSelection, processRectangularSelection} from '../selection'; +/** Tool to handle drag selection. A dotted line box appears and everything enclosed is selected. */ class SelectionBoxTool { constructor (mode) { this.selectionRect = null; diff --git a/src/helper/selection.js b/src/helper/selection.js index 458b5f41..607172e2 100644 --- a/src/helper/selection.js +++ b/src/helper/selection.js @@ -1,11 +1,30 @@ import paper from 'paper'; import Modes from '../modes/modes'; -import {getAllPaperItems} from './helper'; import {getItemsGroup, isGroup} from './group'; import {getRootItem, isCompoundPathItem, isBoundsItem, isPathItem, isPGTextItem} from './item'; import {getItemsCompoundPath, isCompoundPath, isCompoundPathChild} from './compound-path'; +/** + * @param {boolean} includeGuides True if guide layer items like the bounding box should + * be included in the returned items. + * @return {Array} all top-level (direct descendants of a paper.Layer) items + */ +const getAllPaperItems = function (includeGuides) { + includeGuides = includeGuides || false; + const allItems = []; + for (const layer of paper.project.layers) { + for (const child of layer.children) { + // don't give guides back + if (!includeGuides && child.guide) { + continue; + } + allItems.push(child); + } + } + return allItems; +}; + /** * @return {Array} all top-level (direct descendants of a paper.Layer) items * that aren't guide items or helper items. @@ -489,6 +508,7 @@ const shouldShowSelectAll = function () { }; export { + getAllPaperItems, selectAllItems, selectAllSegments, clearSelection, diff --git a/src/index.js b/src/index.js index 2c655e14..bbcbba68 100644 --- a/src/index.js +++ b/src/index.js @@ -1,5 +1,5 @@ import PaintEditor from './containers/paint-editor.jsx'; -import SelectionHOV from './containers/selection-hov.jsx'; +import SelectionHOV from './containers/selection-hoc.jsx'; import ScratchPaintReducer from './reducers/scratch-paint-reducer'; const Wrapped = SelectionHOV(PaintEditor);