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.
This commit is contained in:
Jürg Lehni 2016-01-12 00:54:04 +01:00
parent 20f90bbee2
commit 406d26e884
4 changed files with 121 additions and 110 deletions

View file

@ -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];

View file

@ -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;
},

View file

@ -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:

View file

@ -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);
}
});