From 4fe7c0ab8b25d3cefbe505808df65f6b84999744 Mon Sep 17 00:00:00 2001 From: DD Liu Date: Thu, 6 Feb 2020 18:11:10 -0500 Subject: [PATCH] Revert "Revert "Crosshair"" This reverts commit 999c62ff5cb5311bc74e17297a00a9068f3d9144. --- src/containers/paper-canvas.jsx | 2 +- src/helper/icons/costume-anchor.svg | 16 ++++ .../icons/selection-anchor-expanded.svg | 12 +++ src/helper/layer.js | 75 ++++++++++++++----- .../selection-tools/bounding-box-tool.js | 46 ++++++++++-- src/helper/selection-tools/move-tool.js | 52 ++++++++++++- src/helper/selection-tools/scale-tool.js | 7 ++ src/helper/selection.js | 5 +- src/helper/undo.js | 2 +- src/helper/view.js | 16 +++- 10 files changed, 199 insertions(+), 34 deletions(-) create mode 100644 src/helper/icons/costume-anchor.svg create mode 100644 src/helper/icons/selection-anchor-expanded.svg diff --git a/src/containers/paper-canvas.jsx b/src/containers/paper-canvas.jsx index 41dff0e7..b57b4ac8 100644 --- a/src/containers/paper-canvas.jsx +++ b/src/containers/paper-canvas.jsx @@ -97,7 +97,7 @@ class PaperCanvas extends React.Component { for (const layer of paper.project.layers) { if (layer.data.isRasterLayer) { clearRaster(); - } else if (!layer.data.isBackgroundGuideLayer) { + } else if (!layer.data.isBackgroundGuideLayer && !layer.data.isDragCrosshairLayer) { layer.removeChildren(); } } diff --git a/src/helper/icons/costume-anchor.svg b/src/helper/icons/costume-anchor.svg new file mode 100644 index 00000000..a4103779 --- /dev/null +++ b/src/helper/icons/costume-anchor.svg @@ -0,0 +1,16 @@ + + + + Paint/Center Anchor/Active + Created with Sketch. + + + + + + + + + + + \ No newline at end of file diff --git a/src/helper/icons/selection-anchor-expanded.svg b/src/helper/icons/selection-anchor-expanded.svg new file mode 100644 index 00000000..2b1c85fd --- /dev/null +++ b/src/helper/icons/selection-anchor-expanded.svg @@ -0,0 +1,12 @@ + + + + Paint/Center Anchor/Artwork Center Expanded + Created with Sketch. + + + + + + + \ No newline at end of file diff --git a/src/helper/layer.js b/src/helper/layer.js index c811f0c3..8691aee3 100644 --- a/src/helper/layer.js +++ b/src/helper/layer.js @@ -1,6 +1,10 @@ import paper from '@scratch/paper'; import log from '../log/log'; import {ART_BOARD_WIDTH, ART_BOARD_HEIGHT} from './view'; +import {isGroupItem} from './item'; +import costumeAnchorIcon from './icons/costume-anchor.svg'; + +const CROSSHAIR_SIZE = 28; const _getLayer = function (layerString) { for (const layer of paper.project.layers) { @@ -50,6 +54,10 @@ const getRaster = function () { return _getLayer('isRasterLayer').children[0]; }; +const getDragCrosshairLayer = function () { + return _getLayer('isDragCrosshairLayer'); +}; + const getBackgroundGuideLayer = function () { return _getLayer('isBackgroundGuideLayer'); }; @@ -69,6 +77,16 @@ const getGuideLayer = function () { return layer; }; +const setGuideItem = function (item) { + item.locked = true; + item.guide = true; + if (isGroupItem(item)) { + for (let i = 0; i < item.children.length; i++) { + setGuideItem(item.children[i]); + } + } +}; + /** * Removes the guide layers, e.g. for purposes of exporting the image. Must call showGuideLayers to re-add them. * @param {boolean} includeRaster true if the raster layer should also be hidden @@ -76,7 +94,9 @@ const getGuideLayer = function () { */ const hideGuideLayers = function (includeRaster) { const backgroundGuideLayer = getBackgroundGuideLayer(); + const dragCrosshairLayer = getDragCrosshairLayer(); const guideLayer = getGuideLayer(); + dragCrosshairLayer.remove(); guideLayer.remove(); backgroundGuideLayer.remove(); let rasterLayer; @@ -85,6 +105,7 @@ const hideGuideLayers = function (includeRaster) { rasterLayer.remove(); } return { + dragCrosshairLayer: dragCrosshairLayer, guideLayer: guideLayer, backgroundGuideLayer: backgroundGuideLayer, rasterLayer: rasterLayer @@ -98,6 +119,7 @@ const hideGuideLayers = function (includeRaster) { */ const showGuideLayers = function (guideLayers) { const backgroundGuideLayer = guideLayers.backgroundGuideLayer; + const dragCrosshairLayer = guideLayers.dragCrosshairLayer; const guideLayer = guideLayers.guideLayer; const rasterLayer = guideLayers.rasterLayer; if (rasterLayer && !rasterLayer.index) { @@ -108,6 +130,10 @@ const showGuideLayers = function (guideLayers) { paper.project.addLayer(backgroundGuideLayer); backgroundGuideLayer.sendToBack(); } + if (!dragCrosshairLayer.index) { + paper.project.addLayer(dragCrosshairLayer); + dragCrosshairLayer.bringToFront(); + } if (!guideLayer.index) { paper.project.addLayer(guideLayer); guideLayer.bringToFront(); @@ -163,6 +189,29 @@ const _makeBackgroundPaper = function (width, height, color) { return vGroup; }; +// Helper function for drawing a crosshair +const _makeCrosshair = function (opacity, parent) { + paper.project.importSVG(costumeAnchorIcon, { + applyMatrix: false, + onLoad: function (item) { + item.position = new paper.Point(ART_BOARD_WIDTH / 2, ART_BOARD_HEIGHT / 2); + item.opacity = opacity; + item.parent = parent; + parent.dragCrosshair = item; + item.scale(CROSSHAIR_SIZE / item.bounds.width / paper.view.zoom); + setGuideItem(item); + } + }); +}; + +const _makeDragCrosshairLayer = function () { + const dragCrosshairLayer = new paper.Layer(); + _makeCrosshair(1, dragCrosshairLayer); + dragCrosshairLayer.data.isDragCrosshairLayer = true; + dragCrosshairLayer.visible = false; + return dragCrosshairLayer; +}; + const _makeBackgroundGuideLayer = function () { const guideLayer = new paper.Layer(); guideLayer.locked = true; @@ -173,26 +222,7 @@ const _makeBackgroundGuideLayer = function () { vBackground.guide = true; vBackground.locked = true; - const vLine = new paper.Path.Line(new paper.Point(0, -7), new paper.Point(0, 7)); - vLine.strokeWidth = 2; - vLine.strokeColor = '#ccc'; - vLine.position = new paper.Point(ART_BOARD_WIDTH / 2, ART_BOARD_HEIGHT / 2); - vLine.guide = true; - vLine.locked = true; - - const hLine = new paper.Path.Line(new paper.Point(-7, 0), new paper.Point(7, 0)); - hLine.strokeWidth = 2; - hLine.strokeColor = '#ccc'; - hLine.position = new paper.Point(ART_BOARD_WIDTH / 2, ART_BOARD_HEIGHT / 2); - hLine.guide = true; - hLine.locked = true; - - const circle = new paper.Shape.Circle(new paper.Point(0, 0), 5); - circle.strokeWidth = 2; - circle.strokeColor = '#ccc'; - circle.position = new paper.Point(ART_BOARD_WIDTH / 2, ART_BOARD_HEIGHT / 2); - circle.guide = true; - circle.locked = true; + _makeCrosshair(0.25, guideLayer); guideLayer.data.isBackgroundGuideLayer = true; return guideLayer; @@ -202,19 +232,24 @@ const setupLayers = function () { const backgroundGuideLayer = _makeBackgroundGuideLayer(); _makeRasterLayer(); const paintLayer = _makePaintingLayer(); + const dragCrosshairLayer = _makeDragCrosshairLayer(); const guideLayer = _makeGuideLayer(); backgroundGuideLayer.sendToBack(); + dragCrosshairLayer.bringToFront(); guideLayer.bringToFront(); paintLayer.activate(); }; export { + CROSSHAIR_SIZE, createCanvas, hideGuideLayers, showGuideLayers, + getDragCrosshairLayer, getGuideLayer, getBackgroundGuideLayer, clearRaster, getRaster, + setGuideItem, setupLayers }; diff --git a/src/helper/selection-tools/bounding-box-tool.js b/src/helper/selection-tools/bounding-box-tool.js index af791dbd..46b49a1b 100644 --- a/src/helper/selection-tools/bounding-box-tool.js +++ b/src/helper/selection-tools/bounding-box-tool.js @@ -3,13 +3,15 @@ import keyMirror from 'keymirror'; import {getSelectedRootItems} from '../selection'; import {getGuideColor, removeBoundsPath, removeBoundsHandles} from '../guides'; -import {getGuideLayer} from '../layer'; +import {getGuideLayer, setGuideItem} from '../layer'; +import selectionAnchorIcon from '../icons/selection-anchor-expanded.svg'; import Cursors from '../../lib/cursors'; import ScaleTool from './scale-tool'; import RotateTool from './rotate-tool'; import MoveTool from './move-tool'; +const SELECTION_ANCHOR_SIZE = 20; /** SVG for the rotation icon on the bounding box */ const ARROW_PATH = 'M19.28,1.09C19.28.28,19,0,18.2,0c-1.67,0-3.34,0-5,0-.34,0-.88.24-1,.47a1.4,1.4,' + '0,0,0,.36,1.08,15.27,15.27,0,0,0,1.46,1.36A6.4,6.4,0,0,1,6.52,4,5.85,5.85,0,0,1,5.24,3,15.27,15.27,' + @@ -23,6 +25,7 @@ const BoundingBoxModes = keyMirror({ ROTATE: null, MOVE: null }); +let anchorIcon; /** * Tool that handles transforming the selection and drawing a bounding box with handles. @@ -77,6 +80,17 @@ class BoundingBoxTool { * @return {boolean} True if there was a hit, false otherwise */ onMouseDown (event, clone, multiselect, doubleClicked, hitOptions) { + if (!anchorIcon) { + paper.project.importSVG(selectionAnchorIcon, { + onLoad: function (item) { + anchorIcon = item; + item.visible = false; + item.parent = getGuideLayer(); + setGuideItem(item); + } + }); + } + if (event.event.button > 0) return; // only first mouse button const {hitResult, mode} = this._determineMode(event, multiselect, hitOptions); if (!hitResult) { @@ -204,11 +218,17 @@ class BoundingBoxTool { } if (!this.boundsPath) { - this.boundsPath = new paper.Path.Rectangle(rect); - this.boundsPath.curves[0].divideAtTime(0.5); - this.boundsPath.curves[2].divideAtTime(0.5); - this.boundsPath.curves[4].divideAtTime(0.5); - this.boundsPath.curves[6].divideAtTime(0.5); + this.boundsPath = new paper.Group(); + this.boundsRect = paper.Path.Rectangle(rect); + this.boundsRect.curves[0].divideAtTime(0.5); + this.boundsRect.curves[2].divideAtTime(0.5); + this.boundsRect.curves[4].divideAtTime(0.5); + this.boundsRect.curves[6].divideAtTime(0.5); + this.boundsPath.addChild(this.boundsRect); + if (anchorIcon) { + this.boundsPath.addChild(anchorIcon); + this.boundsPath.selectionAnchor = anchorIcon; + } this._modeMap[BoundingBoxModes.MOVE].setBoundsPath(this.boundsPath); } this.boundsPath.guide = true; @@ -219,6 +239,12 @@ class BoundingBoxTool { this.boundsPath.strokeWidth = 1 / paper.view.zoom; this.boundsPath.strokeColor = getGuideColor(); + if (anchorIcon) { + anchorIcon.visible = true; + anchorIcon.scale(SELECTION_ANCHOR_SIZE / paper.view.zoom / anchorIcon.bounds.width); + anchorIcon.position = rect.center; + } + // Make a template to copy const boundsScaleCircleShadow = new paper.Path.Circle({ @@ -247,8 +273,8 @@ class BoundingBoxTool { const boundsScaleHandle = new paper.Group([boundsScaleCircleShadow, boundsScaleCircle]); boundsScaleHandle.parent = getGuideLayer(); - for (let index = 0; index < this.boundsPath.segments.length; index++) { - const segment = this.boundsPath.segments[index]; + for (let index = 0; index < this.boundsRect.segments.length; index++) { + const segment = this.boundsRect.segments[index]; if (index === 7) { const offset = new paper.Point(0, 20); @@ -295,8 +321,12 @@ class BoundingBoxTool { removeBoundsPath () { removeBoundsPath(); this.boundsPath = null; + this.boundsRect = null; this.boundsScaleHandles.length = 0; this.boundsRotHandles.length = 0; + if (anchorIcon) { + anchorIcon.visible = false; + } } removeBoundsHandles () { removeBoundsHandles(); diff --git a/src/helper/selection-tools/move-tool.js b/src/helper/selection-tools/move-tool.js index efd861d3..19a9b35d 100644 --- a/src/helper/selection-tools/move-tool.js +++ b/src/helper/selection-tools/move-tool.js @@ -2,10 +2,14 @@ import paper from '@scratch/paper'; import Modes from '../../lib/modes'; import {isGroup} from '../group'; import {isCompoundPathItem, getRootItem} from '../item'; -import {snapDeltaToAngle} from '../math'; +import {checkPointsClose, snapDeltaToAngle} from '../math'; import {ART_BOARD_WIDTH, ART_BOARD_HEIGHT} from '../view'; import {clearSelection, cloneSelection, getSelectedLeafItems, getSelectedRootItems, setItemSelection} from '../selection'; +import {getDragCrosshairLayer} from '../layer'; + +/** Snap to align selection center to rotation center within this distance */ +const SNAPPING_THRESHOLD = 4; /** * Tool to handle dragging an item to reposition it in a selection mode. @@ -23,6 +27,7 @@ class MoveTool { this.setSelectedItems = setSelectedItems; this.clearSelectedItems = clearSelectedItems; this.selectedItems = null; + this.selectionCenter = null; this.onUpdateImage = onUpdateImage; this.switchToTextTool = switchToTextTool; this.boundsPath = null; @@ -66,10 +71,27 @@ class MoveTool { this._select(item, true, hitProperties.subselect); } if (hitProperties.clone) cloneSelection(hitProperties.subselect, this.onUpdateImage); + this.selectedItems = this.mode === Modes.RESHAPE ? getSelectedLeafItems() : getSelectedRootItems(); + if (this.selectedItems.length === 0) { + return; + } + + let selectionBounds; + for (const selectedItem of this.selectedItems) { + if (selectionBounds) { + selectionBounds = selectionBounds.unite(selectedItem.bounds); + } else { + selectionBounds = selectedItem.bounds; + } + } + this.selectionCenter = selectionBounds.center; + if (this.boundsPath) { this.selectedItems.push(this.boundsPath); } + + } setBoundsPath (boundsPath) { this.boundsPath = boundsPath; @@ -101,7 +123,21 @@ class MoveTool { const point = event.point; point.x = Math.max(0, Math.min(point.x, ART_BOARD_WIDTH)); point.y = Math.max(0, Math.min(point.y, ART_BOARD_HEIGHT)); + const dragVector = point.subtract(event.downPoint); + let snapVector; + + // Snapping to align center. Not in reshape mode, because reshape doesn't show center crosshair + const center = new paper.Point(ART_BOARD_WIDTH / 2, ART_BOARD_HEIGHT / 2); + if (!event.modifiers.shift && this.mode !== Modes.RESHAPE) { + if (checkPointsClose( + this.selectionCenter.add(dragVector), + center, + SNAPPING_THRESHOLD / paper.view.zoom /* threshold */)) { + + snapVector = center.subtract(this.selectionCenter); + } + } for (const item of this.selectedItems) { // add the position of the item before the drag started @@ -110,12 +146,20 @@ class MoveTool { item.data.origPos = item.position; } - if (event.modifiers.shift) { + if (snapVector) { + item.position = item.data.origPos.add(snapVector); + } else if (event.modifiers.shift) { item.position = item.data.origPos.add(snapDeltaToAngle(dragVector, Math.PI / 4)); } else { item.position = item.data.origPos.add(dragVector); } } + + + // Show the center crosshair above the selected item while dragging. This makes it easier to center sprites. + // Yes, we're calling it once per drag event, but it's better than having the crosshair pop up + // for a split second every time you click a sprite. + getDragCrosshairLayer().visible = true; } onMouseUp () { let moved = false; @@ -127,10 +171,14 @@ class MoveTool { item.data.origPos = null; } this.selectedItems = null; + this.selectionCenter = null; if (moved) { this.onUpdateImage(); } + + // Hide the crosshair we showed earlier. + getDragCrosshairLayer().visible = false; } } diff --git a/src/helper/selection-tools/scale-tool.js b/src/helper/selection-tools/scale-tool.js index 9c754fd3..c9f9daac 100644 --- a/src/helper/selection-tools/scale-tool.js +++ b/src/helper/selection-tools/scale-tool.js @@ -38,6 +38,7 @@ class ScaleTool { this.pivot = boundsPath.bounds[this._getOpposingRectCornerNameByIndex(index)].clone(); this.origPivot = boundsPath.bounds[this._getOpposingRectCornerNameByIndex(index)].clone(); this.corner = boundsPath.bounds[this._getRectCornerNameByIndex(index)].clone(); + this.selectionAnchor = boundsPath.selectionAnchor; this.origSize = this.corner.subtract(this.pivot); this.origCenter = boundsPath.bounds.center; this.isCorner = this._isCorner(index); @@ -86,6 +87,9 @@ class ScaleTool { // Reset position if we were just in alt this.centered = false; this.itemGroup.scale(1 / this.lastSx, 1 / this.lastSy, this.pivot); + if (this.selectionAnchor) { + this.selectionAnchor.scale(this.lastSx, this.lastSy); + } this.lastSx = 1; this.lastSy = 1; } @@ -114,6 +118,9 @@ class ScaleTool { sy *= signy; } this.itemGroup.scale(sx / this.lastSx, sy / this.lastSy, this.pivot); + if (this.selectionAnchor) { + this.selectionAnchor.scale(this.lastSx / sx, this.lastSy / sy); + } this.lastSx = sx; this.lastSy = sy; } diff --git a/src/helper/selection.js b/src/helper/selection.js index 20536141..6b34954b 100644 --- a/src/helper/selection.js +++ b/src/helper/selection.js @@ -13,7 +13,10 @@ import {sortItemsByZIndex} from './math'; */ const getItems = function (options) { const newMatcher = function (item) { - return !(item instanceof paper.Layer) && !item.locked && + return !(item instanceof paper.Layer) && + item.layer.data && item.layer.data.isPaintingLayer && + !item.locked && + !item.isClipMask() && !(item.data && item.data.isHelperItem) && (!options.match || options.match(item)); }; diff --git a/src/helper/undo.js b/src/helper/undo.js index 8be90921..ed36e9e9 100644 --- a/src/helper/undo.js +++ b/src/helper/undo.js @@ -27,7 +27,7 @@ const performSnapshot = function (dispatchPerformSnapshot, format) { const _restore = function (entry, setSelectedItems, onUpdateImage, isBitmapMode) { for (let i = paper.project.layers.length - 1; i >= 0; i--) { const layer = paper.project.layers[i]; - if (!layer.data.isBackgroundGuideLayer) { + if (!layer.data.isBackgroundGuideLayer && !layer.data.isDragCrosshairLayer) { layer.removeChildren(); layer.remove(); } diff --git a/src/helper/view.js b/src/helper/view.js index aa1fa0e4..0e480c3f 100644 --- a/src/helper/view.js +++ b/src/helper/view.js @@ -1,6 +1,6 @@ import paper from '@scratch/paper'; import {getSelectedRootItems} from './selection'; -import {getRaster} from './layer'; +import {CROSSHAIR_SIZE, getBackgroundGuideLayer, getDragCrosshairLayer, getRaster} from './layer'; import {getHitBounds} from './bitmap'; // Vectors are imported and exported at SVG_ART_BOARD size. @@ -30,6 +30,17 @@ const clampViewBounds = () => { } }; +const _resizeCrosshair = () => { + if (getDragCrosshairLayer() && getDragCrosshairLayer().dragCrosshair) { + getDragCrosshairLayer().dragCrosshair.scale( + CROSSHAIR_SIZE / getDragCrosshairLayer().dragCrosshair.bounds.width / paper.view.zoom); + } + if (getBackgroundGuideLayer() && getBackgroundGuideLayer().dragCrosshair) { + getBackgroundGuideLayer().dragCrosshair.scale( + CROSSHAIR_SIZE / getBackgroundGuideLayer().dragCrosshair.bounds.width / paper.view.zoom); + } +}; + // Zoom keeping a project-space point fixed. // This article was helpful http://matthiasberth.com/tech/stable-zoom-and-pan-in-paperjs const zoomOnFixedPoint = (deltaZoom, fixedPoint) => { @@ -43,6 +54,7 @@ const zoomOnFixedPoint = (deltaZoom, fixedPoint) => { view.zoom = newZoom; view.translate(postZoomOffset.multiply(-1)); clampViewBounds(); + _resizeCrosshair(); }; // Zoom keeping the selection center (if any) fixed. @@ -67,6 +79,7 @@ const zoomOnSelection = deltaZoom => { const resetZoom = () => { paper.project.view.zoom = .5; + _resizeCrosshair(); clampViewBounds(); }; @@ -92,6 +105,7 @@ const zoomToFit = isBitmap => { if (ratio < 1) { paper.view.center = bounds.center; paper.view.zoom = paper.view.zoom / ratio; + _resizeCrosshair(); clampViewBounds(); } }