Massive refactoring of transform() / getBounds() code: getBounds() / getStrokeBounds() now supports an optional Matrix parameter which is used to on the fly transform all coordinates and stroke definitions before bounds are calculated. This even supports the correct determination of rotated ellipse bounds for round strokes in symbols.

This commit is contained in:
Jürg Lehni 2011-03-06 21:26:38 +00:00
parent 6def2b9d3a
commit 87981efeb5
4 changed files with 160 additions and 82 deletions

View file

@ -19,8 +19,6 @@ var PlacedSymbol = this.PlacedSymbol = Item.extend({
} else {
this.matrix = new Matrix();
}
// TODO: should size be cached here, or on Symbol?
this._size = this.symbol.getDefinition().getStrokeBounds().getSize();
},
_transform: function(matrix, flags) {
@ -28,17 +26,14 @@ var PlacedSymbol = this.PlacedSymbol = Item.extend({
// raster, simply preconcatenate the internal matrix with the provided
// one.
this.matrix.preConcatenate(matrix);
this._bounds = null;
},
getBounds: function() {
// TODO: Is this right here? Shouldn't we calculate the bounds of the
// symbol transformed by this.matrix?
if (!this._bounds) {
this._bounds = this.matrix.transformBounds(
new Rectangle(this._size).setCenter(0, 0));
}
return this._bounds;
return this.symbol._definition.getStrokeBounds(this.matrix);
},
getStrokeBounds: function() {
return this.getBounds();
},
draw: function(ctx, param) {

View file

@ -70,41 +70,10 @@ var Path = this.Path = PathItem.extend({
// taken into account.
_transform: function(matrix, flags) {
var coords = new Array(6);
for (var i = 0, l = this._segments.length; i < l; i++) {
var segment = this._segments[i];
// Use matrix.transform version() that takes arrays of multiple
// points for largely improved performance, as no calls to
// Point.read() and Point constructors are necessary.
var point = segment._point,
handleIn = segment.getHandleInIfSet(),
handleOut = segment.getHandleOutIfSet(),
x = point.x,
y = point.y;
coords[0] = x;
coords[1] = y;
var index = 2;
// We need to convert handles to absolute coordinates in order
// to transform them.
if (handleIn) {
coords[index++] = handleIn.x + x;
coords[index++] = handleIn.y + y;
}
if (handleOut) {
coords[index++] = handleOut.x + x;
coords[index++] = handleOut.y + y;
}
matrix.transform(coords, 0, coords, 0, index / 2);
x = point.x = coords[0];
y = point.y = coords[1];
index = 2;
if (handleIn) {
handleIn.x = coords[index++] - x;
handleIn.y = coords[index++] - y;
}
if (handleOut) {
handleOut.x = coords[index++] - x;
handleOut.y = coords[index++] - y;
if (!matrix.isIdentity()) {
var coords = new Array(6);
for (var i = 0, l = this._segments.length; i < l; i++) {
this._segments[i]._transformCoordinates(matrix, coords, true);
}
}
},
@ -215,27 +184,32 @@ var Path = this.Path = PathItem.extend({
tMin = epsilon,
tMax = 1 - epsilon;
function calculateBounds(that, strokeRadius) {
function calculateBounds(that, matrix, strokePadding) {
// Code ported and further optimised from:
// http://blog.hackers-cafe.net/2009/06/how-to-calculate-bezier-curves-bounding.html
var segments = that._segments, first = segments[0];
var segments = that._segments,
first = segments[0];
if (!first)
return null;
var min = first._point.clone(),
max = min.clone(),
prev = first,
coords = ['x', 'y'];
var coords = new Array(6),
prevCoords = new Array(6);
// Make coordinates for first segment available in prevCoords.
if (matrix && matrix.isIdentity())
matrix = null;
first._transformCoordinates(matrix, prevCoords, false);
var min = prevCoords.slice(0, 2),
max = min.slice(0), // clone
function processSegment(segment) {
for (var i = 0; i < 2; i++) {
var coord = coords[i];
segment._transformCoordinates(matrix, coords, false);
var v0 = prev._point[coord],
v1 = v0 + prev._handleOut[coord],
v3 = segment._point[coord],
v2 = v3 + segment._handleIn[coord];
for (var i = 0; i < 2; i++) {
var v0 = prevCoords[i], // prev.point
v1 = prevCoords[i + 4], // prev.handleOut
v2 = coords[i + 2], // segment.handleIn
v3 = coords[i]; // segment.point
function add(value, t) {
var radius = 0;
var padding = 0;
if (value == null) {
// Calculate bezier polynomial at t
var u = 1 - t;
@ -246,14 +220,14 @@ var Path = this.Path = PathItem.extend({
// Only add strokeWidth to bounds for points which lie
// within 0 < t < 1. The corner cases for cap and join
// are handled in getStrokeBounds()
radius = strokeRadius;
padding = strokePadding ? strokePadding[i] : 0;
}
var left = value - radius,
right = value + radius;
if (left < min[coord])
min[coord] = left;
if (right > max[coord])
max[coord] = right;
var left = value - padding,
right = value + padding;
if (left < min[i])
min[i] = left;
if (right > max[i])
max[i] = right;
}
add(v3, null);
@ -290,13 +264,16 @@ var Path = this.Path = PathItem.extend({
if (tMin < t2 && t2 < tMax)
add(null, t2);
}
prev = segment;
// Swap coordinate buffers
var tmp = prevCoords;
prevCoords = coords;
coords = tmp;
}
for (var i = 1, l = segments.length; i < l; i++)
processSegment(segments[i]);
if (that.closed)
processSegment(first);
return new Rectangle(min.x, min.y, max.x - min.x , max.y - min.y);
return new Rectangle(min[0], min[1], max[0] - min[0], max[1] - min[1]);
}
/**
@ -338,16 +315,17 @@ var Path = this.Path = PathItem.extend({
/**
* The bounding rectangle of the item excluding stroke width.
*/
getBounds: function() {
return calculateBounds(this, 0);
getBounds: function(matrix) {
return calculateBounds(this, matrix);
},
/**
* The bounding rectangle of the item including stroke width.
*/
getStrokeBounds: function() {
getStrokeBounds: function(matrix) {
var width = this.getStrokeWidth(),
radius = width / 2,
padding = [radius, radius],
join = this.getStrokeJoin(),
cap = this.getStrokeCap(),
// miter is relative to width. Divide it by 2 since we're
@ -356,13 +334,60 @@ var Path = this.Path = PathItem.extend({
segments = this._segments,
length = segments.length,
closed= this.closed,
bounds = calculateBounds(this, radius);
bounds = calculateBounds(this, matrix, padding);
// If a matrix is provided, we need to rotate the stroke circle
// and calculate the bounding box of the resulting rotated elipse:
if (matrix) {
// Get rotated hor and ver vectors, and determine rotation angle
// and elipse values from them:
var mx = matrix.createShiftless(),
hor = mx.transform(new Point(radius, 0)),
ver = mx.transform(new Point(0, radius)),
phi = hor.getAngleInRadians(),
a = hor.getLength(),
b = ver.getLength();
// Formula for rotated ellipses:
// x = cx + a*cos(t)*cos(phi) - b*sin(t)*sin(phi)
// y = cy + b*sin(t)*cos(phi) + a*cos(t)*sin(phi)
// Derivates (by Wolfram Alpha):
// derivative of x = cx + a*cos(t)*cos(phi) - b*sin(t)*sin(phi)
// dx/dt = a sin(t) cos(phi) + b cos(t) sin(phi) = 0
// derivative of y = cy + b*sin(t)*cos(phi) + a*cos(t)*sin(phi)
// dy/dt = b cos(t) cos(phi) - a sin(t) sin(phi) = 0
// this can be simplified to:
// tan(t) = -b * tan(phi) / a // x
// tan(t) = b * cot(phi) / a // y
// Solving for t gives:
// t = pi * n - arctan(b tan(phi)) // x
// t = pi * n + arctan(b cot(phi)) // y
var tx = - Math.atan(b * Math.tan(phi)),
ty = + Math.atan(b / Math.tan(phi)),
// Due to symetry, we don't need to cycle through pi * n
// solutions:
x = a * Math.cos(tx) * Math.cos(phi),
- b * Math.sin(tx) * Math.sin(phi),
y = b * Math.sin(ty) * Math.cos(phi)
+ a * Math.cos(ty) * Math.sin(phi);
// Now update the join / round padding, as required by
// calculateBounds() and code below.
padding = [Math.abs(x), Math.abs(y)];
}
// Create a rectangle of padding size, used for union with bounds
// further down
var joinBounds = new Rectangle(new Size(padding).multiply(2));
function add(point) {
bounds = bounds.include(matrix
? matrix.transform(point) : point);
}
function addBevelJoin(curve, t) {
var point = curve.getPoint(t),
normal = curve.getNormal(t).normalize(radius);
bounds = bounds.include(point.add(normal));
bounds = bounds.include(point.subtract(normal));
add(point.add(normal));
add(point.subtract(normal));
}
function addJoin(segment, join) {
@ -371,8 +396,8 @@ var Path = this.Path = PathItem.extend({
// When both handles are set in a segment, the join setting is
// ignored and round is always used.
if (join == 'round' || handleIn && handleOut) {
bounds = bounds.unite(new Rectangle(new Size(width, width))
.setCenter(segment._point));
bounds = bounds.unite(joinBounds.setCenter(matrix
? matrix.transform(segment._point) : segment._point));
} else {
switch (join) {
case 'bevel':
@ -397,7 +422,7 @@ var Path = this.Path = PathItem.extend({
if (!corner || point.getDistance(corner) > miter) {
addJoin(segment, 'bevel');
} else {
bounds = bounds.include(corner);
add(corner);
}
break;
}
@ -418,8 +443,8 @@ var Path = this.Path = PathItem.extend({
// direction of the tangent, which is the rotated normal
if (cap == 'square')
point = point.add(normal.y, -normal.x);
bounds = bounds.include(point.add(normal));
bounds = bounds.include(point.subtract(normal));
add(point.add(normal));
add(point.subtract(normal));
break;
}
}

View file

@ -137,5 +137,63 @@ var Segment = this.Segment = Base.extend({
+ (this._handleOut.isZero()
? ', handleOut: ' + this._handleOut : '')
+ ' }';
},
_transformCoordinates: function(matrix, coords, change) {
// Use matrix.transform version() that takes arrays of multiple
// points for largely improved performance, as no calls to
// Point.read() and Point constructors are necessary.
var point = this._point,
// If a matrix is defined, only transform handles if they are set.
// This saves some computation time. If no matrix is set, always
// use the real handles, as we just want to receive a filled
// coords array for _calculateBounds().
handleIn = matrix && this.getHandleInIfSet() || this._handleIn,
handleOut = matrix && this.getHandleOutIfSet() || this._handleOut,
x = point.x,
y = point.y;
coords[0] = x;
coords[1] = y;
var index = 2;
// We need to convert handles to absolute coordinates in order
// to transform them.
if (handleIn) {
coords[index++] = handleIn.x + x;
coords[index++] = handleIn.y + y;
}
if (handleOut) {
coords[index++] = handleOut.x + x;
coords[index++] = handleOut.y + y;
}
if (matrix) {
matrix.transform(coords, 0, coords, 0, index / 2);
x = coords[0];
y = coords[1];
if (change) {
// If change is true, we need to set the new values back
point.x = x;
point.y = y;
index = 2;
if (handleIn) {
handleIn.x = coords[index++] - x;
handleIn.y = coords[index++] - y;
}
if (handleOut) {
handleOut.x = coords[index++] - x;
handleOut.y = coords[index++] - y;
}
} else {
// We want to receive the results in coords, so make sure
// handleIn and out are defined too, even if they're 0
if (!handleIn) {
coords[index++] = x;
coords[index++] = y;
}
if (!handleOut) {
coords[index++] = x;
coords[index++] = y;
}
}
}
}
});

View file

@ -18,13 +18,13 @@ test('placedSymbol bounds', function() {
new Rectangle(-50.5, -50.5, 101, 101),
'PlacedSymbol initial bounds.');
placedSymbol.scale(0.5);
placedSymbol.scale(1, 0.5);
compareRectangles(placedSymbol.bounds,
{ x: -25.25, y: -25.25, width: 50.5, height: 50.5 },
{ x: -50.5, y: -25.25, width: 101, height: 50.5 },
'Bounds after scale.');
placedSymbol.rotate(40);
compareRectangles(placedSymbol.bounds,
{ x: -25.50049, y: -25.50049, width: 51.00098, height: 51.00098 },
{ x: -42.04736, y: -37.91846, width: 84.09473, height: 75.83691 },
'Bounds after rotation.');
});