Implement new and shorter segments array notation.

Supporting:

- Closing paths by including `true` as the last entry
- Nested segment arrays that can be passed to PathItem.create() and the CompoundPath constructor to create all sub-paths
This commit is contained in:
Jürg Lehni 2016-07-19 13:08:21 +02:00
parent 13a68cec46
commit e539633852
19 changed files with 201 additions and 154 deletions

View file

@ -72,7 +72,7 @@
"resemblejs": "^2.2.1",
"run-sequence": "^1.2.2",
"stats.js": "0.16.0",
"straps": "^2.0.1"
"straps": "^2.1.0"
},
"browser": {
"canvas": false,

View file

@ -77,7 +77,7 @@ var ProxyContext = new function() {
if (name === 'restore')
this._indents--;
console.log(this.getIndentation() + 'ctx.' + name + '('
+ Array.prototype.slice.call(arguments, 0)
+ Base.slice(arguments, 0)
.map(stringify).join(', ')
+ ');');
if (name === 'save')

View file

@ -166,13 +166,13 @@ Base.inject(/** @lends Base# */{
* @param {Array} list the list to read from, either an arguments object
* or a normal array
* @param {Number} start the index at which to start reading in the list
* @param {Number} length the amount of elements that can be read
* @param {Object} options `options.readNull` controls whether null is
* returned or converted. `options.clone` controls whether passed
* objects should be cloned if they are already provided in the
* required type
* @param {Number} length the amount of elements that can be read
*/
read: function(list, start, options, length) {
read: function(list, start, options, amount) {
// See if it's called directly on Base, and if so, read value and
// return without object conversion.
if (this === Base) {
@ -182,24 +182,29 @@ Base.inject(/** @lends Base# */{
}
var proto = this.prototype,
readIndex = proto._readIndex,
index = start || readIndex && list.__index || 0;
if (!length)
length = list.length - index;
var obj = list[index];
begin = start || readIndex && list.__index || 0,
length = list.length,
obj = list[begin];
amount = amount || length - begin;
// When read() is called on a sub-class of which the object is
// already an instance, or when there is only one value in the list
// and it's null or undefined, return the obj.
if (obj instanceof this
|| options && options.readNull && obj == null && length <= 1) {
|| options && options.readNull && obj == null && amount <= 1) {
if (readIndex)
list.__index = index + 1;
list.__index = begin + 1;
return obj && options && options.clone ? obj.clone() : obj;
}
// Otherwise, create a new object and read through its initialize
// function.
obj = Base.create(this.prototype);
if (readIndex)
obj.__read = true;
obj = obj.initialize.apply(obj, index > 0 || length < list.length
? Array.prototype.slice.call(list, index, index + length)
: list) || obj;
obj = obj.initialize.apply(obj, begin > 0 || begin + amount < length
? Base.slice(list, begin, begin + amount)
: list) || obj;
if (readIndex) {
list.__index = index + obj.__read;
list.__index = begin + obj.__read;
obj.__read = undefined;
}
return obj;
@ -235,11 +240,14 @@ Base.inject(/** @lends Base# */{
* returned or converted. `options.clone` controls whether passed
* objects should be cloned if they are already provided in the
* required type
* @param {Number} amount the amount of elements that should be read
*/
readAll: function(list, start, options) {
readList: function(list, start, options, amount) {
var res = [],
entry;
for (var i = start || 0, l = list.length; i < l; i++) {
entry,
begin = start || 0,
end = amount ? begin + amount : list.length;
for (var i = begin; i < end; i++) {
res.push(Array.isArray(entry = list[i])
? this.read(entry, 0, options)
: this.read(list, i, options, 1));
@ -255,11 +263,16 @@ Base.inject(/** @lends Base# */{
* various Path.Constructors.
*
* @param {Array} list the list to read from, either an arguments object
* or a normal array
* @param {Number} start the index at which to start reading in the list
* or a normal array
* @param {String} name the property name to read from
* @param {Number} start the index at which to start reading in the list
* @param {Object} options `options.readNull` controls whether null is
* returned or converted. `options.clone` controls whether passed
* objects should be cloned if they are already provided in the
* required type
* @param {Number} amount the amount of elements that can be read
*/
readNamed: function(list, name, start, options, length) {
readNamed: function(list, name, start, options, amount) {
var value = this.getNamed(list, name),
hasObject = value !== undefined;
if (hasObject) {
@ -276,7 +289,7 @@ Base.inject(/** @lends Base# */{
// shine through.
filtered[name] = undefined;
}
return this.read(hasObject ? [value] : list, start, options, length);
return this.read(hasObject ? [value] : list, start, options, amount);
},
/**
@ -513,7 +526,7 @@ Base.inject(/** @lends Base# */{
if (Base.isPlainObject(arg))
arg.insert = false;
}
// When reusing an object, initialize it through #_set()
// When reusing an object, initialize it through #set()
// instead of the constructor function:
(useTarget ? obj.set : ctor).apply(obj, args);
// Clear target to only use it once.

View file

@ -80,7 +80,7 @@ var Emitter = {
var handlers = this._callbacks && this._callbacks[type];
if (!handlers)
return false;
var args = [].slice.call(arguments, 1),
var args = Base.slice(arguments, 1),
// Set the current target to `this` if the event object defines
// #target but not #currentTarget.
setTarget = event && event.target && !event.currentTarget;

View file

@ -74,8 +74,8 @@ var Group = Item.extend(/** @lends Group# */{
* Creates a new Group item and places it at the top of the active layer.
*
* @name Group#initialize
* @param {Object} object an object literal containing the properties to be
* set on the group
* @param {Object} object an object containing the properties to be set on
* the group
*
* @example {@paperscript}
* var path = new Path([100, 100], [100, 200]);

View file

@ -2328,24 +2328,21 @@ new function() { // Injection scope for hit-test functions shared with project
* @return {Item[]} the inserted items, or `null` if inserted was not
* possible
*/
insertChildren: function(index, items, _preserve, _proto) {
insertChildren: function(index, items, _preserve) {
// CompoundPath#insertChildren() requires _preserve and _type:
// _preserve avoids changing of the children's path orientation
// _proto enforces the prototype of the inserted items, as used by
// CompoundPath#insertChildren()
var children = this._children;
if (children && items && items.length > 0) {
// We need to clone items because it might be
// an Item#children array. Also, we're removing elements if they
// don't match _type. Use Array.prototype.slice because items can be
// an arguments object.
items = Array.prototype.slice.apply(items);
// We need to clone items because it may be an Item#children array.
// Also, we're removing elements if they don't match _type.
// Use Base.slice() because items can be an arguments object.
items = Base.slice(items);
// Remove the items from their parents first, since they might be
// inserted into their own parents, affecting indices.
// Use the loop also to filter out wrong _type.
// Use the loop also to filter invalid items.
for (var i = items.length - 1; i >= 0; i--) {
var item = items[i];
if (!item || _proto && !(item instanceof _proto)) {
if (!item) {
items.splice(i, 1);
} else {
// Notify parent of change. Don't notify item itself yet,
@ -3949,9 +3946,9 @@ new function() { // Injection scope for hit-test functions shared with project
*
* @name Item#on
* @function
* @param {Object} object an object literal containing one or more of the
* following properties: {@values frame, mousedown, mouseup, mousedrag,
* click, doubleclick, mousemove, mouseenter, mouseleave}
* @param {Object} object an object containing one or more of the following
* properties: {@values frame, mousedown, mouseup, mousedrag, click,
* doubleclick, mousemove, mouseenter, mouseleave}
* @return {Item} this item itself, so calls can be chained
*
* @example {@paperscript}
@ -4018,9 +4015,9 @@ new function() { // Injection scope for hit-test functions shared with project
*
* @name Item#off
* @function
* @param {Object} object an object literal containing one or more of the
* following properties: {@values frame, mousedown, mouseup, mousedrag,
* click, doubleclick, mousemove, mouseenter, mouseleave}
* @param {Object} object an object containing one or more of the following
* properties: {@values frame, mousedown, mouseup, mousedrag, click,
* doubleclick, mousemove, mouseenter, mouseleave}
* @return {Item} this item itself, so calls can be chained
*/

View file

@ -47,8 +47,8 @@ var Layer = Group.extend(/** @lends Layer# */{
* so all newly created items will be placed within it.
*
* @name Layer#initialize
* @param {Object} object an object literal containing the properties to be
* set on the layer
* @param {Object} object an object containing the properties to be set on
* the layer
*
* @example {@paperscript}
* var path = new Path([100, 100], [100, 200]);

View file

@ -408,8 +408,8 @@ statics: new function() {
* object literal.
*
* @name Shape.Circle
* @param {Object} object an object literal containing properties
* describing the shape's attributes
* @param {Object} object an object containing properties describing the
* shape's attributes
* @return {Shape} the newly created shape
*
* @example {@paperscript}
@ -481,8 +481,8 @@ statics: new function() {
* object literal.
*
* @name Shape.Rectangle
* @param {Object} object an object literal containing properties
* describing the shape's attributes
* @param {Object} object an object containing properties describing the
* shape's attributes
* @return {Shape} the newly created shape
*
* @example {@paperscript}
@ -541,8 +541,8 @@ statics: new function() {
* object literal.
*
* @name Shape.Ellipse
* @param {Object} object an object literal containing properties
* describing the shape's attributes
* @param {Object} object an object containing properties describing the
* shape's attributes
* @return {Shape} the newly created shape
*
* @example {@paperscript}

View file

@ -59,8 +59,8 @@ var CompoundPath = PathItem.extend(/** @lends CompoundPath# */{
* at the top of the active layer.
*
* @name CompoundPath#initialize
* @param {Object} object an object literal containing properties to
* be set on the path
* @param {Object} object an object containing properties to be set on the
* path
* @return {CompoundPath} the newly created path
*
* @example {@paperscript}
@ -107,31 +107,44 @@ var CompoundPath = PathItem.extend(/** @lends CompoundPath# */{
},
insertChildren: function insertChildren(index, items, _preserve) {
// Convert CompoundPath items in the children list by adding their
// children to the list, replacing their parent.
// If we're passed an array notation for a simple path, wrap it again
// in an array to turn it into the array notation for a compound-path.
var list = items,
first = list[0];
if (first && typeof first[0] === 'number')
list = [list];
// Perform some conversions depending on the type of item passed:
// Convert array-notation to paths, and expand compound-paths in the
// items list by adding their children to the it replacing their parent.
for (var i = items.length - 1; i >= 0; i--) {
var item = items[i];
if (item instanceof CompoundPath) {
// Clone the items array before modifying it, as it may be a
// passed children array from another item.
items = items.slice();
items.splice.apply(items, [i, 1].concat(item.removeChildren()));
var item = list[i];
// Clone the list array before modifying it, as it may be a passed
// children array from another item.
if (list === items && !(item instanceof Path))
list = Base.slice(list);
if (Array.isArray(item)) {
var path = new Path({ segments: item, insert: false });
// Fix natural clockwise value, so it's not automatically
// determined when inserted into the compound-path.
// TODO: Remove reorientation code instead.
path.setClockwise(path.isClockwise());
list[i] = path;
} else if (item instanceof CompoundPath) {
list.splice.apply(list, [i, 1].concat(item.removeChildren()));
item.remove();
}
}
// Pass on 'path' for _type, to make sure that only paths are added as
// children.
items = insertChildren.base.call(this, index, items, _preserve, Path);
list = insertChildren.base.call(this, index, list, _preserve);
// All children except for the bottom one (first one in list) are set
// to anti-clockwise orientation, so that they appear as holes, but
// only if their orientation was not already specified before
// (= _clockwise is defined).
for (var i = 0, l = !_preserve && items && items.length; i < l; i++) {
var item = items[i];
for (var i = 0, l = !_preserve && list && list.length; i < l; i++) {
var item = list[i];
if (item._clockwise === undefined)
item.setClockwise(item._index === 0);
}
return items;
return list;
},
// DOCS: reduce()

View file

@ -67,8 +67,8 @@ Path.inject({ statics: new function() {
* literal.
*
* @name Path.Line
* @param {Object} object an object literal containing properties
* describing the path's attributes
* @param {Object} object an object containing properties describing the
* path's attributes
* @return {Path} the newly created path
*
* @example {@paperscript}
@ -102,8 +102,8 @@ Path.inject({ statics: new function() {
* object literal.
*
* @name Path.Circle
* @param {Object} object an object literal containing properties
* describing the path's attributes
* @param {Object} object an object containing properties describing the
* path's attributes
* @return {Path} the newly created path
*
* @example {@paperscript}
@ -174,8 +174,8 @@ Path.inject({ statics: new function() {
* object literal.
*
* @name Path.Rectangle
* @param {Object} object an object literal containing properties
* describing the path's attributes
* @param {Object} object an object containing properties describing the
* path's attributes
* @return {Path} the newly created path
*
* @example {@paperscript}
@ -267,8 +267,8 @@ Path.inject({ statics: new function() {
* object literal.
*
* @name Path.Ellipse
* @param {Object} object an object literal containing properties
* describing the path's attributes
* @param {Object} object an object containing properties describing the
* path's attributes
* @return {Path} the newly created path
*
* @example {@paperscript}
@ -317,8 +317,8 @@ Path.inject({ statics: new function() {
* object literal.
*
* @name Path.Arc
* @param {Object} object an object literal containing properties
* describing the path's attributes
* @param {Object} object an object containing properties describing the
* path's attributes
* @return {Path} the newly created path
*
* @example {@paperscript}
@ -363,8 +363,8 @@ Path.inject({ statics: new function() {
* described by an object literal.
*
* @name Path.RegularPolygon
* @param {Object} object an object literal containing properties
* describing the path's attributes
* @param {Object} object an object containing properties describing the
* path's attributes
* @return {Path} the newly created path
*
* @example {@paperscript}
@ -417,8 +417,8 @@ Path.inject({ statics: new function() {
* object literal.
*
* @name Path.Star
* @param {Object} object an object literal containing properties
* describing the path's attributes
* @param {Object} object an object containing properties describing the
* path's attributes
* @return {Path} the newly created path
*
* @example {@paperscript}

View file

@ -51,8 +51,8 @@ var Path = PathItem.extend(/** @lends Path# */{
* top of the active layer.
*
* @name Path#initialize
* @param {Object} object an object literal containing properties to
* be set on the path
* @param {Object} object an object containing properties to be set on the
* path
* @return {Path} the newly created path
*
* @example {@paperscript}
@ -178,13 +178,20 @@ var Path = PathItem.extend(/** @lends Path# */{
},
setSegments: function(segments) {
var fullySelected = this.isFullySelected();
var fullySelected = this.isFullySelected(),
length = segments && segments.length;
this._segments.length = 0;
this._segmentSelection = 0;
// Calculate new curves next time we call getCurves()
this._curves = undefined;
if (segments && segments.length > 0)
this._add(Segment.readAll(segments));
if (length) {
var last = segments[length - 1];
if (typeof last === 'boolean') {
this.setClosed(last);
length--;
}
this._add(Segment.readList(segments, 0, {}, length));
}
// Preserve fullySelected state.
// TODO: Do we still need this?
if (fullySelected)
@ -326,14 +333,14 @@ var Path = PathItem.extend(/** @lends Path# */{
dy = curY - prevY;
parts.push(
dx === 0 ? 'v' + f.number(dy)
: dy === 0 ? 'h' + f.number(dx)
: dy === 0 ? 'h' + f.number(dx)
: 'l' + f.pair(dx, dy));
}
} else {
// c = relative curveto:
parts.push('c' + f.pair(outX - prevX, outY - prevY)
+ ' ' + f.pair(inX - prevX, inY - prevY)
+ ' ' + f.pair(curX - prevX, curY - prevY));
+ ' ' + f.pair( inX - prevX, inY - prevY)
+ ' ' + f.pair(curX - prevX, curY - prevY));
}
}
prevX = curX;
@ -545,7 +552,7 @@ var Path = PathItem.extend(/** @lends Path# */{
add: function(segment1 /*, segment2, ... */) {
return arguments.length > 1 && typeof segment1 !== 'number'
// addSegments
? this._add(Segment.readAll(arguments))
? this._add(Segment.readList(arguments))
// addSegment
: this._add([ Segment.read(arguments) ])[0];
},
@ -589,7 +596,7 @@ var Path = PathItem.extend(/** @lends Path# */{
insert: function(index, segment1 /*, segment2, ... */) {
return arguments.length > 2 && typeof segment1 !== 'number'
// insertSegments
? this._add(Segment.readAll(arguments, 1), index)
? this._add(Segment.readList(arguments, 1), index)
// insertSegment
: this._add([ Segment.read(arguments, 1) ], index)[0];
},
@ -645,7 +652,7 @@ var Path = PathItem.extend(/** @lends Path# */{
* path2.position.x += 30;
*/
addSegments: function(segments) {
return this._add(Segment.readAll(segments));
return this._add(Segment.readList(segments));
},
/**
@ -659,7 +666,7 @@ var Path = PathItem.extend(/** @lends Path# */{
* belongs to another path
*/
insertSegments: function(index, segments) {
return this._add(Segment.readAll(segments), index);
return this._add(Segment.readList(segments), index);
},
/**

View file

@ -34,15 +34,54 @@ var PathItem = Item.extend(/** @lends PathItem# */{
* data describes a plain path or a compound-path with multiple
* sub-paths.
*
* @name PathItem.create
* @param {String} pathData the SVG path-data to parse
* @return {Path|CompoundPath} the newly created path item
*/
create: function(pathData) {
// If there are multiple moveTo commands or a closePath command
// followed by other commands, we have a CompoundPath.
var ctor = (pathData && pathData.match(/m/gi) || []).length > 1
|| /z\s*\S+/i.test(pathData) ? CompoundPath : Path;
return new ctor(pathData);
/**
* Creates a path item from the given segments array, determining if the
* array describes a plain path or a compound-path with multiple
* sub-paths.
*
* @name PathItem.create
* @param {Number[][]} segments the segments array to parse
* @return {Path|CompoundPath} the newly created path item
*/
/**
* Creates a path item from the given object, determining if the
* contained information describes a plain path or a compound-path with
* multiple sub-paths.
*
* @name PathItem.create
* @param {Object} object an object containing the properties describing
* the item to be created
* @return {Path|CompoundPath} the newly created path item
*/
create: function(arg) {
var data,
segments,
compound;
if (Base.isPlainObject(arg)) {
segments = arg.segments;
data = arg.pathData;
} else if (Array.isArray(arg)) {
segments = arg;
} else if (typeof arg === 'string') {
data = arg;
}
if (segments) {
var first = segments[0];
compound = first && Array.isArray(first[0]);
} else if (data) {
// If there are multiple moveTo commands or a closePath command
// followed by other commands, we have a CompoundPath.
compound = (data.match(/m/gi) || []).length > 1
|| /z\s*\S+/i.test(data);
}
var ctor = compound ? CompoundPath : Path;
return new ctor(arg);
}
},

View file

@ -57,8 +57,8 @@ var Segment = Base.extend(/** @lends Segment# */{
* Creates a new Segment object.
*
* @name Segment#initialize
* @param {Object} object an object literal containing properties to
* be set on the segment
* @param {Object} object an object containing properties to be set on the
* segment
*
* @example {@paperscript}
* // Creating segments using object notation:
@ -115,32 +115,31 @@ var Segment = Base.extend(/** @lends Segment# */{
*/
initialize: function Segment(arg0, arg1, arg2, arg3, arg4, arg5) {
var count = arguments.length,
point, handleIn, handleOut,
selection;
// TODO: Use Point.read or Point.readNamed to read these?
if (count === 0) {
// Nothing
} else if (count === 1) {
// NOTE: This copies from existing segments through accessors.
if (arg0 && 'point' in arg0) {
point = arg0.point;
handleIn = arg0.handleIn;
handleOut = arg0.handleOut;
selection = arg0.selection;
point, handleIn, handleOut, selection;
// TODO: Should we use Point.read() or Point.readNamed() to read these?
if (count > 0) {
if (arg0 == null || typeof arg0 === 'object') {
// Handle undefined, null and passed objects:
if (count === 1 && arg0 && 'point' in arg0) {
// NOTE: This copies from segments through accessors.
point = arg0.point;
handleIn = arg0.handleIn;
handleOut = arg0.handleOut;
selection = arg0.selection;
} else {
// It doesn't matter if all of these arguments exist.
// SegmentPoint() creates points with (0, 0) otherwise.
point = arg0;
handleIn = arg1;
handleOut = arg2;
selection = arg3;
}
} else {
point = arg0;
// Read points from the arguments list as a row of numbers.
point = [ arg0, arg1 ];
handleIn = arg2 !== undefined ? [ arg2, arg3 ] : null;
handleOut = arg4 !== undefined ? [ arg4, arg5 ] : null;
}
} else if (arg0 == null || typeof arg0 === 'object') {
// It doesn't matter if all of these arguments exist.
// new SegmentPoint() produces creates points with (0, 0) otherwise.
point = arg0;
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;
handleOut = arg4 !== undefined ? [ arg4, arg5 ] : null;
}
new SegmentPoint(point, this, '_point');
new SegmentPoint(handleIn, this, '_handleIn');

View file

@ -483,8 +483,7 @@ var Color = Base.extend(new function() {
*/
initialize: function Color(arg) {
// We are storing color internally as an array of components
var slice = Array.prototype.slice,
args = arguments,
var args = arguments,
reading = this.__read,
read = 0,
type,
@ -512,7 +511,7 @@ var Color = Base.extend(new function() {
if (reading)
read = 1; // Will be increased below
// Shift type out of the arguments, and process normally.
args = slice.call(args, 1);
args = Base.slice(args, 1);
argType = typeof arg;
}
}
@ -543,7 +542,7 @@ var Color = Base.extend(new function() {
: 1;
}
if (values.length > length)
values = slice.call(values, 0, length);
values = Base.slice(values, 0, length);
} else if (argType === 'string') {
type = 'rgb';
components = fromCSS(arg);

View file

@ -156,7 +156,7 @@ var Gradient = Base.extend(/** @lends Gradient# */{
for (var i = 0, l = _stops.length; i < l; i++)
_stops[i]._owner = undefined;
}
_stops = this._stops = GradientStop.readAll(stops, 0, { clone: true });
_stops = this._stops = GradientStop.readList(stops, 0, { clone: true });
// Now assign this gradient as the new gradients' owner.
for (var i = 0, l = _stops.length; i < l; i++)
_stops[i]._owner = this;

View file

@ -40,8 +40,8 @@ var PointText = TextItem.extend(/** @lends PointText# */{
* literal.
*
* @name PointText#initialize
* @param {Object} object an object literal containing properties
* describing the path's attributes
* @param {Object} object an object containing properties describing the
* path's attributes
* @return {PointText} the newly created point text
*
* @example {@paperscript}

View file

@ -39,7 +39,7 @@ var CanvasView = View.extend(/** @lends CanvasView# */{
if (size.isZero())
throw new Error(
'Cannot create CanvasView with the provided argument: '
+ [].slice.call(arguments, 1));
+ Base.slice(arguments, 1));
canvas = CanvasProvider.getCanvas(size);
}
var ctx = this._context = canvas.getContext('2d');

View file

@ -59,30 +59,9 @@ test('PathItem#create() with SVG path-data (#1101)', function() {
return res;
}
function create(data) {
var first = data && data[0];
if (first == null)
return null;
if (Array.isArray(first[0])) {
return new CompoundPath(data.map(create));
} else {
var closed = data[data.length - 1];
if (typeof closed === 'boolean') {
data.length--;
} else {
closed = false;
}
var path = new Path({ segments: data, closed: closed });
// Fix natural clockwise value, so it's not automatically determined
// when inserted into the compound-path.
path.clockwise = path.clockwise;
return path;
}
}
data.forEach(function(entry, i) {
var path = PathItem.create(entry);
// console.log(JSON.stringify(describe(path)));
equals(path, create(expected[i]), 'data[' + i + ']');
equals(path, PathItem.create(expected[i]), 'data[' + i + ']');
});
});

View file

@ -133,7 +133,8 @@ test('Raster#getSubCanvas', function(assert) {
255, 255, 255, 255
];
equals(function() {
return Base.equals(Array.prototype.slice.call(ctx.getImageData(0, 0, 1, 2).data), expected);
return Base.equals(Base.slice(ctx.getImageData(0, 0, 1, 2).data),
expected);
}, true);
done();
};