From 87981efeb5ac9ec3e3c273fce592ffeec76d57a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrg=20Lehni?= Date: Sun, 6 Mar 2011 21:26:38 +0000 Subject: [PATCH] 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. --- src/item/PlacedSymbol.js | 15 ++-- src/path/Path.js | 161 +++++++++++++++++++++---------------- src/path/Segment.js | 58 +++++++++++++ test/tests/PlacedSymbol.js | 8 +- 4 files changed, 160 insertions(+), 82 deletions(-) diff --git a/src/item/PlacedSymbol.js b/src/item/PlacedSymbol.js index 1bea7c0e..0b5e1e19 100644 --- a/src/item/PlacedSymbol.js +++ b/src/item/PlacedSymbol.js @@ -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) { diff --git a/src/path/Path.js b/src/path/Path.js index 5dea700e..f99a57a8 100644 --- a/src/path/Path.js +++ b/src/path/Path.js @@ -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; } } diff --git a/src/path/Segment.js b/src/path/Segment.js index 0f1c0bbb..065ef089 100644 --- a/src/path/Segment.js +++ b/src/path/Segment.js @@ -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; + } + } + } } }); diff --git a/test/tests/PlacedSymbol.js b/test/tests/PlacedSymbol.js index a6aa3ab3..b1c0a254 100644 --- a/test/tests/PlacedSymbol.js +++ b/test/tests/PlacedSymbol.js @@ -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.'); });