Implement new options to control bounding box in SVG Export

And use it to support SvgExport unit tests. Relates to #972
This commit is contained in:
Jürg Lehni 2016-02-15 00:13:38 +01:00
parent 0e2498bdce
commit 6f4890c63c
4 changed files with 122 additions and 59 deletions

View file

@ -21,7 +21,7 @@
* that they inherit from Item.
*/
var Item = Base.extend(Emitter, /** @lends Item# */{
statics: {
statics: /** @lends Item */{
/**
* Override Item.extend() to merge the subclass' _serializeFields with
* the parent class' _serializeFields.
@ -824,45 +824,6 @@ new function() { // Injection scope for various item event handlers
: bounds;
},
/**
* Protected method used in all the bounds getters. It loops through all the
* children, gets their bounds and finds the bounds around all of them.
* Subclasses override it to define calculations for the various required
* bounding types.
*/
_getBounds: function(matrix, options) {
// NOTE: We cannot cache these results here, since we do not get
// _changed() notifications here for changing geometry in children.
// But cacheName is used in sub-classes such as SymbolItem and Raster.
var children = this._children;
// TODO: What to return if nothing is defined, e.g. empty Groups?
// Scriptographer behaves weirdly then too.
if (!children || children.length === 0)
return new Rectangle();
// Call _updateBoundsCache() even when the group only holds empty /
// invisible items), so future changes in these items will cause right
// handling of _boundsCache.
Item._updateBoundsCache(this, options.cacheItem);
var x1 = Infinity,
x2 = -x1,
y1 = x1,
y2 = x2;
for (var i = 0, l = children.length; i < l; i++) {
var child = children[i];
if (child._visible && !child.isEmpty()) {
var rect = child._getCachedBounds(
matrix && matrix.appended(child._matrix), options);
x1 = Math.min(rect.x, x1);
y1 = Math.min(rect.y, y1);
x2 = Math.max(rect.x + rect.width, x2);
y2 = Math.max(rect.y + rect.height, y2);
}
}
return isFinite(x1)
? new Rectangle(x1, y1, x2 - x1, y2 - y1)
: new Rectangle();
},
setBounds: function(/* rect */) {
var rect = Rectangle.read(arguments),
bounds = this.getBounds(),
@ -893,6 +854,28 @@ new function() { // Injection scope for various item event handlers
this.transform(matrix);
},
/**
* Protected method used in all the bounds getters. It loops through all the
* children, gets their bounds and finds the bounds around all of them.
* Subclasses override it to define calculations for the various required
* bounding types.
*/
_getBounds: function(matrix, options) {
// NOTE: We cannot cache these results here, since we do not get
// _changed() notifications here for changing geometry in children.
// But cacheName is used in sub-classes such as SymbolItem and Raster.
var children = this._children;
// TODO: What to return if nothing is defined, e.g. empty Groups?
// Scriptographer behaves weirdly then too.
if (!children || children.length === 0)
return new Rectangle();
// Call _updateBoundsCache() even when the group only holds empty /
// invisible items), so future changes in these items will cause right
// handling of _boundsCache.
Item._updateBoundsCache(this, options.cacheItem);
return Item._getBounds(children, matrix, options);
},
/**
* Private method that deals with the calling of _getBounds, recursive
* matrix concatenation and handles all the complicated caching mechanisms.
@ -943,7 +926,7 @@ new function() { // Injection scope for various item event handlers
? this : this._parent).getViewMatrix().invert()._shiftless();
},
statics: {
statics: /** @lends Item */{
/**
* Set up a boundsCache structure that keeps track of items that keep
* cached bounds that depend on this item. We store this in the parent,
@ -996,6 +979,31 @@ new function() { // Injection scope for various item event handlers
}
}
}
},
/**
* Gets the combined bounds of all specified items.
*/
_getBounds: function(items, matrix, options) {
var x1 = Infinity,
x2 = -x1,
y1 = x1,
y2 = x2;
options = options || {};
for (var i = 0, l = items.length; i < l; i++) {
var item = items[i];
if (item._visible && !item.isEmpty()) {
var rect = item._getCachedBounds(
matrix && matrix.appended(item._matrix), options);
x1 = Math.min(rect.x, x1);
y1 = Math.min(rect.y, y1);
x2 = Math.max(rect.x + rect.width, x2);
y2 = Math.max(rect.y + rect.height, y2);
}
}
return isFinite(x1)
? new Rectangle(x1, y1, x2 - x1, y2 - y1)
: new Rectangle();
}
}
@ -2085,7 +2093,7 @@ new function() { // Injection scope for hit-test functions shared with project
|| null;
},
statics: {
statics: /** @lends Item */{
// 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(item, options, matrix, param, firstOnly) {

View file

@ -75,7 +75,7 @@ new function() {
var clip = SvgElement.create('clipPath');
clip.appendChild(childNode);
setDefinition(child, clip, 'clip');
SvgElement.set(node, {
SvgElement.set(node, {
'clip-path': 'url(#' + clip.id + ')'
});
} else {
@ -317,7 +317,7 @@ new function() {
if (!item._visible)
attrs.visibility = 'hidden';
return SvgElement.set(node, attrs, formatter);
return SvgElement.set(node, attrs, formatter);
}
var definitions;
@ -404,25 +404,37 @@ new function() {
options = setOptions(options);
var children = this._children,
view = this.getView(),
size = view.getViewSize(),
node = SvgElement.create('svg', {
x: 0,
y: 0,
width: size.width,
height: size.height,
bounds = Base.pick(options.bounds, 'view'),
matrix = Matrix.read(
[options.matrix || bounds === 'view' && view._matrix],
0, { readNull: true }),
rect = bounds === 'view'
? new Rectangle([0, 0], view.getViewSize())
: bounds === 'content'
? Item._getBounds(children, matrix, { stroke: true })
: Rectangle.read([bounds], 0, { readNull: true });
attrs = {
version: '1.1',
xmlns: SvgElement.svg,
'xmlns:xlink': SvgElement.xlink
}, formatter),
parent = node,
matrix = view._matrix;
xmlns: SvgElement.svg,
'xmlns:xlink': SvgElement.xlink,
};
if (rect) {
attrs.width = rect.width;
attrs.height = rect.height;
if (rect.x || rect.y)
attrs.viewBox = formatter.rectangle(rect);
}
var node = SvgElement.create('svg', attrs, formatter),
parent = node;
// If the view has a transformation, wrap all layers in a group with
// that transformation applied to.
if (!matrix.isIdentity())
if (matrix && !matrix.isIdentity()) {
parent = node.appendChild(SvgElement.create('g',
getTransform(matrix), formatter));
for (var i = 0, l = children.length; i < l; i++)
}
for (var i = 0, l = children.length; i < l; i++) {
parent.appendChild(exportSVG(children[i], options, true));
}
return exportDefinitions(node, options);
}
});

View file

@ -485,11 +485,19 @@ var compareSVG = function(done, actual, expected, message, options) {
actual = actual();
}
expected.onLoad = function() {
function compare() {
comparePixels(actual, expected, message, Base.set({
tolerance: 1e-2,
resolution: 72
}, options));
done();
};
}
if (expected instanceof Raster) {
expected.onLoad = compare;
} else if (actual instanceof Raster) {
actual.onLoad = compare;
} else {
compare();
}
};

View file

@ -112,3 +112,38 @@ test('Export SVG path at precision 0', function() {
var path = new Path('M0.123456789,1.9l0.8,1.1');
equals(path.exportSVG({ precision: 0 }).getAttribute('d'), 'M0,2l1,1');
});
test('Export transformed shapes', function(assert) {
var rect = new Shape.Rectangle({
point: [200, 100],
size: [200, 300],
fillColor: 'red'
});
rect.rotate(40);
var circle = new Shape.Circle({
center: [200, 300],
radius: 100,
fillColor: 'green'
});
circle.scale(0.5, 1);
circle.rotate(40);
var ellipse = new Shape.Ellipse({
point: [300, 300],
size: [100, 200],
fillColor: 'blue'
});
ellipse.rotate(-40);
var rect = new Shape.Rectangle({
point: [250, 20],
size: [200, 300],
radius: [40, 20],
fillColor: 'yellow'
});
rect.rotate(-20);
var svg = project.exportSVG({ asString: true, bounds: 'content' });
console.log(svg);
compareSVG(assert.async(), svg, project.activeLayer);
});