From f0edcd31b0bf59f31f5a24e39ae0d9f84ad5ae2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Thu, 17 Mar 2016 13:02:26 +0100 Subject: [PATCH] Fix #769: Implement Item#selection flags to separate selection from item and bounds. --- src/basic/Rectangle.js | 11 +--- src/constants.js | 1 + src/item/Item.js | 106 +++++++++++++++++++---------------- src/item/ItemSelection.js | 17 ++++++ src/item/Project.js | 44 +++++++-------- src/path/CompoundPath.js | 4 +- src/path/Path.js | 24 ++++---- src/path/Segment.js | 81 ++++++++++++++------------ src/path/SegmentPoint.js | 18 ++---- src/path/SegmentSelection.js | 10 ++-- 10 files changed, 170 insertions(+), 146 deletions(-) create mode 100644 src/item/ItemSelection.js diff --git a/src/basic/Rectangle.js b/src/basic/Rectangle.js index 6d03fbe0..e46be966 100644 --- a/src/basic/Rectangle.js +++ b/src/basic/Rectangle.js @@ -904,18 +904,13 @@ new function() { * @default false */ isSelected: function() { - return this._owner._boundsSelected; + return !!(this._owner._selection & /*#=*/ItemSelection.BOUNDS); }, setSelected: function(selected) { var owner = this._owner; - if (owner.setSelected) { - owner._boundsSelected = selected; - // Update the owner's selected state too, so the bounds - // actually get drawn. When deselecting, take a path's - // _segmentSelection into account too, since it will - // have to remain selected even when bounds are deselected - owner.setSelected(selected || owner._segmentSelection > 0); + if (owner.changeSelection) { + owner.changeSelection(/*#=*/ItemSelection.BOUNDS, selected); } } }) diff --git a/src/constants.js b/src/constants.js index 64f54c5a..b360edf0 100644 --- a/src/constants.js +++ b/src/constants.js @@ -12,4 +12,5 @@ /*#*/ include('util/Numerical.js'); /*#*/ include('item/ChangeFlag.js'); +/*#*/ include('item/ItemSelection.js'); /*#*/ include('path/SegmentSelection.js'); diff --git a/src/item/Item.js b/src/item/Item.js index 43d6d54e..f8a3a0d6 100644 --- a/src/item/Item.js +++ b/src/item/Item.js @@ -51,22 +51,30 @@ var Item = Base.extend(Emitter, /** @lends Item# */{ _applyMatrix: true, _canApplyMatrix: true, _canScaleStroke: false, + _pivot: null, + _visible: true, + _blendMode: 'normal', + _opacity: 1, + _locked: false, + _guide: false, + _clipMask: false, + _selection: 0, _boundsSelected: false, _selectChildren: false, // Provide information about fields to be serialized, with their defaults - // that can be ommited. + // that can be omitted. _serializeFields: { name: null, applyMatrix: null, matrix: new Matrix(), pivot: null, - locked: false, visible: true, blendMode: 'normal', opacity: 1, + locked: false, guide: false, - selected: false, clipMask: false, + selected: false, data: {} } }, @@ -425,7 +433,6 @@ new function() { // Injection scope for various item event handlers * @default false * @ignore */ - _locked: false, /** * Specifies whether the item is visible. When set to `false`, the item @@ -446,7 +453,6 @@ new function() { // Injection scope for various item event handlers * // Hide the path: * path.visible = false; */ - _visible: true, /** * The blend mode with which the item is composited onto the canvas. Both @@ -488,7 +494,6 @@ new function() { // Injection scope for various item event handlers * // Set the blend mode of circle2: * circle2.blendMode = 'multiply'; */ - _blendMode: 'normal', /** * The opacity of the item as a value between `0` and `1`. @@ -516,7 +521,6 @@ new function() { // Injection scope for various item event handlers * // Make circle2 50% transparent: * circle2.opacity = 0.5; */ - _opacity: 1, // TODO: Implement guides /** @@ -528,7 +532,26 @@ new function() { // Injection scope for various item event handlers * @default true * @ignore */ - _guide: false, + + getSelection: function() { + return this._selection; + }, + + setSelection: function(selection) { + if (selection !== this._selection) { + this._selection = selection; + var project = this._project; + if (project) { + project._updateSelection(this); + this._changed(/*#=*/Change.ATTRIBUTE); + } + } + }, + + changeSelection: function(flag, selected) { + var selection = this._selection; + this.setSelection(selected ? selection | flag : selection & ~flag); + }, /** * Specifies whether the item is selected. This will also return `true` for @@ -563,39 +586,29 @@ new function() { // Injection scope for various item event handlers if (children[i].isSelected()) return true; } - return this._selected; + return !!(this._selection & /*#=*/ItemSelection.ITEM); }, - setSelected: function(selected, noChildren) { - // Don't recursively call #setSelected() if it was called with - // noChildren set to true, see #setFullySelected(). - if (!noChildren && this._selectChildren) { + setSelected: function(selected) { + if (this._selectChildren) { var children = this._children; for (var i = 0, l = children.length; i < l; i++) children[i].setSelected(selected); } - if ((selected = !!selected) ^ this._selected) { - this._selected = selected; - var project = this._project; - if (project) { - project._updateSelection(this); - this._changed(/*#=*/Change.ATTRIBUTE); - } - } + this.changeSelection(/*#=*/ItemSelection.ITEM, selected); }, - _selected: false, - isFullySelected: function() { - var children = this._children; - if (children && this._selected) { + var children = this._children, + selected = !!(this._selection & /*#=*/ItemSelection.ITEM); + if (children && selected) { for (var i = 0, l = children.length; i < l; i++) if (!children[i].isFullySelected()) return false; return true; } // If there are no children, this is the same as #selected - return this._selected; + return selected; }, setFullySelected: function(selected) { @@ -604,8 +617,7 @@ new function() { // Injection scope for various item event handlers for (var i = 0, l = children.length; i < l; i++) children[i].setFullySelected(selected); } - // Pass true for hidden noChildren argument - this.setSelected(selected, true); + this.changeSelection(/*#=*/ItemSelection.ITEM, selected); }, /** @@ -636,8 +648,6 @@ new function() { // Injection scope for various item event handlers } }, - _clipMask: false, - // TODO: get/setIsolated (print specific feature) // TODO: get/setKnockout (print specific feature) // TODO: get/setAlphaIsShape @@ -778,9 +788,7 @@ new function() { // Injection scope for various item event handlers this._pivot = Point.read(arguments, 0, { clone: true, readNull: true }); // No need for _changed() since the only thing this affects is _position this._position = undefined; - }, - - _pivot: null, + } }, Base.each({ // Produce getters for bounds properties: getStrokeBounds: { stroke: true }, getHandleBounds: { handle: true }, @@ -1565,9 +1573,9 @@ new function() { // Injection scope for various item event handlers // in case #applyMatrix is true. this.setApplyMatrix(source._applyMatrix); this.setPivot(source._pivot); - // Copy over the selection state, use setSelected so the item - // is also added to Project#selectedItems if it is selected. - this.setSelected(source._selected); + // Copy over the selection state, use setSelection so the item + // is also added to Project#_selectionItems if it is selected. + this.setSelection(source._selection); // Copy over data and name as well. var data = source._data, name = source._name; @@ -1876,7 +1884,7 @@ new function() { // Injection scope for hit-test functions shared with project // See if we should check self (own content), by filtering for type, // guides and selected items if that's required. var checkSelf = !(options.guides && !this._guide - || options.selected && !this._selected + || options.selected && !this.isSelected(true) // Support legacy Item#type property to match hyphenated // class-names. || options.type && options.type !== Base.hyphenate(this._class) @@ -4240,23 +4248,27 @@ new function() { // Injection scope for hit-test functions shared with project return updated; }, - _drawSelection: function(ctx, matrix, size, selectedItems, updateVersion) { - if ((this._drawSelected || this._boundsSelected) - && this._isUpdated(updateVersion)) { + _drawSelection: function(ctx, matrix, size, selectionItems, updateVersion) { + var selection = this._selection, + itemSelected = selection & /*#=*/ItemSelection.ITEM, + boundsSelected = selection & /*#=*/ItemSelection.BOUNDS + || itemSelected && this._boundsSelected; + if (!this._drawSelected) + itemSelected = false; + if ((itemSelected || boundsSelected) && this._isUpdated(updateVersion)) { // Allow definition of selected color on a per item and per // layer level, with a fallback to #009dec var layer, - color = this.getSelectedColor(true) - || (layer = this.getLayer()) && layer.getSelectedColor(true), + color = this.getSelectedColor(true) || (layer = this.getLayer()) + && layer.getSelectedColor(true), mx = matrix.appended(this.getGlobalMatrix(true)); ctx.strokeStyle = ctx.fillStyle = color ? color.toCanvasStyle(ctx) : '#009dec'; - if (this._drawSelected) - this._drawSelected(ctx, mx, selectedItems); - if (this._boundsSelected) { + if (itemSelected) + this._drawSelected(ctx, mx, selectionItems); + if (boundsSelected) { var half = size / 2, - coords = mx._transformCorners( - this.getInternalBounds()); + coords = mx._transformCorners(this.getInternalBounds()); // Now draw a rectangle that connects the transformed // bounds corners, and draw the corners. ctx.beginPath(); diff --git a/src/item/ItemSelection.js b/src/item/ItemSelection.js new file mode 100644 index 00000000..7878e4f2 --- /dev/null +++ b/src/item/ItemSelection.js @@ -0,0 +1,17 @@ +/* + * Paper.js - The Swiss Army Knife of Vector Graphics Scripting. + * http://paperjs.org/ + * + * Copyright (c) 2011 - 2016, Juerg Lehni & Jonathan Puckey + * http://scratchdisk.com/ & http://jonathanpuckey.com/ + * + * Distributed under the MIT license. See LICENSE file for details. + * + * All rights reserved. + */ + +var ItemSelection = { + ITEM: 1, + BOUNDS: 2, + PIVOT: 4 +}; diff --git a/src/item/Project.js b/src/item/Project.js index b6da5574..79dd9b26 100644 --- a/src/item/Project.js +++ b/src/item/Project.js @@ -62,8 +62,8 @@ var Project = PaperScopeItem.extend(/** @lends Project# */{ // (e.g. PointText#_getBounds) this._view = View.create(this, element || CanvasProvider.getCanvas(1, 1)); - this._selectedItems = {}; - this._selectedItemCount = 0; + this._selectionItems = {}; + this._selectionCount = 0; // See Item#draw() for an explanation of _updateVersion this._updateVersion = 0; // Change tracking, not in use for now. Activate once required: @@ -282,15 +282,15 @@ var Project = PaperScopeItem.extend(/** @lends Project# */{ // TODO: Return groups if their children are all selected, and filter // out their children from the list. // TODO: The order of these items should be that of their drawing order. - var selectedItems = this._selectedItems, + var selectionItems = this._selectionItems, items = []; - for (var id in selectedItems) { - var item = selectedItems[id]; - if (item.isInserted()) { + for (var id in selectionItems) { + var item = selectionItems[id], + selection = item._selection; + if (selection & /*#=*/ItemSelection.ITEM && item.isInserted()) { items.push(item); - } else { - this._selectedItemCount--; - delete selectedItems[id]; + } else if (!selection) { + this._updateSelection(item); } } return items; @@ -299,15 +299,15 @@ var Project = PaperScopeItem.extend(/** @lends Project# */{ _updateSelection: function(item) { var id = item._id, - selectedItems = this._selectedItems; - if (item._selected) { - if (selectedItems[id] !== item) { - this._selectedItemCount++; - selectedItems[id] = item; + selectionItems = this._selectionItems; + if (item._selection) { + if (selectionItems[id] !== item) { + this._selectionCount++; + selectionItems[id] = item; } - } else if (selectedItems[id] === item) { - this._selectedItemCount--; - delete selectedItems[id]; + } else if (selectionItems[id] === item) { + this._selectionCount--; + delete selectionItems[id]; } }, @@ -324,9 +324,9 @@ var Project = PaperScopeItem.extend(/** @lends Project# */{ * Deselects all selected items in the project. */ deselectAll: function() { - var selectedItems = this._selectedItems; - for (var i in selectedItems) - selectedItems[i].setFullySelected(false); + var selectionItems = this._selectionItems; + for (var i in selectionItems) + selectionItems[i].setFullySelected(false); }, /** @@ -866,10 +866,10 @@ var Project = PaperScopeItem.extend(/** @lends Project# */{ ctx.restore(); // Draw the selection of the selected items in the project: - if (this._selectedItemCount > 0) { + if (this._selectionCount > 0) { ctx.save(); ctx.strokeWidth = 1; - var items = this._selectedItems, + var items = this._selectionItems, size = this._scope.settings.handleSize, version = this._updateVersion; for (var id in items) { diff --git a/src/path/CompoundPath.js b/src/path/CompoundPath.js index 9c0ccc1a..42a03e5c 100644 --- a/src/path/CompoundPath.js +++ b/src/path/CompoundPath.js @@ -293,14 +293,14 @@ var CompoundPath = PathItem.extend(/** @lends CompoundPath# */{ } }, - _drawSelected: function(ctx, matrix, selectedItems) { + _drawSelected: function(ctx, matrix, selectionItems) { var children = this._children; for (var i = 0, l = children.length; i < l; i++) { var child = children[i], mx = child._matrix; // Do not draw this child now if it's separately marked as selected, // as it would be drawn twice otherwise. - if (!selectedItems[child._id]) { + if (!selectionItems[child._id]) { child._drawSelected(ctx, mx.isIdentity() ? matrix : matrix.appended(mx)); } diff --git a/src/path/Path.js b/src/path/Path.js index e23ecae0..d3ec0df4 100644 --- a/src/path/Path.js +++ b/src/path/Path.js @@ -935,8 +935,8 @@ var Path = PathItem.extend(/** @lends Path# */{ */ isFullySelected: function() { var length = this._segments.length; - return this._selected && length > 0 && this._segmentSelection - === length * /*#=*/SegmentSelection.SEGMENT; + return this.isSelected(true) && length > 0 && this._segmentSelection + === length * /*#=*/SegmentSelection.ALL; }, setFullySelected: function(selected) { @@ -947,22 +947,20 @@ var Path = PathItem.extend(/** @lends Path# */{ this.setSelected(selected); }, - setSelected: function setSelected(selected) { + setSelection: function setSelection(selection) { // Deselect all segments when path is marked as not selected - if (!selected) + if (!(selection & /*#=*/ItemSelection.ITEM)) this._selectSegments(false); - // No need to pass true for noChildren since Path has none anyway. - setSelected.base.call(this, selected); + setSelection.base.call(this, selection); }, _selectSegments: function(selected) { - var length = this._segments.length; - this._segmentSelection = selected - ? length * /*#=*/SegmentSelection.SEGMENT : 0; - for (var i = 0; i < length; i++) { - this._segments[i]._selection = selected - ? /*#=*/SegmentSelection.SEGMENT : 0; - } + var segments = this._segments, + length = segments.length, + selection = selected ? /*#=*/SegmentSelection.ALL : 0; + this._segmentSelection = selection * length; + for (var i = 0; i < length; i++) + segments[i]._selection = selection; }, _updateSelection: function(segment, oldSelection, newSelection) { diff --git a/src/path/Segment.js b/src/path/Segment.js index 45a37a12..818ea011 100644 --- a/src/path/Segment.js +++ b/src/path/Segment.js @@ -115,7 +115,8 @@ var Segment = Base.extend(/** @lends Segment# */{ */ initialize: function Segment(arg0, arg1, arg2, arg3, arg4, arg5) { var count = arguments.length, - point, handleIn, handleOut; + point, handleIn, handleOut, + selection; // TODO: Use Point.read or Point.readNamed to read these? if (count === 0) { // Nothing @@ -125,17 +126,17 @@ var Segment = Base.extend(/** @lends Segment# */{ point = arg0.point; handleIn = arg0.handleIn; handleOut = arg0.handleOut; + selection = arg0.selection; } else { point = arg0; } - } else if (count === 2 && typeof arg0 === 'number') { - point = arguments; - } else if (count <= 3) { + } else if (typeof arg0 === 'object') { + // It doesn't matter if all of these arguments exist. + // new SegmentPoint() produces creates points with (0, 0) otherwise. point = arg0; - // Doesn't matter if these arguments exist, SegmentPointcreate - // produces creates points with (0, 0) otherwise handleIn = arg1; handleOut = arg2; + selection = arg3; } else { // Read points from the arguments list as a row of numbers point = arg0 !== undefined ? [ arg0, arg1 ] : null; handleIn = arg2 !== undefined ? [ arg2, arg3 ] : null; @@ -144,14 +145,20 @@ var Segment = Base.extend(/** @lends Segment# */{ new SegmentPoint(point, this, '_point'); new SegmentPoint(handleIn, this, '_handleIn'); new SegmentPoint(handleOut, this, '_handleOut'); + if (selection) + this.setSelection(selection); }, _serialize: function(options) { // If it is has no handles, only serialize point, otherwise handles too. - return Base.serialize(this.hasHandles() - ? [this._point, this._handleIn, this._handleOut] - : this._point, - options, true); + var point = this._point, + selection = this._selection, + obj = selection || this.hasHandles() + ? [point, this._handleIn, this._handleOut] + : point; + if (selection) + obj.push(selection); + return Base.serialize(obj, options, true); }, _changed: function(point) { @@ -253,8 +260,30 @@ var Segment = Base.extend(/** @lends Segment# */{ this._handleOut.set(0, 0); }, - _getSelectionFlag: function(point) { - return !point ? /*#=*/SegmentSelection.SEGMENT + getSelection: function() { + return this._selection; + }, + + setSelection: function(selection) { + var oldSelection = this._selection, + path = this._path; + // Set the selection state even if path is not defined yet, to allow + // selected segments to be inserted into paths and make JSON + // deserialization work. + this._selection = selection = selection || 0; + // If the selection state of the segment has changed, we need to let + // it's path know and possibly add or remove it from + // project._selectionItems + if (path && selection !== oldSelection) { + path._updateSelection(this, oldSelection, selection); + // Let path know that we changed something and the view should be + // redrawn + path._changed(/*#=*/Change.ATTRIBUTE); + } + }, + + _getSelection: function(point) { + return !point ? /*#=*/SegmentSelection.ALL : point === this._point ? /*#=*/SegmentSelection.POINT : point === this._handleIn ? /*#=*/SegmentSelection.HANDLE_IN : point === this._handleOut ? /*#=*/SegmentSelection.HANDLE_OUT @@ -277,33 +306,13 @@ var Segment = Base.extend(/** @lends Segment# */{ * path.segments[2].selected = true; */ isSelected: function(_point) { - return !!(this._selection & this._getSelectionFlag(_point)); + return !!(this._selection & this._getSelection(_point)); }, setSelected: function(selected, _point) { - var path = this._path, - selected = !!selected, // convert to boolean - selection = this._selection, - oldSelection = selection, - flag = this._getSelectionFlag(_point); - if (selected) { - selection |= flag; - } else { - selection &= ~flag; - } - // Set the selection state even if path is not defined yet, to allow - // selected segments to be inserted into paths and make JSON - // deserialization work. - this._selection = selection; - // If the selection state of the segment has changed, we need to let - // it's path know and possibly add or remove it from - // project._selectedItems - if (path && selection !== oldSelection) { - path._updateSelection(this, oldSelection, selection); - // Let path know that we changed something and the view should be - // redrawn - path._changed(/*#=*/Change.ATTRIBUTE); - } + var selection = this._selection, + flag = this._getSelection(_point); + this.setSelection(selected ? selection | flag : selection & ~flag); }, /** diff --git a/src/path/SegmentPoint.js b/src/path/SegmentPoint.js index 83945d2b..303483d7 100644 --- a/src/path/SegmentPoint.js +++ b/src/path/SegmentPoint.js @@ -18,13 +18,14 @@ */ var SegmentPoint = Point.extend({ initialize: function SegmentPoint(point, owner, key) { - var x, y, selected; + var x, y, + selected; if (!point) { x = y = 0; } else if ((x = point[0]) !== undefined) { // Array-like y = point[1]; } else { - // So we don't have to modify the point argument which causes + // So we don't have to modify the point argument which would cause // deoptimization: var pt = point; // If not Point-like already, read Point from arguments @@ -38,9 +39,9 @@ var SegmentPoint = Point.extend({ this._x = x; this._y = y; this._owner = owner; - // We have to set the owner's property that points to this point already - // now, so #setSelected(true) can work. owner[key] = this; + // We need to call #setSelected(true) after setting property on the + // owner that references this point. if (selected) this.setSelected(true); }, @@ -52,15 +53,6 @@ var SegmentPoint = Point.extend({ return this; }, - _serialize: function(options) { - var f = options.formatter, - x = f.number(this._x), - y = f.number(this._y); - return this.isSelected() - ? { x: x, y: y, selected: true } - : [x, y]; - }, - getX: function() { return this._x; }, diff --git a/src/path/SegmentSelection.js b/src/path/SegmentSelection.js index 101bda6f..559ef294 100644 --- a/src/path/SegmentSelection.js +++ b/src/path/SegmentSelection.js @@ -11,11 +11,11 @@ */ // Path#_segmentSelection is the addition of all segment's states, and is -// compared with SegmentSelection.SEGMENT, the combination of all +// compared with SegmentSelection.ALL, the combination of all // SegmentSelection values to see if all segments are fully selected. var SegmentSelection = { - HANDLE_IN: 1, - HANDLE_OUT: 2, - POINT: 4, - SEGMENT: 7 // HANDLE_IN | HANDLE_OUT | POINT + POINT: 1, + HANDLE_IN: 2, + HANDLE_OUT: 4, + ALL: 1 | 2 | 4 // POINT | HANDLE_IN | HANDLE_OUT };