From 406d26e8840a3a7a609145bc9c21655a3c3f8b4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Tue, 12 Jan 2016 00:54:04 +0100 Subject: [PATCH] Unify the way item parents and projects as parents of top-level layers are handled as owners. This automatically adds support for named children on project.layers, closing #491. --- src/item/Item.js | 117 +++++++++++++++++++++-------------------- src/item/Layer.js | 45 ++++++++-------- src/project/Project.js | 63 ++++++++++++---------- src/svg/SVGExport.js | 6 +-- 4 files changed, 121 insertions(+), 110 deletions(-) diff --git a/src/item/Item.js b/src/item/Item.js index 8a69e70b..b1a55a20 100644 --- a/src/item/Item.js +++ b/src/item/Item.js @@ -90,8 +90,8 @@ var Item = Base.extend(Emitter, /** @lends Item# */{ matrix = this._matrix = new Matrix(), // Allow setting another project than the currently active one. project = hasProps && props.project || paper.project; - if (!internal) - this._id = UID.get(); + this._id = internal ? null : UID.get(); + this._parent = this._index = null; // Inherit the applyMatrix setting from paper.settings.applyMatrix this._applyMatrix = this._canApplyMatrix && paper.settings.applyMatrix; // Handle matrix before everything else, to avoid issues with @@ -100,31 +100,37 @@ var Item = Base.extend(Emitter, /** @lends Item# */{ matrix.translate(point); matrix._owner = this; this._style = new Style(project._currentStyle, this, project); - // If _project is already set, the item was already moved into the scene - // graph. Used by Layer, where it's added to project.layers instead - if (!this._project) { - // Do not insert into DOM if it's an internal path, if props.insert - // is false, or if the props are setting a different parent anyway. - if (internal || hasProps && props.insert === false) { - this._setProject(project); - } else if (hasProps && props.parent) { - this.setParent(props.parent); - } else { - // Create a new layer if there is no active one. This will - // automatically make it the new activeLayer. - (project._activeLayer || new Layer()).addChild(this); - } + // Do not add to the project if it's an internal path, if props.insert + // is false, or if the props are setting a different parent anyway. + if (internal || hasProps && props.insert === false) { + this._setProject(project); + } else if (hasProps && props.parent) { + props.parent.addChild(this); + } else { + this._addToProject(project); } // Filter out Item.NO_INSERT before _set(), for performance reasons. - if (hasProps && props !== Item.NO_INSERT) + if (hasProps && props !== Item.NO_INSERT) { // Filter out insert, parent and project properties as these were // handled above. - this._set(props, { insert: true, project: true, parent: true }, - // Don't check for plain object as that's done by hasProps. - true); + this._set(props, + { internal: true, insert: true, project: true, parent: true }, + // Don't check for plain object, as that's handled by hasProps. + true); + } return hasProps; }, + /* + * Private helper used in the constructor function to add the created item + * to the project scene graph. Overridden in Layer. + */ + _addToProject: function(project) { + // Create a new layer if there is no active one. This will + // automatically make it the new activeLayer. + (project._activeLayer || new Layer()).addChild(this); + }, + _events: Base.each(['onMouseDown', 'onMouseUp', 'onMouseDrag', 'onClick', 'onDoubleClick', 'onMouseMove', 'onMouseEnter', 'onMouseLeave'], function(name) { @@ -323,10 +329,10 @@ var Item = Base.extend(Emitter, /** @lends Item# */{ if (name === (+name) + '') throw new Error( 'Names consisting only of numbers are not supported.'); - var parent = this._parent; - if (name && parent) { - var children = parent._children, - namedChildren = parent._namedChildren; + var owner = this._getOwner(); + if (name && owner) { + var children = owner._children, + namedChildren = owner._namedChildren; (namedChildren[name] = namedChildren[name] || []).push(this); // Only set this item if there isn't one under the same name already if (!(name in children)) @@ -1266,6 +1272,12 @@ var Item = Base.extend(Emitter, /** @lends Item# */{ return item.addChild(this); }, + /** + * Private helper to return the owner, either the parent, or the project + * for top-level layers. See Layer#_getOwner() + */ + _getOwner: '#getParent', + /** * The children items contained within this item. Items that define a * {@link #name} can also be accessed by name. @@ -1361,7 +1373,8 @@ var Item = Base.extend(Emitter, /** @lends Item# */{ * @bean */ getNextSibling: function() { - return this._parent && this._parent._children[this._index + 1] || null; + var owner = this._getOwner(); + return owner && owner._children[this._index + 1] || null; }, /** @@ -1371,7 +1384,8 @@ var Item = Base.extend(Emitter, /** @lends Item# */{ * @bean */ getPreviousSibling: function() { - return this._parent && this._parent._children[this._index - 1] || null; + var owner = this._getOwner(); + return owner && owner._children[this._index - 1] || null; }, /** @@ -1527,13 +1541,13 @@ var Item = Base.extend(Emitter, /** @lends Item# */{ * or duplicates it within the same project. When passed an item, * copies the item into the specified item. * - * @param {Project|Layer|Group|CompoundPath} item the item or project to + * @param {Project|Layer|Group|CompoundPath} owner the item or project to * copy the item to * @return {Item} the new copy of the item */ - copyTo: function(itemOrProject) { + copyTo: function(owner) { // Pass false fo insert, since we're inserting at a specific location. - return itemOrProject.addChild(this.clone(false)); + return owner.addChild(this.clone(false)); }, /** @@ -1950,7 +1964,7 @@ var Item = Base.extend(Emitter, /** @lends Item# */{ * @see #matches(match) */ getItems: function(match) { - return Item._getItems(this._children, match, this._matrix); + return Item._getItems(this, match, this._matrix); }, /** @@ -1969,15 +1983,13 @@ var Item = Base.extend(Emitter, /** @lends Item# */{ * @see #getItems(match) */ getItem: function(match) { - return Item._getItems(this._children, match, this._matrix, null, true) - [0] || null; + return Item._getItems(this, match, this._matrix, null, true)[0] || null; }, statics: { // NOTE: We pass children instead of item as first argument so the // method can be used for Project#layers as well in Project. - _getItems: function _getItems(children, match, matrix, param, - firstOnly) { + _getItems: function _getItems(item, match, matrix, param, firstOnly) { if (!param) { // Set up a couple of "side-car" values for the recursive calls // of _getItems below, mainly related to the handling of @@ -2007,7 +2019,8 @@ var Item = Base.extend(Emitter, /** @lends Item# */{ }); } } - var items = param.items, + var children = item._children, + items = param.items, rect = param.rect; matrix = rect && (matrix || new Matrix()); for (var i = 0, l = children && children.length; i < l; i++) { @@ -2035,9 +2048,7 @@ var Item = Base.extend(Emitter, /** @lends Item# */{ break; } if (param.recursive !== false) { - _getItems(child._children, match, - childMatrix, param, - firstOnly); + _getItems(child, match, childMatrix, param, firstOnly); } if (firstOnly && items.length > 0) break; @@ -2078,9 +2089,7 @@ var Item = Base.extend(Emitter, /** @lends Item# */{ // it as a child (this won't be successful on some classes, returning // null). var res = Base.importJSON(json, this); - return res !== this - ? this.addChild(res) - : res; + return res !== this ? this.addChild(res) : res; }, /** @@ -2273,20 +2282,16 @@ var Item = Base.extend(Emitter, /** @lends Item# */{ * Sends this item to the back of all other items within the same parent. */ sendToBack: function() { - // If there is no parent and the item is a layer, delegate to project - // instead. - return (this._parent || this instanceof Layer && this._project) - .insertChild(0, this); + var owner = this._getOwner(); + return owner ? owner.insertChild(0, this) : null; }, /** * Brings this item to the front of all other items within the same parent. */ bringToFront: function() { - // If there is no parent and the item is a layer, delegate to project - // instead. - return (this._parent || this instanceof Layer && this._project) - .addChild(this); + var owner = this._getOwner(); + return owner ? owner.addChild(this) : null; }, /** @@ -2360,10 +2365,10 @@ var Item = Base.extend(Emitter, /** @lends Item# */{ * Removes the item from its parent's named children list. */ _removeNamed: function() { - var parent = this._parent; - if (parent) { - var children = parent._children, - namedChildren = parent._namedChildren, + var owner = this._getOwner(); + if (owner) { + var children = owner._children, + namedChildren = owner._namedChildren, name = this._name, namedArray = namedChildren[name], index = namedArray ? namedArray.indexOf(this) : -1; @@ -2373,10 +2378,10 @@ var Item = Base.extend(Emitter, /** @lends Item# */{ delete children[name]; // Remove this entry namedArray.splice(index, 1); - // If there are any items left in the named array, set - // the last of them to be this.parent.children[this.name] + // If there are any items left in the named array, set the first + // of them to be children[this.name] if (namedArray.length) { - children[name] = namedArray[namedArray.length - 1]; + children[name] = namedArray[0]; } else { // Otherwise delete the empty array delete namedChildren[name]; diff --git a/src/item/Layer.js b/src/item/Layer.js index a17c1c98..aec2a4f1 100644 --- a/src/item/Layer.js +++ b/src/item/Layer.js @@ -62,19 +62,26 @@ var Layer = Group.extend(/** @lends Layer# */{ * position: view.center * }); */ - initialize: function Layer(arg) { - var props = Base.isPlainObject(arg) - ? new Base(arg) // clone so we can add insert = false - : { children: Array.isArray(arg) ? arg : arguments }, - insert = props.insert; - // Call the group constructor but don't insert yet! - props.insert = false; - Group.call(this, props); - if (insert === undefined || insert) { - this._project.addChild(this); - // When inserted, also activate the layer by default. - this.activate(); - } + initialize: function Layer() { + Group.apply(this, arguments); + }, + + /** + * Private helper used in the constructor function to add the newly created + * item to the project scene graph. + */ + _addToProject: function(project) { + project.addChild(this); + // When inserted, also activate the layer by default. + this.activate(); + }, + + /** + * Private helper to return the owner, either the parent, or the project + * for top-level layers. + */ + _getOwner: function() { + return this._parent || this._index != null && this._project; }, /** @@ -89,7 +96,7 @@ var Layer = Group.extend(/** @lends Layer# */{ if (project._activeLayer === this) project._activeLayer = this.getNextSibling() || this.getPreviousSibling(); - Base.splice(project.layers, null, this._index, 1); + Base.splice(project._children, null, this._index, 1); this._installEvents(false); // Notify self of the insertion change. We only need this // notification if we're tracking changes for now. @@ -107,16 +114,6 @@ var Layer = Group.extend(/** @lends Layer# */{ return false; }, - getNextSibling: function getNextSibling() { - return this._parent ? getNextSibling.base.call(this) - : this._project.layers[this._index + 1] || null; - }, - - getPreviousSibling: function getPreviousSibling() { - return this._parent ? getPreviousSibling.base.call(this) - : this._project.layers[this._index - 1] || null; - }, - isInserted: function isInserted() { return this._parent ? isInserted.base.call(this) : this._index != null; }, diff --git a/src/project/Project.js b/src/project/Project.js index 982acb4e..262ad840 100644 --- a/src/project/Project.js +++ b/src/project/Project.js @@ -52,7 +52,8 @@ var Project = PaperScopeItem.extend(/** @lends Project# */{ // so paper.project is set, as required by Layer and DoumentView // constructors. PaperScopeItem.call(this, true); - this.layers = []; + this._children = []; + this._namedChildren = {}; this._activeLayer = null; this.symbols = []; this._currentStyle = new Style(null, null, this); @@ -76,7 +77,7 @@ var Project = PaperScopeItem.extend(/** @lends Project# */{ // project serialization later, but deserialization of a layers array // will always work. // Pass true for compact, so 'Project' does not get added as the class - return Base.serialize(this.layers, options, true, dictionary); + return Base.serialize(this._children, options, true, dictionary); }, /** @@ -92,8 +93,9 @@ var Project = PaperScopeItem.extend(/** @lends Project# */{ * {@link Project#symbols}. */ clear: function() { - for (var i = this.layers.length - 1; i >= 0; i--) - this.layers[i].remove(); + var children = this._children; + for (var i = children.length - 1; i >= 0; i--) + children[i].remove(); this.symbols = []; }, @@ -103,7 +105,7 @@ var Project = PaperScopeItem.extend(/** @lends Project# */{ * @return Boolean */ isEmpty: function() { - return this.layers.length === 0; + return this._children.length === 0; }, /** @@ -189,9 +191,14 @@ var Project = PaperScopeItem.extend(/** @lends Project# */{ * * The layers contained within the project. * - * @name Project#layers + * @bean * @type Layer[] */ + getLayers: function() { + return this._children; + }, + + // TODO: Define #setLayers()? /** * The layer which is currently active. New items will be created on this @@ -237,7 +244,7 @@ var Project = PaperScopeItem.extend(/** @lends Project# */{ insertChild: function(index, item, _preserve) { if (item instanceof Layer) { item._remove(false, true); - Base.splice(this.layers, [item], index, 0); + Base.splice(this._children, [item], index, 0); item._setProject(this, true); // See Item#_remove() for an explanation of this: if (this._changes) @@ -283,9 +290,9 @@ var Project = PaperScopeItem.extend(/** @lends Project# */{ * Selects all items in the project. */ selectAll: function() { - var layers = this.layers; - for (var i = 0, l = layers.length; i < l; i++) - layers[i].setFullySelected(true); + var children = this._children; + for (var i = 0, l = children.length; i < l; i++) + children[i].setFullySelected(true); }, /** @@ -338,10 +345,11 @@ var Project = PaperScopeItem.extend(/** @lends Project# */{ // We don't need to do this here, but it speeds up things since we won't // repeatedly convert in Item#hitTest() then. var point = Point.read(arguments), - options = HitResult.getOptions(Base.read(arguments)); + options = HitResult.getOptions(Base.read(arguments)), + children = this._children; // Loop backwards, so layers that get drawn last are tested first - for (var i = this.layers.length - 1; i >= 0; i--) { - var res = this.layers[i]._hitTest(point, options); + for (var i = children.length - 1; i >= 0; i--) { + var res = children[i]._hitTest(point, options); if (res) return res; } return null; @@ -585,7 +593,7 @@ var Project = PaperScopeItem.extend(/** @lends Project# */{ * @return {Item[]} the list of matching items contained in the project */ getItems: function(match) { - return Item._getItems(this.layers, match); + return Item._getItems(this, match); }, /** @@ -602,7 +610,7 @@ var Project = PaperScopeItem.extend(/** @lends Project# */{ * @return {Item} the first item in the project matching the given criteria */ getItem: function(match) { - return Item._getItems(this.layers, match, null, null, true)[0] || null; + return Item._getItems(this, match, null, null, true)[0] || null; }, /** @@ -708,18 +716,19 @@ var Project = PaperScopeItem.extend(/** @lends Project# */{ ctx.save(); matrix.applyToContext(ctx); // Use new Base() so we can use param.extend() to easily override values - var param = new Base({ - offset: new Point(0, 0), - pixelRatio: pixelRatio, - viewMatrix: matrix.isIdentity() ? null : matrix, - matrices: [new Matrix()], // Start with the identity matrix. - // Tell the drawing routine that we want to keep _globalMatrix up to - // date. Item#rasterize() and Raster#getAverageColor() should not - // set this. - updateMatrix: true - }); - for (var i = 0, layers = this.layers, l = layers.length; i < l; i++) - layers[i].draw(ctx, param); + var children = this._children, + param = new Base({ + offset: new Point(0, 0), + pixelRatio: pixelRatio, + viewMatrix: matrix.isIdentity() ? null : matrix, + matrices: [new Matrix()], // Start with the identity matrix. + // Tell the drawing routine that we want to keep _globalMatrix + // up to date. Item#rasterize() and Raster#getAverageColor() + // should not set this. + updateMatrix: true + }); + for (var i = 0, l = children.length; i < l; i++) + children[i].draw(ctx, param); ctx.restore(); // Draw the selection of the selected items in the project: diff --git a/src/svg/SVGExport.js b/src/svg/SVGExport.js index da6f108e..7469407f 100644 --- a/src/svg/SVGExport.js +++ b/src/svg/SVGExport.js @@ -417,7 +417,7 @@ new function() { Project.inject({ exportSVG: function(options) { options = setOptions(options); - var layers = this.layers, + var children = this._children, view = this.getView(), size = view.getViewSize(), node = createElement('svg', { @@ -436,8 +436,8 @@ new function() { if (!matrix.isIdentity()) parent = node.appendChild( createElement('g', getTransform(matrix))); - for (var i = 0, l = layers.length; i < l; i++) - parent.appendChild(exportSVG(layers[i], options, true)); + for (var i = 0, l = children.length; i < l; i++) + parent.appendChild(exportSVG(children[i], options, true)); return exportDefinitions(node, options); } });