Improve performance of Path constructors and handling of { insert: false } Item creation.

This commit is contained in:
Jürg Lehni 2014-02-26 16:15:51 +01:00
parent 737466d15c
commit ccfd51a65a
9 changed files with 107 additions and 85 deletions

View file

@ -39,7 +39,15 @@ var Item = Base.extend(Callback, /** @lends Item# */{
if (name) if (name)
proto._type = Base.hyphenate(name); proto._type = Base.hyphenate(name);
return res; return res;
} },
/**
* An object constant that can be passed to Item#initialize() to avoid
* insertion into the DOM.
*
* @private
*/
NO_INSERT: { insert: false }
}, },
_class: 'Item', _class: 'Item',
@ -68,6 +76,16 @@ var Item = Base.extend(Callback, /** @lends Item# */{
// Do nothing, but declare it for named constructors. // Do nothing, but declare it for named constructors.
}, },
/**
* Private helper for #initialize() that tries setting properties from the
* passed props object, and apply the point translation to the internal
* matrix.
*
* @param {Object} props the properties to be applied to the item
* @param {Point} point the point by which to transform the internal matrix
* @returns {Boolean} {@true if the properties were successfully be applied,
* or if none were provided}
*/
_initialize: function(props, point) { _initialize: function(props, point) {
// Define this Item's unique id. But allow the creation of internally // Define this Item's unique id. But allow the creation of internally
// used paths with no ids. // used paths with no ids.
@ -95,7 +113,10 @@ var Item = Base.extend(Callback, /** @lends Item# */{
} }
} }
this._style = new Style(this._project._currentStyle, this); this._style = new Style(this._project._currentStyle, this);
return props ? this._set(props, { insert: true }) : true; // Filter out Item.NO_INSERT before _set(), for performance reasons
return props && props !== Item.NO_INSERT
? this._set(props, { insert: true }) // Filter out insert prop.
: true;
}, },
_events: new function() { _events: new function() {
@ -1443,7 +1464,7 @@ var Item = Base.extend(Callback, /** @lends Item# */{
* } * }
*/ */
clone: function(insert) { clone: function(insert) {
return this._clone(new this.constructor({ insert: false }), insert); return this._clone(new this.constructor(Item.NO_INSERT), insert);
}, },
_clone: function(copy, insert) { _clone: function(copy, insert) {
@ -1538,12 +1559,10 @@ var Item = Base.extend(Callback, /** @lends Item# */{
// See Project#draw() for an explanation of new Base() // See Project#draw() for an explanation of new Base()
this.draw(ctx, new Base({ transforms: [matrix] })); this.draw(ctx, new Base({ transforms: [matrix] }));
ctx.restore(); ctx.restore();
var raster = new Raster({ var raster = new Raster(Item.NO_INSERT);
canvas: canvas, raster.setCanvas(canvas);
insert: false
});
raster.transform(new Matrix().translate(topLeft.add(size.divide(2))) raster.transform(new Matrix().translate(topLeft.add(size.divide(2)))
// Take resolution into acocunt and scale back to original size. // Take resolution into account and scale back to original size.
.scale(1 / scale)); .scale(1 / scale));
raster.insertAbove(this); raster.insertAbove(this);
// NOTE: We don't need to release the canvas since it now belongs to the // NOTE: We don't need to release the canvas since it now belongs to the

View file

@ -99,10 +99,9 @@ var PlacedSymbol = Item.extend(/** @lends PlacedSymbol# */{
}, },
clone: function(insert) { clone: function(insert) {
return this._clone(new PlacedSymbol({ var copy = new PlacedSymbol(Item.NO_INSERT);
symbol: this.symbol, copy.setSymbol(this._symbol);
insert: false return this._clone(copy, insert);
}), insert);
}, },
isEmpty: function() { isEmpty: function() {

View file

@ -97,17 +97,19 @@ var Raster = Item.extend(/** @lends Raster# */{
}, },
clone: function(insert) { clone: function(insert) {
var param = { insert: false }, var copy = new Raster(Item.NO_INSERT),
image = this._image; image = this._image,
canvas = this._canvas;
if (image) { if (image) {
param.image = image; copy.setImage(image);
} else if (this._canvas) { } else if (canvas) {
// If the Raster contains a Canvas object, we need to create // If the Raster contains a Canvas object, we need to create a new
// a new one and draw this raster's canvas on it. // one and draw this raster's canvas on it.
var canvas = param.canvas = CanvasProvider.getCanvas(this._size); var copyCanvas = CanvasProvider.getCanvas(this._size);
canvas.getContext('2d').drawImage(this._canvas, 0, 0); copyCanvas.getContext('2d').drawImage(canvas, 0, 0);
copy.setCanvas(copyCanvas);
} }
return this._clone(new Raster(param), insert); return this._clone(copy, insert);
}, },
/** /**
@ -394,10 +396,8 @@ var Raster = Item.extend(/** @lends Raster# */{
*/ */
getSubRaster: function(rect) { // TODO: Fix argument assignment! getSubRaster: function(rect) { // TODO: Fix argument assignment!
var rect = Rectangle.read(arguments), var rect = Rectangle.read(arguments),
raster = new Raster({ raster = new Raster(Item.NO_INSERT);
canvas: this.getSubCanvas(rect), raster.setCanvas(this.getSubCanvas(rect));
insert: false
});
raster.translate(rect.getCenter().subtract(this.getSize().divide(2))); raster.translate(rect.getCenter().subtract(this.getSize().divide(2)));
raster._matrix.preConcatenate(this._matrix); raster._matrix.preConcatenate(this._matrix);
raster.insertAbove(this); raster.insertAbove(this);

View file

@ -39,12 +39,11 @@ var Shape = Item.extend(/** @lends Shape# */{
}, },
clone: function(insert) { clone: function(insert) {
return this._clone(new Shape({ var copy = new Shape(Item.NO_INSERT);
shape: this._shape, copy.setShape(this._shape);
size: this._size, copy.setSize(this._size);
radius: this._radius, copy.setRadius(this._radius);
insert: false return this._clone(copy, insert);
}), insert);
}, },
/** /**

View file

@ -20,9 +20,19 @@ Path.inject({ statics: new function() {
new Segment([0, 1], [kappa, 0 ], [-kappa, 0]) new Segment([0, 1], [kappa, 0 ], [-kappa, 0])
]; ];
function createPath(segments, closed, args) {
var props = Base.getNamed(args),
path = new Path(props && props.insert === false && Item.NO_INSERT);
path._add(segments);
// No need to use setter for _closed since _add() called _changed().
path._closed = true;
// Set named arguments at the end, since some depend on geometry to be
// defined (e.g. #clockwise)
return path.set(props);
}
function createEllipse(center, radius, args) { function createEllipse(center, radius, args) {
var path = new Path(), var segments = new Array(4);
segments = new Array(4);
for (var i = 0; i < 4; i++) { for (var i = 0; i < 4; i++) {
var segment = ellipseSegments[i]; var segment = ellipseSegments[i];
segments[i] = new Segment( segments[i] = new Segment(
@ -31,11 +41,7 @@ Path.inject({ statics: new function() {
segment._handleOut.multiply(radius) segment._handleOut.multiply(radius)
); );
} }
path._add(segments); return createPath(segments, true, args);
path._closed = true;
// Set named arguments at the end, since some depend on geometry to be
// defined (e.g. #clockwise)
return path.set(Base.getNamed(args));
} }
@ -73,10 +79,10 @@ Path.inject({ statics: new function() {
* }); * });
*/ */
Line: function(/* from, to */) { Line: function(/* from, to */) {
return new Path([ return createPath([
Point.readNamed(arguments, 'from'), new Segment(Point.readNamed(arguments, 'from')),
Point.readNamed(arguments, 'to') new Segment(Point.readNamed(arguments, 'to'))
]).set(Base.getNamed(arguments)); ], false, arguments);
}, },
/** /**
@ -210,22 +216,22 @@ Path.inject({ statics: new function() {
bl = rect.getBottomLeft(true), bl = rect.getBottomLeft(true),
tl = rect.getTopLeft(true), tl = rect.getTopLeft(true),
tr = rect.getTopRight(true), tr = rect.getTopRight(true),
br = rect.getBottomRight(true); br = rect.getBottomRight(true),
path = new Path(); segments;
if (!radius || radius.isZero()) { if (!radius || radius.isZero()) {
path._add([ segments = [
new Segment(bl), new Segment(bl),
new Segment(tl), new Segment(tl),
new Segment(tr), new Segment(tr),
new Segment(br) new Segment(br)
]); ];
} else { } else {
radius = Size.min(radius, rect.getSize(true).divide(2)); radius = Size.min(radius, rect.getSize(true).divide(2));
var rx = radius.width, var rx = radius.width,
ry = radius.height, ry = radius.height,
hx = rx * kappa, hx = rx * kappa,
hy = ry * kappa; hy = ry * kappa;
path._add([ segments = [
new Segment(bl.add(rx, 0), null, [-hx, 0]), new Segment(bl.add(rx, 0), null, [-hx, 0]),
new Segment(bl.subtract(0, ry), [0, hy]), new Segment(bl.subtract(0, ry), [0, hy]),
new Segment(tl.add(0, ry), null, [0, -hy]), new Segment(tl.add(0, ry), null, [0, -hy]),
@ -234,11 +240,9 @@ Path.inject({ statics: new function() {
new Segment(tr.add(0, ry), [0, -hy], null), new Segment(tr.add(0, ry), [0, -hy], null),
new Segment(br.subtract(0, ry), null, [0, hy]), new Segment(br.subtract(0, ry), null, [0, hy]),
new Segment(br.subtract(rx, 0), [hx, 0]) new Segment(br.subtract(rx, 0), [hx, 0])
]); ];
} }
// No need to use setter for _closed since _add() called _changed(). return createPath(segments, true, arguments);
path._closed = true;
return path.set(Base.getNamed(arguments));
}, },
/** /**
@ -329,10 +333,13 @@ Path.inject({ statics: new function() {
var from = Point.readNamed(arguments, 'from'), var from = Point.readNamed(arguments, 'from'),
through = Point.readNamed(arguments, 'through'), through = Point.readNamed(arguments, 'through'),
to = Point.readNamed(arguments, 'to'), to = Point.readNamed(arguments, 'to'),
path = new Path(); props = Base.getNamed(arguments),
// See createPath() for an explanation of the following sequence
path = new Path(props && props.insert === false
&& Item.NO_INSERT);
path.moveTo(from); path.moveTo(from);
path.arcTo(through, to); path.arcTo(through, to);
return path.set(Base.getNamed(arguments)); return path.set(props);
}, },
/** /**
@ -372,19 +379,15 @@ Path.inject({ statics: new function() {
var center = Point.readNamed(arguments, 'center'), var center = Point.readNamed(arguments, 'center'),
sides = Base.readNamed(arguments, 'sides'), sides = Base.readNamed(arguments, 'sides'),
radius = Base.readNamed(arguments, 'radius'), radius = Base.readNamed(arguments, 'radius'),
path = new Path(),
step = 360 / sides, step = 360 / sides,
three = !(sides % 3), three = !(sides % 3),
vector = new Point(0, three ? -radius : radius), vector = new Point(0, three ? -radius : radius),
offset = three ? -1 : 0.5, offset = three ? -1 : 0.5,
segments = new Array(sides); segments = new Array(sides);
for (var i = 0; i < sides; i++) { for (var i = 0; i < sides; i++)
segments[i] = new Segment(center.add( segments[i] = new Segment(center.add(
vector.rotate((i + offset) * step))); vector.rotate((i + offset) * step)));
} return createPath(segments, true, arguments);
path._add(segments);
path._closed = true;
return path.set(Base.getNamed(arguments));
}, },
/** /**
@ -432,17 +435,13 @@ Path.inject({ statics: new function() {
points = Base.readNamed(arguments, 'points') * 2, points = Base.readNamed(arguments, 'points') * 2,
radius1 = Base.readNamed(arguments, 'radius1'), radius1 = Base.readNamed(arguments, 'radius1'),
radius2 = Base.readNamed(arguments, 'radius2'), radius2 = Base.readNamed(arguments, 'radius2'),
path = new Path(),
step = 360 / points, step = 360 / points,
vector = new Point(0, -1), vector = new Point(0, -1),
segments = new Array(points); segments = new Array(points);
for (var i = 0; i < points; i++) { for (var i = 0; i < points; i++)
segments[i] = new Segment(center.add( segments[i] = new Segment(center.add(vector.rotate(step * i)
vector.rotate(step * i).multiply(i % 2 ? radius2 : radius1))); .multiply(i % 2 ? radius2 : radius1)));
} return createPath(segments, true, arguments);
path._add(segments);
path._closed = true;
return path.set(Base.getNamed(arguments));
} }
}; };
}}); }});

View file

@ -106,11 +106,17 @@ var Path = PathItem.extend(/** @lends Path# */{
? arguments ? arguments
: null; : null;
// Always call setSegments() to initialize a few related variables. // Always call setSegments() to initialize a few related variables.
this.setSegments(segments || []); if (segments && segments.length > 0) {
if (!segments && typeof arg === 'string') { // This sets _curves and _selectedSegmentState too!
this.setPathData(arg); this.setSegments(segments);
// Erase for _initialize() call below. } else {
arg = null; this._curves = undefined; // For hidden class optimization
this._selectedSegmentState = 0;
if (!segments && typeof arg === 'string') {
this.setPathData(arg);
// Erase for _initialize() call below.
arg = null;
}
} }
// Only pass on arg as props if it wasn't consumed for segments already. // Only pass on arg as props if it wasn't consumed for segments already.
this._initialize(!segments && arg); this._initialize(!segments && arg);
@ -121,15 +127,12 @@ var Path = PathItem.extend(/** @lends Path# */{
}, },
clone: function(insert) { clone: function(insert) {
var copy = this._clone(new Path({ var copy = new Path(Item.NO_INSERT);
segments: this._segments, copy.setSegments(this._segments);
insert: false
}), insert);
// Speed up things a little by copy over values that don't need checking
copy._closed = this._closed; copy._closed = this._closed;
if (this._clockwise !== undefined) if (this._clockwise !== undefined)
copy._clockwise = this._clockwise; copy._clockwise = this._clockwise;
return copy; return this._clone(copy, insert);
}, },
_changed: function _changed(flags) { _changed: function _changed(flags) {
@ -177,7 +180,10 @@ var Path = PathItem.extend(/** @lends Path# */{
this._selectedSegmentState = 0; this._selectedSegmentState = 0;
// Calculate new curves next time we call getCurves() // Calculate new curves next time we call getCurves()
this._curves = undefined; this._curves = undefined;
this._add(Segment.readAll(segments)); if (segments && segments.length > 0)
this._add(Segment.readAll(segments));
// Preserve fullySelected state.
// TODO: Do we still need this?
if (fullySelected) if (fullySelected)
this.setFullySelected(true); this.setFullySelected(true);
}, },
@ -1785,7 +1791,7 @@ var Path = PathItem.extend(/** @lends Path# */{
// Code to check stroke join / cap areas // Code to check stroke join / cap areas
function addAreaPoint(point) { function addToArea(point) {
area.add(point); area.add(point);
} }
@ -1803,10 +1809,10 @@ var Path = PathItem.extend(/** @lends Path# */{
if (join !== 'round' && (segment._handleIn.isZero() if (join !== 'round' && (segment._handleIn.isZero()
|| segment._handleOut.isZero())) || segment._handleOut.isZero()))
Path._addSquareJoin(segment, join, radius, miterLimit, Path._addSquareJoin(segment, join, radius, miterLimit,
addAreaPoint, true); addToArea, true);
} else if (cap !== 'round') { } else if (cap !== 'round') {
// It's a cap // It's a cap
Path._addSquareCap(segment, cap, radius, addAreaPoint, true); Path._addSquareCap(segment, cap, radius, addToArea, true);
} }
// See if the above produced an area to check for // See if the above produced an area to check for
if (!area.isEmpty()) { if (!area.isEmpty()) {

View file

@ -364,7 +364,7 @@ PathItem.inject(new function() {
seg = startSeg = segments[i]; seg = startSeg = segments[i];
if (seg._visited || !operator(seg._winding)) if (seg._visited || !operator(seg._winding))
continue; continue;
var path = new Path({ insert: false }), var path = new Path(Item.NO_INSERT),
inter = seg._intersection, inter = seg._intersection,
startInterSeg = inter && inter._segment, startInterSeg = inter && inter._segment,
added = false, // Wether a first segment as added already added = false, // Wether a first segment as added already

View file

@ -183,7 +183,7 @@ var Project = PaperScopeItem.extend(/** @lends Project# */{
// NOTE: If there is no layer and this project is not the active // NOTE: If there is no layer and this project is not the active
// one, passing insert: false and calling addChild on the // one, passing insert: false and calling addChild on the
// project will handle it correctly. // project will handle it correctly.
|| this.addChild(new Layer({ insert: false }))).addChild(child); || this.addChild(new Layer(Item.NO_INSERT))).addChild(child);
} else { } else {
child = null; child = null;
} }

View file

@ -59,7 +59,7 @@ var PointText = TextItem.extend(/** @lends PointText# */{
}, },
clone: function(insert) { clone: function(insert) {
return this._clone(new PointText({ insert: false }), insert); return this._clone(new PointText(Item.NO_INSERT), insert);
}, },
/** /**