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(), matrix = this._matrix = new Matrix(),
// Allow setting another project than the currently active one. // Allow setting another project than the currently active one.
project = hasProps && props.project || paper.project; project = hasProps && props.project || paper.project;
if (!internal) this._id = internal ? null : UID.get();
this._id = UID.get(); this._parent = this._index = null;
// Inherit the applyMatrix setting from paper.settings.applyMatrix // Inherit the applyMatrix setting from paper.settings.applyMatrix
this._applyMatrix = this._canApplyMatrix && paper.settings.applyMatrix; this._applyMatrix = this._canApplyMatrix && paper.settings.applyMatrix;
// Handle matrix before everything else, to avoid issues with // Handle matrix before everything else, to avoid issues with
@ -100,31 +100,37 @@ var Item = Base.extend(Emitter, /** @lends Item# */{
matrix.translate(point); matrix.translate(point);
matrix._owner = this; matrix._owner = this;
this._style = new Style(project._currentStyle, this, project); this._style = new Style(project._currentStyle, this, project);
// If _project is already set, the item was already moved into the scene // Do not add to the project if it's an internal path, if props.insert
// graph. Used by Layer, where it's added to project.layers instead // is false, or if the props are setting a different parent anyway.
if (!this._project) { if (internal || hasProps && props.insert === false) {
// Do not insert into DOM if it's an internal path, if props.insert this._setProject(project);
// is false, or if the props are setting a different parent anyway. } else if (hasProps && props.parent) {
if (internal || hasProps && props.insert === false) { props.parent.addChild(this);
this._setProject(project); } else {
} else if (hasProps && props.parent) { this._addToProject(project);
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);
}
} }
// Filter out Item.NO_INSERT before _set(), for performance reasons. // 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 // Filter out insert, parent and project properties as these were
// handled above. // handled above.
this._set(props, { insert: true, project: true, parent: true }, this._set(props,
// Don't check for plain object as that's done by hasProps. { internal: true, insert: true, project: true, parent: true },
true); // Don't check for plain object, as that's handled by hasProps.
true);
}
return hasProps; 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', _events: Base.each(['onMouseDown', 'onMouseUp', 'onMouseDrag', 'onClick',
'onDoubleClick', 'onMouseMove', 'onMouseEnter', 'onMouseLeave'], 'onDoubleClick', 'onMouseMove', 'onMouseEnter', 'onMouseLeave'],
function(name) { function(name) {
@ -323,10 +329,10 @@ var Item = Base.extend(Emitter, /** @lends Item# */{
if (name === (+name) + '') if (name === (+name) + '')
throw new Error( throw new Error(
'Names consisting only of numbers are not supported.'); 'Names consisting only of numbers are not supported.');
var parent = this._parent; var owner = this._getOwner();
if (name && parent) { if (name && owner) {
var children = parent._children, var children = owner._children,
namedChildren = parent._namedChildren; namedChildren = owner._namedChildren;
(namedChildren[name] = namedChildren[name] || []).push(this); (namedChildren[name] = namedChildren[name] || []).push(this);
// Only set this item if there isn't one under the same name already // Only set this item if there isn't one under the same name already
if (!(name in children)) if (!(name in children))
@ -1266,6 +1272,12 @@ var Item = Base.extend(Emitter, /** @lends Item# */{
return item.addChild(this); 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 * The children items contained within this item. Items that define a
* {@link #name} can also be accessed by name. * {@link #name} can also be accessed by name.
@ -1361,7 +1373,8 @@ var Item = Base.extend(Emitter, /** @lends Item# */{
* @bean * @bean
*/ */
getNextSibling: function() { 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 * @bean
*/ */
getPreviousSibling: function() { 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, * or duplicates it within the same project. When passed an item,
* copies the item into the specified 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 * copy the item to
* @return {Item} the new copy of the item * @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. // 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) * @see #matches(match)
*/ */
getItems: function(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) * @see #getItems(match)
*/ */
getItem: function(match) { getItem: function(match) {
return Item._getItems(this._children, match, this._matrix, null, true) return Item._getItems(this, match, this._matrix, null, true)[0] || null;
[0] || null;
}, },
statics: { statics: {
// NOTE: We pass children instead of item as first argument so the // NOTE: We pass children instead of item as first argument so the
// method can be used for Project#layers as well in Project. // method can be used for Project#layers as well in Project.
_getItems: function _getItems(children, match, matrix, param, _getItems: function _getItems(item, match, matrix, param, firstOnly) {
firstOnly) {
if (!param) { if (!param) {
// Set up a couple of "side-car" values for the recursive calls // Set up a couple of "side-car" values for the recursive calls
// of _getItems below, mainly related to the handling of // 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; rect = param.rect;
matrix = rect && (matrix || new Matrix()); matrix = rect && (matrix || new Matrix());
for (var i = 0, l = children && children.length; i < l; i++) { for (var i = 0, l = children && children.length; i < l; i++) {
@ -2035,9 +2048,7 @@ var Item = Base.extend(Emitter, /** @lends Item# */{
break; break;
} }
if (param.recursive !== false) { if (param.recursive !== false) {
_getItems(child._children, match, _getItems(child, match, childMatrix, param, firstOnly);
childMatrix, param,
firstOnly);
} }
if (firstOnly && items.length > 0) if (firstOnly && items.length > 0)
break; 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 // it as a child (this won't be successful on some classes, returning
// null). // null).
var res = Base.importJSON(json, this); var res = Base.importJSON(json, this);
return res !== this return res !== this ? this.addChild(res) : res;
? 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. * Sends this item to the back of all other items within the same parent.
*/ */
sendToBack: function() { sendToBack: function() {
// If there is no parent and the item is a layer, delegate to project var owner = this._getOwner();
// instead. return owner ? owner.insertChild(0, this) : null;
return (this._parent || this instanceof Layer && this._project)
.insertChild(0, this);
}, },
/** /**
* Brings this item to the front of all other items within the same parent. * Brings this item to the front of all other items within the same parent.
*/ */
bringToFront: function() { bringToFront: function() {
// If there is no parent and the item is a layer, delegate to project var owner = this._getOwner();
// instead. return owner ? owner.addChild(this) : null;
return (this._parent || this instanceof Layer && this._project)
.addChild(this);
}, },
/** /**
@ -2360,10 +2365,10 @@ var Item = Base.extend(Emitter, /** @lends Item# */{
* Removes the item from its parent's named children list. * Removes the item from its parent's named children list.
*/ */
_removeNamed: function() { _removeNamed: function() {
var parent = this._parent; var owner = this._getOwner();
if (parent) { if (owner) {
var children = parent._children, var children = owner._children,
namedChildren = parent._namedChildren, namedChildren = owner._namedChildren,
name = this._name, name = this._name,
namedArray = namedChildren[name], namedArray = namedChildren[name],
index = namedArray ? namedArray.indexOf(this) : -1; index = namedArray ? namedArray.indexOf(this) : -1;
@ -2373,10 +2378,10 @@ var Item = Base.extend(Emitter, /** @lends Item# */{
delete children[name]; delete children[name];
// Remove this entry // Remove this entry
namedArray.splice(index, 1); namedArray.splice(index, 1);
// If there are any items left in the named array, set // If there are any items left in the named array, set the first
// the last of them to be this.parent.children[this.name] // of them to be children[this.name]
if (namedArray.length) { if (namedArray.length) {
children[name] = namedArray[namedArray.length - 1]; children[name] = namedArray[0];
} else { } else {
// Otherwise delete the empty array // Otherwise delete the empty array
delete namedChildren[name]; delete namedChildren[name];

View file

@ -62,19 +62,26 @@ var Layer = Group.extend(/** @lends Layer# */{
* position: view.center * position: view.center
* }); * });
*/ */
initialize: function Layer(arg) { initialize: function Layer() {
var props = Base.isPlainObject(arg) Group.apply(this, arguments);
? 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! * Private helper used in the constructor function to add the newly created
props.insert = false; * item to the project scene graph.
Group.call(this, props); */
if (insert === undefined || insert) { _addToProject: function(project) {
this._project.addChild(this); project.addChild(this);
// When inserted, also activate the layer by default. // When inserted, also activate the layer by default.
this.activate(); 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) if (project._activeLayer === this)
project._activeLayer = this.getNextSibling() project._activeLayer = this.getNextSibling()
|| this.getPreviousSibling(); || this.getPreviousSibling();
Base.splice(project.layers, null, this._index, 1); Base.splice(project._children, null, this._index, 1);
this._installEvents(false); this._installEvents(false);
// Notify self of the insertion change. We only need this // Notify self of the insertion change. We only need this
// notification if we're tracking changes for now. // notification if we're tracking changes for now.
@ -107,16 +114,6 @@ var Layer = Group.extend(/** @lends Layer# */{
return false; 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() { isInserted: function isInserted() {
return this._parent ? isInserted.base.call(this) : this._index != null; 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 // so paper.project is set, as required by Layer and DoumentView
// constructors. // constructors.
PaperScopeItem.call(this, true); PaperScopeItem.call(this, true);
this.layers = []; this._children = [];
this._namedChildren = {};
this._activeLayer = null; this._activeLayer = null;
this.symbols = []; this.symbols = [];
this._currentStyle = new Style(null, null, this); 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 // project serialization later, but deserialization of a layers array
// will always work. // will always work.
// Pass true for compact, so 'Project' does not get added as the class // 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}. * {@link Project#symbols}.
*/ */
clear: function() { clear: function() {
for (var i = this.layers.length - 1; i >= 0; i--) var children = this._children;
this.layers[i].remove(); for (var i = children.length - 1; i >= 0; i--)
children[i].remove();
this.symbols = []; this.symbols = [];
}, },
@ -103,7 +105,7 @@ var Project = PaperScopeItem.extend(/** @lends Project# */{
* @return Boolean * @return Boolean
*/ */
isEmpty: function() { 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. * The layers contained within the project.
* *
* @name Project#layers * @bean
* @type Layer[] * @type Layer[]
*/ */
getLayers: function() {
return this._children;
},
// TODO: Define #setLayers()?
/** /**
* The layer which is currently active. New items will be created on this * 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) { insertChild: function(index, item, _preserve) {
if (item instanceof Layer) { if (item instanceof Layer) {
item._remove(false, true); item._remove(false, true);
Base.splice(this.layers, [item], index, 0); Base.splice(this._children, [item], index, 0);
item._setProject(this, true); item._setProject(this, true);
// See Item#_remove() for an explanation of this: // See Item#_remove() for an explanation of this:
if (this._changes) if (this._changes)
@ -283,9 +290,9 @@ var Project = PaperScopeItem.extend(/** @lends Project# */{
* Selects all items in the project. * Selects all items in the project.
*/ */
selectAll: function() { selectAll: function() {
var layers = this.layers; var children = this._children;
for (var i = 0, l = layers.length; i < l; i++) for (var i = 0, l = children.length; i < l; i++)
layers[i].setFullySelected(true); 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 // We don't need to do this here, but it speeds up things since we won't
// repeatedly convert in Item#hitTest() then. // repeatedly convert in Item#hitTest() then.
var point = Point.read(arguments), 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 // Loop backwards, so layers that get drawn last are tested first
for (var i = this.layers.length - 1; i >= 0; i--) { for (var i = children.length - 1; i >= 0; i--) {
var res = this.layers[i]._hitTest(point, options); var res = children[i]._hitTest(point, options);
if (res) return res; if (res) return res;
} }
return null; return null;
@ -585,7 +593,7 @@ var Project = PaperScopeItem.extend(/** @lends Project# */{
* @return {Item[]} the list of matching items contained in the project * @return {Item[]} the list of matching items contained in the project
*/ */
getItems: function(match) { 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 * @return {Item} the first item in the project matching the given criteria
*/ */
getItem: function(match) { 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(); ctx.save();
matrix.applyToContext(ctx); matrix.applyToContext(ctx);
// Use new Base() so we can use param.extend() to easily override values // Use new Base() so we can use param.extend() to easily override values
var param = new Base({ var children = this._children,
offset: new Point(0, 0), param = new Base({
pixelRatio: pixelRatio, offset: new Point(0, 0),
viewMatrix: matrix.isIdentity() ? null : matrix, pixelRatio: pixelRatio,
matrices: [new Matrix()], // Start with the identity matrix. viewMatrix: matrix.isIdentity() ? null : matrix,
// Tell the drawing routine that we want to keep _globalMatrix up to matrices: [new Matrix()], // Start with the identity matrix.
// date. Item#rasterize() and Raster#getAverageColor() should not // Tell the drawing routine that we want to keep _globalMatrix
// set this. // up to date. Item#rasterize() and Raster#getAverageColor()
updateMatrix: true // should not set this.
}); updateMatrix: true
for (var i = 0, layers = this.layers, l = layers.length; i < l; i++) });
layers[i].draw(ctx, param); for (var i = 0, l = children.length; i < l; i++)
children[i].draw(ctx, param);
ctx.restore(); ctx.restore();
// Draw the selection of the selected items in the project: // Draw the selection of the selected items in the project:

View file

@ -417,7 +417,7 @@ new function() {
Project.inject({ Project.inject({
exportSVG: function(options) { exportSVG: function(options) {
options = setOptions(options); options = setOptions(options);
var layers = this.layers, var children = this._children,
view = this.getView(), view = this.getView(),
size = view.getViewSize(), size = view.getViewSize(),
node = createElement('svg', { node = createElement('svg', {
@ -436,8 +436,8 @@ new function() {
if (!matrix.isIdentity()) if (!matrix.isIdentity())
parent = node.appendChild( parent = node.appendChild(
createElement('g', getTransform(matrix))); createElement('g', getTransform(matrix)));
for (var i = 0, l = layers.length; i < l; i++) for (var i = 0, l = children.length; i < l; i++)
parent.appendChild(exportSVG(layers[i], options, true)); parent.appendChild(exportSVG(children[i], options, true));
return exportDefinitions(node, options); return exportDefinitions(node, options);
} }
}); });