mirror of
https://github.com/scratchfoundation/paper.js.git
synced 2025-08-28 22:08:54 -04:00
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:
parent
0e2498bdce
commit
6f4890c63c
4 changed files with 122 additions and 59 deletions
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue