//////////////////////////////////////////////////////////////////////////////// // // ADOBE SYSTEMS INCORPORATED // Copyright 2008 Adobe Systems Incorporated // All Rights Reserved. // // NOTICE: Adobe permits you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// package spark.primitives { import flash.display.Graphics; import flash.display.GraphicsPath; import flash.geom.Matrix; import flash.geom.Point; import flash.geom.Rectangle; import mx.core.mx_internal; import mx.events.PropertyChangeEvent; import mx.graphics.IStroke; import mx.utils.MatrixUtil; import spark.primitives.supportClasses.FilledElement; use namespace mx_internal; /** * The Path class is a filled graphic element that draws a series of path segments. * In vector graphics, a path is a series of points connected by straight or curved line segments. * Together the lines form an image. In Flex, you use the Path class to define a complex vector shape * constructed from a set of line segments. * *

Typically, the first element of a path definition is a Move segment to specify the starting pen * position of the graphic. You then use the Line, CubicBezier and QuadraticBezier segments to * draw the lines of the graphic. When using these classes, you only specify the x and y coordinates * of the end point of the line; the x and y coordinate of the starting point is defined by the current * pen position.

* *

After drawing a line segment, the current pen position becomes the x and y coordinates of the end * point of the line. You can use multiple Move segments in the path definition to * reposition the pen.

* *

The syntax used by the Path class to define the shape is the same as the SVG path syntax, * which makes it easy to convert SVG paths to Flex paths.

* * @includeExample examples/ArrowExample.mxml * * @langversion 3.0 * @playerversion Flash 10 * @playerversion AIR 1.5 * @productversion Flex 4 */ public class Path extends FilledElement { include "../core/Version.as"; //-------------------------------------------------------------------------- // // Constructor // //-------------------------------------------------------------------------- /** * Constructor. * * @langversion 3.0 * @playerversion Flash 10 * @playerversion AIR 1.5 * @productversion Flex 4 */ public function Path() { super(); } //-------------------------------------------------------------------------- // // Variables // //-------------------------------------------------------------------------- /** * Dirty flag to indicate when path data has changed. * * @langversion 3.0 * @playerversion Flash 10 * @playerversion AIR 1.5 * @productversion Flex 4 */ private var graphicsPathChanged:Boolean = true; /** * Private data structure to hold the parsed * path segment information * * @langversion 3.0 * @playerversion Flash 10 * @playerversion AIR 1.5 * @productversion Flex 4 */ private var segments:PathSegmentsCollection; /** * A GraphicsPath object that contains the drawing * commands to draw this Path. * * The data commands expressed in a Path's data * property are translated into drawing commands and * coordinate parameters for those commands, and then * drawn to screen. */ mx_internal var graphicsPath:GraphicsPath = new GraphicsPath(new Vector.(), new Vector.()); //-------------------------------------------------------------------------- // // Properties // //-------------------------------------------------------------------------- //---------------------------------- // data //---------------------------------- private var _data:String; [Inspectable(category="General")] /** * A string containing a compact represention of the path segments. This is an alternate * way of setting the segments property. Setting this property overrides any values * stored in the segments array property. * *

The value is a space-delimited string describing each path segment. Each * segment entry has a single character which denotes the segment type and * two or more segment parameters.

* *

If the segment command is upper-case, the parameters are absolute values. * If the segment command is lower-case, the parameters are relative values.

* *

The following table shows the syntax for the segments: * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
Segment TypeCommandParametersExample
MoveM/mx yM 10 20 - Move line to 10, 20.
LineL/lx yL 50 30 - Line to 50, 30.
Horizontal lineH/hxH 40 = Horizontal line to 40.
Vertical lineV/vyV 100 - Vertical line to 100.
QuadraticBezierQ/qcontrolX controlY x yQ 110 45 90 30 - Curve to 90, 30 with the control point at 110, 45.
CubicBezierC/ccontrol1X control1Y control2X control2Y x yC 45 50 20 30 10 20 - Curve to 10, 20 with the first control point at 45, 50 and the second control point at 20, 30.
Close pathZ/zn/aCloses off the path.
*

* * @default null * * @langversion 3.0 * @playerversion Flash 10 * @playerversion AIR 1.5 * @productversion Flex 4 */ public function set data(value:String):void { if (_data == value) return; segments = new PathSegmentsCollection(value); graphicsPathChanged = true; // Clear our cached measurement and data values clearCachedBoundingBoxWithStroke(); invalidateSize(); invalidateDisplayList(); _data = value; } /** * @private */ public function get data():String { return _data; } //---------------------------------- // winding //---------------------------------- private var _winding:String = "evenOdd"; /** * Fill rule for intersecting or overlapping path segments. * Possible values are GraphicsPathWinding.EVEN_ODD or GraphicsPathWinding.NON_ZERO. * * @default evenOdd * * @see flash.display.GraphicsPathWinding * * @langversion 3.0 * @playerversion Flash 10 * @playerversion AIR 1.5 * @productversion Flex 4 */ public function set winding(value:String):void { if (_winding != value) { _winding = value; graphicsPathChanged = true; invalidateDisplayList(); } } /** * @private */ public function get winding():String { return _winding; } //---------------------------------- // bounds //---------------------------------- private function getBounds():Rectangle { return segments ? segments.getBounds() : new Rectangle(); } //-------------------------------------------------------------------------- // // Overridden methods // //-------------------------------------------------------------------------- /** * @inheritDoc * * @langversion 3.0 * @playerversion Flash 10 * @playerversion AIR 1.5 * @productversion Flex 4 */ override protected function measure():void { var bounds:Rectangle = getBounds(); measuredWidth = bounds.width; measuredHeight = bounds.height; measuredX = bounds.left; measuredY = bounds.top; } /** * @private * Storage for the cached bounding box for particular transformation and size. * We are also caching the bounding box original x & y, so that we can reuse it * when bounds are requested with same size and transform that only differs by offsets. */ private var _boundingBoxCached:Rectangle; private var _boundingBoxMatrixCached:Matrix; private var _boundingBoxWidthParamCached:Number; private var _boundingBoxHeightParamCached:Number; private var _boundingBoxX:Number; private var _boundingBoxY:Number; /** * @private * Returns the bounding box for the path including stroke, if the path is resized * to the specified size and transformed with "m". * Pass null for "m" to specify identity matrix. * * Calling this method multiple times with the same parameters, or differences * only in the matrix offset is fast, as it returns the cached bounding box. * * Don't modify directly the return value! */ private function getBoundingBoxWithStroke(width:Number, height:Number, m:Matrix):Rectangle { if (_boundingBoxCached && _boundingBoxWidthParamCached == width && _boundingBoxHeightParamCached == height) { // Compare matrices: if (!m && !_boundingBoxMatrixCached) { _boundingBoxCached.x = _boundingBoxX; _boundingBoxCached.y = _boundingBoxY; return _boundingBoxCached; } else if (m && _boundingBoxMatrixCached && m.a == _boundingBoxMatrixCached.a && m.b == _boundingBoxMatrixCached.b && m.c == _boundingBoxMatrixCached.c && m.d == _boundingBoxMatrixCached.d) { _boundingBoxCached.x = _boundingBoxX + m.tx; _boundingBoxCached.y = _boundingBoxY + m.ty; return _boundingBoxCached; } } // Setup the matrix, ignore tx & ty, we'll account for it later if (m) { _boundingBoxMatrixCached = m.clone(); _boundingBoxMatrixCached.tx = 0; _boundingBoxMatrixCached.ty = 0; } else _boundingBoxMatrixCached = null; // Remember width and height _boundingBoxWidthParamCached = width; _boundingBoxHeightParamCached = height; _boundingBoxCached = computeBoundsWithStroke(_boundingBoxWidthParamCached, _boundingBoxHeightParamCached, m); // Remember the original x & y: _boundingBoxX = _boundingBoxCached.x - (m ? m.tx : 0); _boundingBoxY = _boundingBoxCached.y - (m ? m.ty : 0); return _boundingBoxCached; // No need to return clone, as this is for internal use only } /** * @private * Static storage for intermediate calculations while calculating miter-limit bounds. */ static private var tangent:Point = new Point(); /** * @private * Returns true when we have a valid tangent for curSegment. Pass prevSegment * to know what the starting point of curSegment is. */ private function tangentIsValid(prevSegment:PathSegment, curSegment:PathSegment, sx:Number, sy:Number, m:Matrix):Boolean { // TODO (egeorgie): optimize, we don't need to compute the tangent, // but just make sure the segment is not collapsed into a single point? // Check the start tangent only. If it's valid, // then there is a valid end tangent as well. curSegment.getTangent(prevSegment, true, sx, sy, m, tangent); return (tangent.x != 0 || tangent.y != 0); } /** * @private * @return Returns the axis aligned bounding box of the path when * resized to (width, height) and then transformed by matrix m. */ mx_internal function computeBoundsWithStroke(width:Number, height:Number, m:Matrix):Rectangle { var naturalBounds:Rectangle = getBounds(); var sx:Number = naturalBounds.width == 0 ? 1 : width / naturalBounds.width; var sy:Number = naturalBounds.height == 0 ? 1 : height / naturalBounds.height; // First, figure out the bounding box without stroke var pathBBox:Rectangle; // Special case, if there's no transformation or only offset, // then the non-stroked path bounds for the give size can be // scaled from the pre-transform natural bounds: if (!m || MatrixUtil.isDeltaIdentity(m) || !this.segments) { pathBBox = new Rectangle(naturalBounds.x * sx, naturalBounds.y * sy, naturalBounds.width * sx, naturalBounds.height * sy); if (m) pathBBox.offset(m.tx, m.ty); } else { pathBBox = this.segments.getBoundingBox(width, height, m); } // Do we have stroke? var strokeSettings:IStroke = this.stroke; if (!strokeSettings || !this.segments) return pathBBox; // Always add half the stroke weight, even for miter limit paths, // as a point on a curve and not necessarily a joint tip could be // an extreme that pushes the bounds. var strokeExtents:Rectangle = getStrokeExtents(); pathBBox.inflate(strokeExtents.right, strokeExtents.bottom); var seg:Vector. = segments.data; if (strokeSettings.joints != "miter" || seg.length < 2) { // TODO (egeorgie): Will overshoot for "bevel" // by the assumed roundness of the joints. return pathBBox; } // Use strokeExtents to get the transformed stroke weight. var halfWeight:Number = strokeExtents.width / 2; // Miter limit is always at least 1 var miterLimit:Number = Math.max(1, strokeSettings.miterLimit); var count:int = seg.length; var start:int = 0; var end:int; var lastMoveX:Number = 0; var lastMoveY:Number = 0; var lastOpenSegment:int = 0; while (true) { // Find a segment with a valid tangent or stop at a MoveSegment while (start < count && !(seg[start] is MoveSegment)) { var prevSegment:PathSegment = start > 0 ? seg[start - 1] : null; if (tangentIsValid(prevSegment, seg[start], sx, sy, m)) break; start++; } if (start >= count) break; // No more segments with valid tangents var startSegment:PathSegment = seg[start]; if (startSegment is MoveSegment) { // remember the last move segment lastOpenSegment = start + 1; lastMoveX = startSegment.x; lastMoveY = startSegment.y; // move onto next segment: start++; continue; } // Does the current segment close to a previous segment and form a joint with it? // Note, even if the segment was originally a close segment, it may not form a joint // with the segment it closes to, unless it's followed by a MoveSegment or it's the last // segment in the sequence. if ((start == count - 1 || seg[start + 1] is MoveSegment) && startSegment.x == lastMoveX && startSegment.y == lastMoveY) { end = lastOpenSegment; } else end = start + 1; // Find a segment with a valid tangent or stop at a MoveSegment while (end < count && !(seg[end] is MoveSegment)) { if (tangentIsValid(startSegment, seg[end], sx, sy, m)) break; end++; } if (end >= count) break; // No more segments with valid tangents var endSegment:PathSegment = seg[end]; if (!(endSegment is MoveSegment)) { addMiterLimitStrokeToBounds(start > 0 ? seg[start - 1] : null, startSegment, endSegment, miterLimit, halfWeight, sx, sy, m, pathBBox); } // Move on to the next segment, but never go back. End could be less // than start, because of implicit/explicit CloseSegments. start = start > end ? start + 1 : end; } return pathBBox; } /** * @private * Returns the bounds of the element, including stroke in local coordinates. */ override protected function getStrokeBounds():Rectangle { return getBoundingBoxWithStroke(width, height, null); } /** * @private */ override protected function get needsDisplayObject():Boolean { // Rendering with miter limit into the same DisplayObject will cause the // slow code-path Player execution for all graphics in that DisplayObject. // Make sure that we don't share the DisplayObject with other elements when // we have stroke with miter joints. return super.needsDisplayObject || (stroke && stroke.joints == "miter"); } /** * @private * Calculates the miter stroke contribution to the "result" bounding box. * * @param segment0 The segment before the first segment of the joint. * @param segment1 The first segment of the joint. * @param segment2 The second segment of the joint. * * @param miterLimit The miter limit. It must be at least 1. * * @param weight The transformed stroke weight (the outside part only, * if stroke is centered, this must be weight/2). * * @param sx The pre-transform horizontal scale factor for the segments. * @param sy The pre-transform vertical scale factor for the segments. * * @param m The transformation for the segments. * * @param result The bounding box to be accumulating the bounds. */ private function addMiterLimitStrokeToBounds(segment0:PathSegment, segment1:PathSegment, segment2:PathSegment, miterLimit:Number, weight:Number, sx:Number, sy:Number, m:Matrix, result:Rectangle):void { // The tip of the joint var pt:Point; pt = MatrixUtil.transformPoint(segment1.x * sx, segment1.y * sy, m).clone(); var jointX:Number = pt.x; var jointY:Number = pt.y; // End tangent for segment1: var t0:Point = new Point(); segment1.getTangent(segment0, false /*start*/, sx, sy, m, t0); // Start tangent for segment2: var t1:Point = new Point(); segment2.getTangent(segment1, true /*start*/, sx, sy, m, t1); // Valid tangents? if (t0.length == 0 || t1.length == 0) return; // The tip of the stroke lies on the bisector of the angle and lies at a distance // of weight / sin(A/2), where A is the angle between the tangents. // Quick and dirty way to calculate it, make the tangents unit length: t0.normalize(1); t0.x = -t0.x; t0.y = -t0.y; t1.normalize(1); // Find the vector from t0 to the midPoint from t0 to t1 var halfT0T1:Point = new Point( (t1.x - t0.x) * 0.5, (t1.y - t0.y) * 0.5); // sin(A/2) == halfT0T1.length / t1.length() var sinHalfAlpha:Number = halfT0T1.length; if (Math.abs(sinHalfAlpha) < 1.0E-9) { // Don't count degenerate joints that are close to 0 degrees so // we avoid cases like this one L 0 0 0 50 100 0 30 0 50 0 Z return; } // Find the vector of the bisect var bisect:Point = new Point( -0.5 * (t0.x + t1.x), -0.5 * (t0.y + t1.y) ); if (bisect.length == 0) { // 180 degrees, nothing to contribute return; } // Is there miter limit at play? if (sinHalfAlpha == 0 || miterLimit < 1 / sinHalfAlpha) { // The miter limit is reached. Calculate two extra points that may // contribute to the bounds. // The points lie on the line perpendicular to the bisect and intersecting // it at offset of miterLimit * weight from the joint tip. // The points are equally offset from the bisect by a factor of X, // where X / sinAlpha == (weight / sinAlpha - miterLimit * weight) / bisect.lenght. var bisectLength:Number = bisect.length; bisect.normalize(1); halfT0T1.normalize((weight - miterLimit * weight * sinHalfAlpha) / bisectLength); var pt0:Point = new Point(jointX + miterLimit * weight * bisect.x + halfT0T1.x, jointY + miterLimit * weight * bisect.y + halfT0T1.y); var pt1:Point = new Point(jointX + miterLimit * weight * bisect.x - halfT0T1.x, jointY + miterLimit * weight * bisect.y - halfT0T1.y); // Add it to the rectangle: MatrixUtil.rectUnion(pt0.x, pt0.y, pt0.x, pt0.y, result); MatrixUtil.rectUnion(pt1.x, pt1.y, pt1.x, pt1.y, result); } else { // miter limit is not reached, add the tip of the stroke bisect.normalize(1); var strokeTip:Point = new Point(jointX + bisect.x * weight / sinHalfAlpha, jointY + bisect.y * weight / sinHalfAlpha); // Add it to the rectangle: MatrixUtil.rectUnion(strokeTip.x, strokeTip.y, strokeTip.x, strokeTip.y, result); } } /** * @private */ override protected function transformWidthForLayout(width:Number, height:Number, postLayoutTransform:Boolean = true):Number { var m:Matrix = getComplexMatrix(postLayoutTransform); // Optimize for no-stroke, no transform if (!m && !stroke) return width; return getBoundingBoxWithStroke(width, height, m).width; } /** * @private */ override protected function transformHeightForLayout(width:Number, height:Number, postLayoutTransform:Boolean = true):Number { var m:Matrix = getComplexMatrix(postLayoutTransform); // Optimize for no-stroke, no transform if (!m && !stroke) return height; return getBoundingBoxWithStroke(width, height, m).height; } /** * @private * A helper function to implement the getBoundsXAtSize() and getBoundsYAtSize() * mehtods. Calculates where the top-left corner of the bounds would end up * if the path is resized to the specified size. */ private function getBoundsAtSize(width:Number, height:Number, m:Matrix):Rectangle { var strokeExtents:Rectangle = null; if (!isNaN(width)) { strokeExtents = getStrokeExtents(true /*postLayoutTransform*/); width -= strokeExtents.width; } if (!isNaN(height)) { if (!strokeExtents) strokeExtents = getStrokeExtents(true /*postLayoutTransform*/); height -= strokeExtents.height; } var newWidth:Number = preferredWidthPreTransform(); var newHeight:Number = preferredHeightPreTransform(); // Calculate the width and height pre-transform: if (m) { var newSize:Point = MatrixUtil.fitBounds(width, height, m, explicitWidth, explicitHeight, newWidth, newHeight, minWidth, minHeight, maxWidth, maxHeight); if (newSize) { newWidth = newSize.x; newHeight = newSize.y; } else { newWidth = minWidth; newHeight = minHeight; } } return getBoundingBoxWithStroke(newWidth, newHeight, m); } /** * @inheritDoc * * @langversion 3.0 * @playerversion Flash 10 * @playerversion AIR 1.5 * @productversion Flex 4 */ override public function getBoundsXAtSize(width:Number, height:Number, postLayoutTransform:Boolean = true):Number { var m:Matrix = getComplexMatrix(postLayoutTransform); return getBoundsAtSize(width, height, m).x + (m ? 0 : this.x); } /** * @inheritDoc * * @langversion 3.0 * @playerversion Flash 10 * @playerversion AIR 1.5 * @productversion Flex 4 */ override public function getBoundsYAtSize(width:Number, height:Number, postLayoutTransform:Boolean = true):Number { var m:Matrix = getComplexMatrix(postLayoutTransform); return getBoundsAtSize(width, height, m).y + (m ? 0 : this.y); } /** * @private */ override public function getLayoutBoundsX(postLayoutTransform:Boolean = true):Number { var m:Matrix = getComplexMatrix(postLayoutTransform); // Optimize for no-stroke, no transform if (!m && !stroke) { if (measuredX == 0) return this.x; var naturalBounds:Rectangle = getBounds(); var sx:Number = (naturalBounds.width == 0 || _width == 0) ? 1 : _width / naturalBounds.width; return this.x + measuredX * sx; } return getBoundingBoxWithStroke(_width, _height, m).x + (m ? 0 : this.x); } /** * @private */ override public function getLayoutBoundsY(postLayoutTransform:Boolean = true):Number { var m:Matrix = getComplexMatrix(postLayoutTransform); // Optimize for no-stroke, no transform if (!m && !stroke) { if (measuredY == 0) return this.y; var naturalBounds:Rectangle = getBounds(); var sy:Number = (naturalBounds.height == 0 || _height == 0) ? 1 : _height / naturalBounds.height; return this.y + measuredY * sy; } return getBoundingBoxWithStroke(_width, _height, m).y + (m ? 0 : this.y); } /** * @private */ private function setLayoutBoundsTransformed(width:Number, height:Number, m:Matrix):void { var size:Point = fitLayoutBoundsIterative(width, height, m); // We couldn't find a solution, try to relax the constraints if (!size && !isNaN(width) && !isNaN(height)) { // Try without width constraint var size1:Point = fitLayoutBoundsIterative(NaN, height, m); // Try without height constraint var size2:Point = fitLayoutBoundsIterative(width, NaN, m); // Ignore solutions that will exceeed the requested size if (size1 && getBoundingBoxWithStroke(size1.x, size1.y, m).width > width) size1 = null; if (size2 && getBoundingBoxWithStroke(size2.x, size2.y, m).height > height) size2 = null; // Which size was better? if (size1 && size2) { // Pick the one closest to the natural bounds ratio: // ? abs(n.x / n.y - size1.x / size1.y) < abs(n.x / n.y - size2.x / size2.y) // <=> abs((n.x * size1.y - n.y * size1.x) / (n.y * size1.y)) < abs((n.x * size2.y - n.y * size2.x) / (n.y * size2.y)) // <=> abs((n.x * size1.y - n.y * size1.x)) / (n.y * size1.y) < abs((n.x * size2.y - n.y * size2.x)) / (n.y * size2.y) // <=> abs(n.x * size1.y - n.y * size1.x) / size1.y < abs(n.x * size2.y - n.y * size2.x) / size2.y // <=> abs(n.x * size1.y - n.y * size1.x) * size2.y < abs(n.x * size2.y - n.y * size2.x) * size1.y var n:Point = getBounds().size; var pickSize1:Boolean = Math.abs( n.x * size1.y - n.y * size1.x ) * size2.y < Math.abs( n.x * size2.y - n.y * size2.x ) * size1.y; if (pickSize1) size = size1; else size = size2; } else if (size1) { size = size1; } else { size = size2; } } if (size) setActualSize(size.x, size.y); else setActualSize(minWidth, minHeight); } /** * Iteratively approach a solution. Returns 0 if no exact solution exists. * NaN values for width/height mean "not constrained" in that dimesion. * * @private */ private function fitLayoutBoundsIterative(width:Number, height:Number, m:Matrix):Point { // Start from the natural bounds //var naturalBounds:Rectangle = getBounds(); var newWidth:Number = this.preferredWidthPreTransform(); var newHeight:Number = this.preferredHeightPreTransform(); var fitWidth:Number = MatrixUtil.transformBounds(newWidth, newHeight, m).x; var fitHeight:Number = MatrixUtil.transformBounds(newWidth, newHeight, m).y; if (isNaN(width)) fitWidth = NaN; if (isNaN(height)) fitHeight = NaN; var i:int = 0; while (i++ < 150) { // Get current post-transform bounds var transformedBounds:Rectangle = getBoundingBoxWithStroke(newWidth, newHeight, m); var widthDistance:Number = isNaN(width) ? 0 : width - transformedBounds.width; var heightDistance:Number = isNaN(height) ? 0 : height - transformedBounds.height; if (Math.abs(widthDistance) < 0.1 && Math.abs(heightDistance) < 0.1) { return new Point(newWidth, newHeight); } // Try to fit bounds plus difference between fitWidth += widthDistance * 0.5; fitHeight += heightDistance * 0.5; var newSize:Point = MatrixUtil.fitBounds(fitWidth, fitHeight, m, explicitWidth, explicitHeight, preferredWidthPreTransform(), preferredHeightPreTransform(), minWidth, minHeight, maxWidth, maxHeight); if (!newSize) break; newWidth = newSize.x; newHeight = newSize.y; } return null; } /** * @private */ override public function setLayoutBoundsSize(width:Number, height:Number, postLayoutTransform:Boolean=true):void { // We have special handling for miter-limit stroked non-transformed paths, // Otherwise we default to the generic GraphicElement sizing algorithm. if (isNaN(width) && isNaN(height)) // resetting to original size? { super.setLayoutBoundsSize(width, height, postLayoutTransform); return; } // Resize transformed path with the iterative solution var m:Matrix = getComplexMatrix(postLayoutTransform); if (m) { setLayoutBoundsTransformed(width, height, m); return; } if (!stroke || stroke.joints != "miter") // no stroke or no miter joints? { super.setLayoutBoundsSize(width, height, postLayoutTransform); return; } // Miter limit requires special handling since the stroke extents depend on // the path size as the size changes the angles of the joints. var newWidth:Number = preferredWidthPreTransform(); var newHeight:Number = preferredHeightPreTransform(); var bestWidth:Number; var bestHeight:Number; var bestScore:Number; // Special case if we are constrained in both dimensions if (!isNaN(width) && !isNaN(height)) { var size:Point = fitLayoutBoundsIterative(width, height, new Matrix()); if (size) { setActualSize(size.x, size.y); return; } // If we didn't find a solution, start from the natural bounds newWidth = getBounds().width; newHeight = getBounds().height; bestWidth = this.minWidth; bestHeight = this.minHeight; bestScore = (width - bestWidth) * (width - bestWidth) + (height - bestHeight) * (height - bestHeight); } var i:int = 0; while (i++ < 150) { var boundsWithStroke:Rectangle = getBoundingBoxWithStroke(newWidth, newHeight, null); var widthProximity:Number = 0; var heightProximity:Number = 0; if (!isNaN(width)) widthProximity = Math.abs(width - boundsWithStroke.width); if (!isNaN(height)) heightProximity = Math.abs(height - boundsWithStroke.height); if (!isNaN(width) && !isNaN(height)) { var score:Number = (width - boundsWithStroke.width) * (width - boundsWithStroke.width) + (height - boundsWithStroke.height) * (height - boundsWithStroke.height); if (!isNaN(score) && score < bestScore && boundsWithStroke.width <= width && boundsWithStroke.height <= height) { bestScore = score; bestWidth = newWidth; bestHeight = newHeight; } } // Are we close enough? We want sub-pixel difference if (widthProximity < 1e-5 && heightProximity < 1e-5) { setActualSize(newWidth, newHeight); return; } var boundsWithoutStroke:Rectangle = segments.getBoundingBox(newWidth, newHeight, null); var strokeWidth:Number = boundsWithStroke.width - boundsWithoutStroke.width; var strokeHeight:Number = boundsWithStroke.height - boundsWithoutStroke.height; if (!isNaN(width)) newWidth = width - strokeWidth; if (!isNaN(height)) newHeight = height - strokeHeight; } setActualSize(bestWidth, bestHeight); } /** * @private * Use measuredX and measuredY instead of drawX and drawY */ override protected function beginDraw(g:Graphics):void { // Don't call super.beginDraw() since it will also set up an // invisible fill. // Adjust the position by the internal scale factor var naturalBounds:Rectangle = getBounds(); var sx:Number = naturalBounds.width == 0 ? 1 : width / naturalBounds.width; var sy:Number = naturalBounds.height == 0 ? 1 : height / naturalBounds.height; var origin:Point = new Point(drawX, drawY); var bounds:Rectangle = new Rectangle( drawX + measuredX * sx, drawY + measuredY * sy, width, height); if (stroke) { var strokeBounds:Rectangle = getStrokeBounds(); // Objects drawn in shared display objects are drawn at x,y rather // than 0,0 so need to move the strokeBounds if sharing. strokeBounds.offsetPoint(origin); stroke.apply(g, strokeBounds, origin); } else { g.lineStyle(); } if (fill) fill.begin(g, bounds, origin); } private var _drawBounds:Rectangle = new Rectangle(); /** * @inheritDoc * * @langversion 3.0 * @playerversion Flash 10 * @playerversion AIR 1.5 * @productversion Flex 4 */ override protected function draw(g:Graphics):void { if (drawX != _drawBounds.x || drawY != _drawBounds.y || width != _drawBounds.width || height != _drawBounds.height) { graphicsPathChanged = true; _drawBounds.x = drawX; _drawBounds.y = drawY; _drawBounds.width = width; _drawBounds.height = height; } if (graphicsPathChanged) { var rcBounds:Rectangle = getBounds(); var sx:Number = rcBounds.width == 0 ? 1 : width / rcBounds.width; var sy:Number = rcBounds.height == 0 ? 1 : height / rcBounds.height; if (segments) segments.generateGraphicsPath(graphicsPath, drawX, drawY, sx, sy); graphicsPathChanged = false; } g.drawPath(graphicsPath.commands, graphicsPath.data, winding); } /** * @inheritDoc * * @langversion 3.0 * @playerversion Flash 10 * @playerversion AIR 1.5 * @productversion Flex 4 */ override protected function endDraw(g:Graphics):void { // Set a transparent line style because filled, unclosed shapes will // automatically be closed by the Player. When they are, we want the line // to be invisible. g.lineStyle(); super.endDraw(g); } //-------------------------------------------------------------------------- // // Methods // //-------------------------------------------------------------------------- /** * @inheritDoc * * @langversion 3.0 * @playerversion Flash 10 * @playerversion AIR 1.5 * @productversion Flex 4 */ override protected function invalidateDisplayObjectSharing():void { graphicsPathChanged = true; super.invalidateDisplayObjectSharing(); } /** * @private */ private function clearCachedBoundingBoxWithStroke():void { _boundingBoxCached = null; _boundingBoxMatrixCached = null; } /** * @private * Make sure we clear the cached boundingBoxWithStroke */ override protected function stroke_propertyChangeHandler(event:PropertyChangeEvent):void { super.stroke_propertyChangeHandler(event); switch (event.property) { case "weight": case "scaleMode": case "joints": case "miterLimit": clearCachedBoundingBoxWithStroke(); // Parent layout takes stroke weight into account invalidateParentSizeAndDisplayList(); break; } } /** * @private * Make sure we clear the cached boundingBoxWithStroke */ override public function set stroke(value:IStroke):void { super.stroke = value; clearCachedBoundingBoxWithStroke(); } } } //-------------------------------------------------------------------------- // // Internal Helper Class - PathSegmentsCollection // //-------------------------------------------------------------------------- /** * Helper class that takes in a string and stores and generates a vector of * Path segments. * Provides methods for generating GraphicsPath and calculating bounds. */ class PathSegmentsCollection { //-------------------------------------------------------------------------- // // Constructor // //-------------------------------------------------------------------------- /** * Constructor. * * @param value * * @langversion 3.0 * @playerversion Flash 10 * @playerversion AIR 1.5 * @productversion Flex 4 */ public function PathSegmentsCollection(value:String) { if (!value) { _segments = new Vector.(); return; } var newSegments:Vector. = new Vector.(); var charCount:int = value.length; var c:Number; // current char code, String.charCodeAt() returns Number. var useRelative:Boolean; var prevIdentifier:Number = 0; var prevX:Number = 0; var prevY:Number = 0; var lastMoveX:Number = 0; var lastMoveY:Number = 0; var x:Number; var y:Number; var controlX:Number; var controlY:Number; var control2X:Number; var control2Y:Number; var lastMoveSegmentIndex:int = -1; _dataLength = charCount; _charPos = 0; while (true) { // Skip any whitespace or commas first skipWhiteSpace(value); // Are we done parsing? if (_charPos >= charCount) break; // Get the next character c = value.charCodeAt(_charPos++); // Is this a start of a number? // The RegExp for a float is /[+-]?\d*\.?\d+([Ee][+-]?\d+)?/ if ((c >= 0x30 && c < 0x3A) || // A digit (c == 0x2B || c == 0x2D) || // '+' & '-' (c == 0x2E)) // '.' { c = prevIdentifier; _charPos--; } else if (c >= 0x41 && c <= 0x56) // Between 'C' and 'V' useRelative = false; else if (c >= 0x61 && c <= 0x7A) // Between 'c' and 'v' useRelative = true; switch(c) { case 0x63: // c case 0x43: // C controlX = getNumber(useRelative, prevX, value); controlY = getNumber(useRelative, prevY, value); control2X = getNumber(useRelative, prevX, value); control2Y = getNumber(useRelative, prevY, value); x = getNumber(useRelative, prevX, value); y = getNumber(useRelative, prevY, value); newSegments.push(new CubicBezierSegment(controlX, controlY, control2X, control2Y, x, y)); prevX = x; prevY = y; prevIdentifier = 0x63; break; case 0x6D: // m case 0x4D: // M x = getNumber(useRelative, prevX, value); y = getNumber(useRelative, prevY, value); newSegments.push(new MoveSegment(x, y)); prevX = x; prevY = y; // If a moveto is followed by multiple pairs of coordinates, // the subsequent pairs are treated as implicit lineto commands. prevIdentifier = (c == 0x6D) ? 0x6C : 0x4C; // c == 'm' ? 'l' : 'L' // Fix for bug SDK-24457: // If the Quadratic segment is isolated, the Player // won't draw fill correctly. We need to generate // a dummy line segment. var curSegmentIndex:int = newSegments.length - 1; if (lastMoveSegmentIndex + 2 == curSegmentIndex && newSegments[lastMoveSegmentIndex + 1] is QuadraticBezierSegment) { // Insert a dummy LineSegment newSegments.splice(lastMoveSegmentIndex + 1, 0, new LineSegment(lastMoveX, lastMoveY)); curSegmentIndex++; } lastMoveSegmentIndex = curSegmentIndex; lastMoveX = x; lastMoveY = y; break; case 0x6C: // l case 0x4C: // L x = getNumber(useRelative, prevX, value); y = getNumber(useRelative, prevY, value); newSegments.push(new LineSegment(x, y)); prevX = x; prevY = y; prevIdentifier = 0x6C; break; case 0x68: // h case 0x48: // H x = getNumber(useRelative, prevX, value); y = prevY; newSegments.push(new LineSegment(x, y)); prevX = x; prevY = y; prevIdentifier = 0x68; break; case 0x76: // v case 0x56: // V x = prevX; y = getNumber(useRelative, prevY, value); newSegments.push(new LineSegment(x, y)); prevX = x; prevY = y; prevIdentifier = 0x76; break; case 0x71: // q case 0x51: // Q controlX = getNumber(useRelative, prevX, value); controlY = getNumber(useRelative, prevY, value); x = getNumber(useRelative, prevX, value); y = getNumber(useRelative, prevY, value); newSegments.push(new QuadraticBezierSegment(controlX, controlY, x, y)); prevX = x; prevY = y; prevIdentifier = 0x71; break; case 0x74: // t case 0x54: // T // control is a reflection of the previous control point if (prevIdentifier == 0x74 || prevIdentifier == 0x71) // 't' or 'q' { controlX = prevX + (prevX - controlX); controlY = prevY + (prevY - controlY); } else { controlX = prevX; controlY = prevY; } x = getNumber(useRelative, prevX, value); y = getNumber(useRelative, prevY, value); newSegments.push(new QuadraticBezierSegment(controlX, controlY, x, y)); prevX = x; prevY = y; prevIdentifier = 0x74; break; case 0x73: // s case 0x53: // S if (prevIdentifier == 0x73 || prevIdentifier == 0x63) // s or c { controlX = prevX + (prevX - control2X); controlY = prevY + (prevY - control2Y); } else { controlX = prevX; controlY = prevY; } control2X = getNumber(useRelative, prevX, value); control2Y = getNumber(useRelative, prevY, value); x = getNumber(useRelative, prevX, value); y = getNumber(useRelative, prevY, value); newSegments.push(new CubicBezierSegment(controlX, controlY, control2X, control2Y, x, y)); prevX = x; prevY = y; prevIdentifier = 0x73; break; case 0x7A: // z case 0x5A: // Z x = lastMoveX; y = lastMoveY; newSegments.push(new LineSegment(x, y)); prevX = x; prevY = y; prevIdentifier = 0x7A; break; default: // unknown identifier, throw error? _segments = new Vector.(); return; } } // Fix for bug SDK-24457: // If the Quadratic segment is isolated, the Player // won't draw fill correctly. We need to generate // a dummy line segment. curSegmentIndex = newSegments.length; if (lastMoveSegmentIndex + 2 == curSegmentIndex && newSegments[lastMoveSegmentIndex + 1] is QuadraticBezierSegment) { // Insert a dummy LineSegment newSegments.splice(lastMoveSegmentIndex + 1, 0, new LineSegment(lastMoveX, lastMoveY)); curSegmentIndex++; } _segments = newSegments; } //-------------------------------------------------------------------------- // // Properties // //-------------------------------------------------------------------------- //---------------------------------- // data //---------------------------------- private var _segments:Vector.; /** * A Vector of the actual path segments. May be empty, but always non-null. */ public function get data():Vector. { return _segments; } //---------------------------------- // bounds //---------------------------------- private var _bounds:Rectangle; /** * The bounds of the segments in local coordinates. */ public function getBounds():Rectangle { if (_bounds) return _bounds; // First, allocate temporary bounds, as getBoundingBox() requires // natual bounds to calculate a scaling factor _bounds = new Rectangle(0, 0, 1, 1); // Pass in the same size to getBoundingBox // so that the scaling factor is (1, 1). _bounds = getBoundingBox(1, 1, null /*Matrix*/); return _bounds; } //-------------------------------------------------------------------------- // // Methods // //-------------------------------------------------------------------------- /** * @return Returns the axis aligned bounding box of the segments stretched to * width, height and then transformed with transformation matrix m. */ public function getBoundingBox(width:Number, height:Number, m:Matrix):Rectangle { var naturalBounds:Rectangle = getBounds(); var sx:Number = naturalBounds.width == 0 ? 1 : width / naturalBounds.width; var sy:Number = naturalBounds.height == 0 ? 1 : height / naturalBounds.height; var prevSegment:PathSegment; var pathBBox:Rectangle; var count:int = _segments.length; for (var i:int = 0; i < count; i++) { var segment:PathSegment = _segments[i]; pathBBox = segment.getBoundingBox(prevSegment, sx, sy, m, pathBBox); prevSegment = segment; } // If path is empty, it's untransformed bounding box is (0,0), so we return transformed point (0,0) if (!pathBBox) { var x:Number = m ? m.tx : 0; var y:Number = m ? m.ty : 0; pathBBox = new Rectangle(x, y); } return pathBBox; } /** * Workhorse method that iterates through the segments * array and draws each path egment based on its control points. * * Segments are drawn from the x and y position of the path. * Additionally, segments are drawn by taking into account the scale * applied to the path. * * @param tx A Number representing the x position of where this * path segment should be drawn * * @param ty A Number representing the y position of where this * path segment should be drawn * * @param sx A Number representing the scaleX at which to draw * this path segment * * @param sy A Number representing the scaleY at which to draw this * path segment */ public function generateGraphicsPath(graphicsPath:GraphicsPath, tx:Number, ty:Number, sx:Number, sy:Number):void { graphicsPath.commands = null; graphicsPath.data = null; // Always start by moving to drawX, drawY. Otherwise // the path will begin at the previous pen location // if it does not start with a MoveSegment. graphicsPath.moveTo(tx, ty); var curSegment:PathSegment; var prevSegment:PathSegment; var count:int = _segments.length; for (var i:int = 0; i < count; i++) { prevSegment = curSegment; curSegment = _segments[i]; curSegment.draw(graphicsPath, tx, ty, sx, sy, prevSegment); } } //-------------------------------------------------------------------------- // // Private methods // //-------------------------------------------------------------------------- private var _charPos:int = 0; private var _dataLength:int = 0; private function skipWhiteSpace(data:String):void { while (_charPos < _dataLength) { var c:Number = data.charCodeAt(_charPos); if (c != 0x20 && // Space c != 0x2C && // Comma c != 0xD && // Carriage return c != 0x9 && // Tab c != 0xA) // New line { break; } _charPos++; } } private function getNumber(useRelative:Boolean, offset:Number, value:String):Number { // Parse the string and find the first occurrance of the following RexExp // numberRegExp:RegExp = /[+-]?\d*\.?\d+([Ee][+-]?\d+)?/g; skipWhiteSpace(value); // updates _charPos if (_charPos >= _dataLength) return NaN; // Remember the start of the number var numberStart:int = _charPos; var hasSignCharacter:Boolean = false; var hasDigits:Boolean = false; // The number could start with '+' or '-' (the "[+-]?" part of the RegExp) var c:Number = value.charCodeAt(_charPos); if (c == 0x2B || c == 0x2D) // '+' or '-' { hasSignCharacter = true; _charPos++; } // The index of the '.' if any var dotIndex:int = -1; // First sequence of digits and optional dot in between (the "\d*\.?\d+" part of the RegExp) while (_charPos < _dataLength) { c = value.charCodeAt(_charPos); if (c >= 0x30 && c < 0x3A) // A digit { hasDigits = true; } else if (c == 0x2E && dotIndex == -1) // '.' { dotIndex = _charPos; } else break; _charPos++; } // Now check whether we had at least one digit. if (!hasDigits) { // Go to the end of the data _charPos = _dataLength; return NaN; } // 1. Was the last character a '.'? If so, rewind one character back. if (c == 0x2E) _charPos--; // So far we have a valid number, remember its end character index var numberEnd:int = _charPos; // Check to see if we have scientific notation (the "([Ee][+-]?\d+)?" part of the RegExp) if (c == 0x45 || c == 0x65) { _charPos++; // Check for '+' or '-' if (_charPos < _dataLength) { c = value.charCodeAt(_charPos); if (c == 0x2B || c == 0x2D) _charPos++; } // Find all the digits var digitStart:int = _charPos; while (_charPos < _dataLength) { c = value.charCodeAt(_charPos); // Not a digit? if (!(c >= 0x30 && c < 0x3A)) { break; } _charPos++; } // Do we have at least one digit? if (digitStart < _charPos) numberEnd = _charPos; // Scientific notation, update the end index of the number. else _charPos = numberEnd; // No scientific notation, rewind back to the end index of the number. } // Use parseFloat to get the actual number. // TODO (egeorgie): we could build the number while matching the RegExp which will save the substr and parseFloat var subString:String = value.substr(numberStart, numberEnd - numberStart); var result:Number = parseFloat(subString); if (isNaN(result)) { // Go to the end of the data _charPos = _dataLength; return NaN; } _charPos = numberEnd; return useRelative ? result + offset : result; } } //-------------------------------------------------------------------------- // // Internal Helper Class - PathSegment // //-------------------------------------------------------------------------- import flash.display.GraphicsPath; import flash.geom.Matrix; import flash.geom.Rectangle; import mx.events.PropertyChangeEvent; /** * The PathSegment class is the base class for a segment of a path. * This class is not created directly. It is the base class for * MoveSegment, LineSegment, CubicBezierSegment and QuadraticBezierSegment. * * @langversion 3.0 * @playerversion Flash 10 * @playerversion AIR 1.5 * @productversion Flex 4 */ class PathSegment extends Object { //-------------------------------------------------------------------------- // // Constructor // //-------------------------------------------------------------------------- /** * Constructor. * * @param _x The x position of the pen in the current coordinate system. * * @param _y The y position of the pen in the current coordinate system. * * @langversion 3.0 * @playerversion Flash 10 * @playerversion AIR 1.5 * @productversion Flex 4 */ public function PathSegment(_x:Number = 0, _y:Number = 0) { super(); x = _x; y = _y; } //-------------------------------------------------------------------------- // // Properties // //-------------------------------------------------------------------------- //---------------------------------- // x //---------------------------------- /** * The ending x position for this segment. * * @default 0 * * @langversion 3.0 * @playerversion Flash 10 * @playerversion AIR 1.5 * @productversion Flex 4 */ public var x:Number = 0; //---------------------------------- // y //---------------------------------- /** * The ending y position for this segment. * * @default 0 * * @langversion 3.0 * @playerversion Flash 10 * @playerversion AIR 1.5 * @productversion Flex 4 */ public var y:Number = 0; //-------------------------------------------------------------------------- // // Methods // //-------------------------------------------------------------------------- /** * Draws this path segment. You can determine the current pen position by * reading the x and y values of the previous segment. * * @param g The graphics context to draw into. * @param prev The previous segment drawn, or null if this is the first segment. * * @langversion 3.0 * @playerversion Flash 10 * @playerversion AIR 1.5 * @productversion Flex 4 */ public function draw(graphicsPath:GraphicsPath, dx:Number,dy:Number,sx:Number,sy:Number,prev:PathSegment):void { // Override to draw your segment } /** * @param prev The previous segment drawn, or null if this is the first segment. * @param sx Pre-transform scale factor for x coordinates. * @param sy Pre-transform scale factor for y coordinates. * @param m Transformation matrix. * @param rect If non-null, rect is expanded to include the bounding box of the segment. * @return Returns the union of rect and the axis aligned bounding box of the post-transformed * path segment. * * @langversion 3.0 * @playerversion Flash 10 * @playerversion AIR 1.5 * @productversion Flex 4 */ public function getBoundingBox(prev:PathSegment, sx:Number, sy:Number, m:Matrix, rect:Rectangle):Rectangle { // Override to calculate your segment's bounding box. return rect; } /** * Returns the tangent for the segment. * @param prev The previous segment drawn, or null if this is the first segment. * @param start If true, returns the tangent to the start point, otherwise the tangend to the end point. * @param sx Pre-transform scale factor for x coordinates. * @param sy Pre-transform scale factor for y coordinates. * @param m Transformation matrix. * @param result The tangent is returned as vector (x, y) in result. */ public function getTangent(prev:PathSegment, start:Boolean, sx:Number, sy:Number, m:Matrix, result:Point):void { result.x = 0; result.y = 0; } } //-------------------------------------------------------------------------- // // Internal Helper Class - LineSegment // //-------------------------------------------------------------------------- import flash.display.GraphicsPath; import flash.geom.Matrix; import flash.geom.Point; import flash.geom.Rectangle; import mx.utils.MatrixUtil; /** * The LineSegment draws a line from the current pen position to the coordinate located at x, y. * * * @langversion 3.0 * @playerversion Flash 10 * @playerversion AIR 1.5 * @productversion Flex 4 */ class LineSegment extends PathSegment { //-------------------------------------------------------------------------- // // Constructor // //-------------------------------------------------------------------------- /** * Constructor. * * @param x The current location of the pen along the x axis. The draw() method uses * this value to determine where to draw to. * * @param y The current location of the pen along the y axis. The draw() method uses * this value to determine where to draw to. * * @langversion 3.0 * @playerversion Flash 10 * @playerversion AIR 1.5 * @productversion Flex 4 */ public function LineSegment(x:Number = 0, y:Number = 0) { super(x, y); } //-------------------------------------------------------------------------- // // Methods // //-------------------------------------------------------------------------- /** * @inheritDoc * * @langversion 3.0 * @playerversion Flash 10 * @playerversion AIR 1.5 * @productversion Flex 4 */ override public function draw(graphicsPath:GraphicsPath, dx:Number,dy:Number,sx:Number,sy:Number,prev:PathSegment):void { graphicsPath.lineTo(dx + x*sx, dy + y*sy); } /** * @inheritDoc * * @langversion 3.0 * @playerversion Flash 10 * @playerversion AIR 1.5 * @productversion Flex 4 */ override public function getBoundingBox(prev:PathSegment, sx:Number, sy:Number, m:Matrix, rect:Rectangle):Rectangle { pt = MatrixUtil.transformPoint(x * sx, y * sy, m); var x1:Number = pt.x; var y1:Number = pt.y; // If the previous segment actually draws, then only add the end point to the rectangle, // as the start point would have been added by the previous segment: if (prev != null && !(prev is MoveSegment)) return MatrixUtil.rectUnion(x1, y1, x1, y1, rect); var pt:Point = MatrixUtil.transformPoint(prev ? prev.x * sx : 0, prev ? prev.y * sy : 0, m); var x2:Number = pt.x; var y2:Number = pt.y; return MatrixUtil.rectUnion(Math.min(x1, x2), Math.min(y1, y2), Math.max(x1, x2), Math.max(y1, y2), rect); } /** * Returns the tangent for the segment. * @param prev The previous segment drawn, or null if this is the first segment. * @param start If true, returns the tangent to the start point, otherwise the tangend to the end point. * @param sx Pre-transform scale factor for x coordinates. * @param sy Pre-transform scale factor for y coordinates. * @param m Transformation matrix. * @param result The tangent is returned as vector (x, y) in result. */ override public function getTangent(prev:PathSegment, start:Boolean, sx:Number, sy:Number, m:Matrix, result:Point):void { var pt0:Point = MatrixUtil.transformPoint(prev ? prev.x * sx : 0, prev ? prev.y * sy : 0, m).clone(); var pt1:Point = MatrixUtil.transformPoint(x * sx, y * sy, m); result.x = pt1.x - pt0.x; result.y = pt1.y - pt0.y; } } //-------------------------------------------------------------------------- // // Internal Helper Class - MoveSegment // //-------------------------------------------------------------------------- import flash.display.GraphicsPath; /** * The MoveSegment moves the pen to the x,y position. This class calls the Graphics.moveTo() method * from the draw() method. * * * @see flash.display.Graphics * * @langversion 3.0 * @playerversion Flash 10 * @playerversion AIR 1.5 * @productversion Flex 4 */ class MoveSegment extends PathSegment { //-------------------------------------------------------------------------- // // Constructor // //-------------------------------------------------------------------------- /** * Constructor. * * @param x The target x-axis location in 2-d coordinate space. * * @param y The target y-axis location in 2-d coordinate space. * * @langversion 3.0 * @playerversion Flash 10 * @playerversion AIR 1.5 * @productversion Flex 4 */ public function MoveSegment(x:Number = 0, y:Number = 0) { super(x, y); } //-------------------------------------------------------------------------- // // Methods // //-------------------------------------------------------------------------- /** * @inheritDoc * * The MoveSegment class moves the pen to the position specified by the * x and y properties. * * @langversion 3.0 * @playerversion Flash 10 * @playerversion AIR 1.5 * @productversion Flex 4 */ override public function draw(graphicsPath:GraphicsPath, dx:Number,dy:Number,sx:Number,sy:Number,prev:PathSegment):void { graphicsPath.moveTo(dx+x*sx, dy+y*sy); } } //-------------------------------------------------------------------------- // // Internal Helper Class - CubicBezierSegment // //-------------------------------------------------------------------------- import flash.display.GraphicsPath; import flash.geom.Matrix; import flash.geom.Point; import flash.geom.Rectangle; import mx.utils.MatrixUtil; /** * The CubicBezierSegment draws a cubic bezier curve from the current pen position * to x, y. The control1X and control1Y properties specify the first control point; * the control2X and control2Y properties specify the second control point. * *

Cubic bezier curves are not natively supported in Flash Player. This class does * an approximation based on the fixed midpoint algorithm and uses 4 quadratic curves * to simulate a cubic curve.

* *

For details on the fixed midpoint algorithm, see:
* http://timotheegroleau.com/Flash/articles/cubic_bezier_in_flash.htm

* * * @langversion 3.0 * @playerversion Flash 10 * @playerversion AIR 1.5 * @productversion Flex 4 */ class CubicBezierSegment extends PathSegment { //-------------------------------------------------------------------------- // // Constructor // //-------------------------------------------------------------------------- /** * Constructor. * *

For a CubicBezierSegment, there are two control points, each with x and y coordinates. Control points * are points that define the direction and amount of curves of a Bezier curve. * The curved line never reaches the control points; however, the line curves as though being drawn * toward the control point.

* * @param _control1X The x-axis location in 2-d coordinate space of the first control point. * * @param _control1Y The y-axis location of the first control point. * * @param _control2X The x-axis location of the second control point. * * @param _control2Y The y-axis location of the second control point. * * @param x The x-axis location of the starting point of the curve. * * @param y The y-axis location of the starting point of the curve. * * * @langversion 3.0 * @playerversion Flash 10 * @playerversion AIR 1.5 * @productversion Flex 4 */ public function CubicBezierSegment( _control1X:Number = 0, _control1Y:Number = 0, _control2X:Number = 0, _control2Y:Number = 0, x:Number = 0, y:Number = 0) { super(x, y); control1X = _control1X; control1Y = _control1Y; control2X = _control2X; control2Y = _control2Y; } //-------------------------------------------------------------------------- // // Variables // //-------------------------------------------------------------------------- private var _qPts:QuadraticPoints; //-------------------------------------------------------------------------- // // Properties // //-------------------------------------------------------------------------- //---------------------------------- // control1X //---------------------------------- /** * The first control point x position. * * @langversion 3.0 * @playerversion Flash 10 * @playerversion AIR 1.5 * @productversion Flex 4 */ public var control1X:Number = 0; //---------------------------------- // control1Y //---------------------------------- /** * The first control point y position. * * @langversion 3.0 * @playerversion Flash 10 * @playerversion AIR 1.5 * @productversion Flex 4 */ public var control1Y:Number = 0; //---------------------------------- // control2X //---------------------------------- /** * The second control point x position. * * @langversion 3.0 * @playerversion Flash 10 * @playerversion AIR 1.5 * @productversion Flex 4 */ public var control2X:Number = 0; //---------------------------------- // control2Y //---------------------------------- /** * The second control point y position. * * @langversion 3.0 * @playerversion Flash 10 * @playerversion AIR 1.5 * @productversion Flex 4 */ public var control2Y:Number = 0; //-------------------------------------------------------------------------- // // Methods // //-------------------------------------------------------------------------- /** * Draws the segment. * * @param g The graphics context where the segment is drawn. * * @param prev The previous location of the pen. * * @langversion 3.0 * @playerversion Flash 10 * @playerversion AIR 1.5 * @productversion Flex 4 */ override public function draw(graphicsPath:GraphicsPath, dx:Number, dy:Number, sx:Number, sy:Number, prev:PathSegment):void { var qPts:QuadraticPoints = getQuadraticPoints(prev); graphicsPath.curveTo(dx + qPts.control1.x*sx, dy+qPts.control1.y*sy, dx+qPts.anchor1.x*sx, dy+qPts.anchor1.y*sy); graphicsPath.curveTo(dx + qPts.control2.x*sx, dy+qPts.control2.y*sy, dx+qPts.anchor2.x*sx, dy+qPts.anchor2.y*sy); graphicsPath.curveTo(dx + qPts.control3.x*sx, dy+qPts.control3.y*sy, dx+qPts.anchor3.x*sx, dy+qPts.anchor3.y*sy); graphicsPath.curveTo(dx + qPts.control4.x*sx, dy+qPts.control4.y*sy, dx+qPts.anchor4.x*sx, dy+qPts.anchor4.y*sy); } /** * @inheritDoc * * @langversion 3.0 * @playerversion Flash 10 * @playerversion AIR 1.5 * @productversion Flex 4 */ override public function getBoundingBox(prev:PathSegment, sx:Number, sy:Number, m:Matrix, rect:Rectangle):Rectangle { var qPts:QuadraticPoints = getQuadraticPoints(prev); rect = MatrixUtil.getQBezierSegmentBBox(prev ? prev.x : 0, prev ? prev.y : 0, qPts.control1.x, qPts.control1.y, qPts.anchor1.x, qPts.anchor1.y, sx, sy, m, rect); rect = MatrixUtil.getQBezierSegmentBBox(qPts.anchor1.x, qPts.anchor1.y, qPts.control2.x, qPts.control2.y, qPts.anchor2.x, qPts.anchor2.y, sx, sy, m, rect); rect = MatrixUtil.getQBezierSegmentBBox(qPts.anchor2.x, qPts.anchor2.y, qPts.control3.x, qPts.control3.y, qPts.anchor3.x, qPts.anchor3.y, sx, sy, m, rect); rect = MatrixUtil.getQBezierSegmentBBox(qPts.anchor3.x, qPts.anchor3.y, qPts.control4.x, qPts.control4.y, qPts.anchor4.x, qPts.anchor4.y, sx, sy, m, rect); return rect; } /** * Returns the tangent for the segment. * @param prev The previous segment drawn, or null if this is the first segment. * @param start If true, returns the tangent to the start point, otherwise the tangend to the end point. * @param sx Pre-transform scale factor for x coordinates. * @param sy Pre-transform scale factor for y coordinates. * @param m Transformation matrix. * @param result The tangent is returned as vector (x, y) in result. */ override public function getTangent(prev:PathSegment, start:Boolean, sx:Number, sy:Number, m:Matrix, result:Point):void { // Get the approximation (we want the tangents to be the same as the ones we use to draw var qPts:QuadraticPoints = getQuadraticPoints(prev); var pt0:Point = MatrixUtil.transformPoint(prev ? prev.x * sx : 0, prev ? prev.y * sy : 0, m).clone(); var pt1:Point = MatrixUtil.transformPoint(qPts.control1.x * sx, qPts.control1.y * sy, m).clone(); var pt2:Point = MatrixUtil.transformPoint(qPts.anchor1.x * sx, qPts.anchor1.y * sy, m).clone(); var pt3:Point = MatrixUtil.transformPoint(qPts.control2.x * sx, qPts.control2.y * sy, m).clone(); var pt4:Point = MatrixUtil.transformPoint(qPts.anchor2.x * sx, qPts.anchor2.y * sy, m).clone(); var pt5:Point = MatrixUtil.transformPoint(qPts.control3.x * sx, qPts.control3.y * sy, m).clone(); var pt6:Point = MatrixUtil.transformPoint(qPts.anchor3.x * sx, qPts.anchor3.y * sy, m).clone(); var pt7:Point = MatrixUtil.transformPoint(qPts.control4.x * sx, qPts.control4.y * sy, m).clone(); var pt8:Point = MatrixUtil.transformPoint(qPts.anchor4.x * sx, qPts.anchor4.y * sy, m).clone(); if (start) { QuadraticBezierSegment.getQTangent(pt0.x, pt0.y, pt1.x, pt1.y, pt2.x, pt2.y, start, result); // If there is no tangent if (result.x == 0 && result.y == 0) { // Try 3 & 4 QuadraticBezierSegment.getQTangent(pt0.x, pt0.y, pt3.x, pt3.y, pt4.x, pt4.y, start, result); // If there is no tangent if (result.x == 0 && result.y == 0) { // Try 5 & 6 QuadraticBezierSegment.getQTangent(pt0.x, pt0.y, pt5.x, pt5.y, pt6.x, pt6.y, start, result); // If there is no tangent if (result.x == 0 && result.y == 0) // Try 7 & 8 QuadraticBezierSegment.getQTangent(pt0.x, pt0.y, pt7.x, pt7.y, pt8.x, pt8.y, start, result); } } } else { QuadraticBezierSegment.getQTangent(pt6.x, pt6.y, pt7.x, pt7.y, pt8.x, pt8.y, start, result); // If there is no tangent if (result.x == 0 && result.y == 0) { // Try 4 & 5 QuadraticBezierSegment.getQTangent(pt4.x, pt4.y, pt5.x, pt5.y, pt8.x, pt8.y, start, result); // If there is no tangent if (result.x == 0 && result.y == 0) { // Try 2 & 3 QuadraticBezierSegment.getQTangent(pt2.x, pt2.y, pt3.x, pt3.y, pt8.x, pt8.y, start, result); // If there is no tangent if (result.x == 0 && result.y == 0) // Try 0 & 1 QuadraticBezierSegment.getQTangent(pt0.x, pt0.y, pt1.x, pt1.y, pt8.x, pt8.y, start, result); } } } } /** * @private * Tim Groleau's method to approximate a cubic bezier with 4 quadratic beziers, * with endpoint and control point of each saved. */ private function getQuadraticPoints(prev:PathSegment):QuadraticPoints { if (_qPts) return _qPts; var p1:Point = new Point(prev ? prev.x : 0, prev ? prev.y : 0); var p2:Point = new Point(x, y); var c1:Point = new Point(control1X, control1Y); var c2:Point = new Point(control2X, control2Y); // calculates the useful base points var PA:Point = Point.interpolate(c1, p1, 3/4); var PB:Point = Point.interpolate(c2, p2, 3/4); // get 1/16 of the [p2, p1] segment var dx:Number = (p2.x - p1.x) / 16; var dy:Number = (p2.y - p1.y) / 16; _qPts = new QuadraticPoints; // calculates control point 1 _qPts.control1 = Point.interpolate(c1, p1, 3/8); // calculates control point 2 _qPts.control2 = Point.interpolate(PB, PA, 3/8); _qPts.control2.x -= dx; _qPts.control2.y -= dy; // calculates control point 3 _qPts.control3 = Point.interpolate(PA, PB, 3/8); _qPts.control3.x += dx; _qPts.control3.y += dy; // calculates control point 4 _qPts.control4 = Point.interpolate(c2, p2, 3/8); // calculates the 3 anchor points _qPts.anchor1 = Point.interpolate(_qPts.control1, _qPts.control2, 0.5); _qPts.anchor2 = Point.interpolate(PA, PB, 0.5); _qPts.anchor3 = Point.interpolate(_qPts.control3, _qPts.control4, 0.5); // the 4th anchor point is p2 _qPts.anchor4 = p2; return _qPts; } } //-------------------------------------------------------------------------- // // Internal Helper Class - QuadraticPoints // //-------------------------------------------------------------------------- import flash.geom.Point; /** * Utility class to store the computed quadratic points. * * @langversion 3.0 * @playerversion Flash 10 * @playerversion AIR 1.5 * @productversion Flex 4 */ class QuadraticPoints { public var control1:Point; public var anchor1:Point; public var control2:Point; public var anchor2:Point; public var control3:Point; public var anchor3:Point; public var control4:Point; public var anchor4:Point; /** * Constructor. * * @langversion 3.0 * @playerversion Flash 10 * @playerversion AIR 1.5 * @productversion Flex 4 */ public function QuadraticPoints() { super(); } } //-------------------------------------------------------------------------- // // Internal Helper Class - QuadraticBezierSegment // //-------------------------------------------------------------------------- import flash.display.GraphicsPath; import flash.geom.Matrix; import flash.geom.Point; import flash.geom.Rectangle; import mx.utils.MatrixUtil; /** * The QuadraticBezierSegment draws a quadratic curve from the current pen position * to x, y. * * Quadratic bezier is the native curve type * in Flash Player. * * * @langversion 3.0 * @playerversion Flash 10 * @playerversion AIR 1.5 * @productversion Flex 4 */ class QuadraticBezierSegment extends PathSegment { //-------------------------------------------------------------------------- // // Constructor // //-------------------------------------------------------------------------- /** * Constructor. * *

For a QuadraticBezierSegment, there is one control point. A control point * is a point that defines the direction and amount of a Bezier curve. * The curved line never reaches the control point; however, the line curves as though being drawn * toward the control point.

* * @param _control1X The x-axis location in 2-d coordinate space of the control point. * * @param _control1Y The y-axis location in 2-d coordinate space of the control point. * * @param x The x-axis location of the starting point of the curve. * * @param y The y-axis location of the starting point of the curve. * * * @langversion 3.0 * @playerversion Flash 10 * @playerversion AIR 1.5 * @productversion Flex 4 */ public function QuadraticBezierSegment( _control1X:Number = 0, _control1Y:Number = 0, x:Number = 0, y:Number = 0) { super(x, y); control1X = _control1X; control1Y = _control1Y; } //-------------------------------------------------------------------------- // // Properties // //-------------------------------------------------------------------------- //---------------------------------- // control1X //---------------------------------- /** * The control point's x position. * * @langversion 3.0 * @playerversion Flash 10 * @playerversion AIR 1.5 * @productversion Flex 4 */ public var control1X:Number = 0; //---------------------------------- // control1Y //---------------------------------- /** * The control point's y position. * * @langversion 3.0 * @playerversion Flash 10 * @playerversion AIR 1.5 * @productversion Flex 4 */ public var control1Y:Number = 0; //-------------------------------------------------------------------------- // // Methods // //-------------------------------------------------------------------------- /** * Draws the segment using the control point location and the x and y coordinates. * This method calls the Graphics.curveTo() method. * * @see flash.display.Graphics * * @param g The graphics context where the segment is drawn. * * @param prev The previous location of the pen. * * @langversion 3.0 * @playerversion Flash 10 * @playerversion AIR 1.5 * @productversion Flex 4 */ override public function draw(graphicsPath:GraphicsPath, dx:Number,dy:Number,sx:Number,sy:Number,prev:PathSegment):void { graphicsPath.curveTo(dx+control1X*sx, dy+control1Y*sy, dx+x*sx, dy+y*sy); } static public function getQTangent(x0:Number, y0:Number, x1:Number, y1:Number, x2:Number, y2:Number, start:Boolean, result:Point):void { if (start) { if (x0 == x1 && y0 == y1) { result.x = x2 - x0; result.y = y2 - y0; } else { result.x = x1 - x0; result.y = y1 - y0; } } else { if (x2 == x1 && y2 == y1) { result.x = x2 - x0; result.y = y2 - y0; } else { result.x = x2 - x1; result.y = y2 - y1; } } } /** * Returns the tangent for the segment. * @param prev The previous segment drawn, or null if this is the first segment. * @param start If true, returns the tangent to the start point, otherwise the tangend to the end point. * @param sx Pre-transform scale factor for x coordinates. * @param sy Pre-transform scale factor for y coordinates. * @param m Transformation matrix. * @param result The tangent is returned as vector (x, y) in result. */ override public function getTangent(prev:PathSegment, start:Boolean, sx:Number, sy:Number, m:Matrix, result:Point):void { var pt0:Point = MatrixUtil.transformPoint(prev ? prev.x * sx : 0, prev ? prev.y * sy : 0, m).clone(); var pt1:Point = MatrixUtil.transformPoint(control1X * sx, control1Y * sy, m).clone();; var pt2:Point = MatrixUtil.transformPoint(x * sx, y * sy, m).clone(); getQTangent(pt0.x, pt0.y, pt1.x, pt1.y, pt2.x, pt2.y, start, result); } /** * @inheritDoc * * @langversion 3.0 * @playerversion Flash 10 * @playerversion AIR 1.5 * @productversion Flex 4 */ override public function getBoundingBox(prev:PathSegment, sx:Number, sy:Number, m:Matrix, rect:Rectangle):Rectangle { return MatrixUtil.getQBezierSegmentBBox(prev ? prev.x : 0, prev ? prev.y : 0, control1X, control1Y, x, y, sx, sy, m, rect); } }