| //////////////////////////////////////////////////////////////////////////////// |
| // |
| // Licensed to the Apache Software Foundation (ASF) under one or more |
| // contributor license agreements. See the NOTICE file distributed with |
| // this work for additional information regarding copyright ownership. |
| // The ASF licenses this file to You under the Apache License, Version 2.0 |
| // (the "License"); you may not use this file except in compliance with |
| // the License. You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| // |
| //////////////////////////////////////////////////////////////////////////////// |
| |
| 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. |
| * |
| * <p>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.</p> |
| * |
| * <p>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.</p> |
| * |
| * <p>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.</p> |
| * |
| * @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 <code>data</code> |
| * 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.<int>(), new Vector.<Number>()); |
| |
| //-------------------------------------------------------------------------- |
| // |
| // 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. |
| * |
| * <p>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.</p> |
| * |
| * <p>If the segment command is upper-case, the parameters are absolute values. |
| * If the segment command is lower-case, the parameters are relative values.</p> |
| * |
| * <p>The following table shows the syntax for the segments: |
| * |
| * |
| * <table class="innertable"> |
| * <tr> |
| * <th>Segment Type</th> |
| * <th>Command</th> |
| * <th>Parameters</th> |
| * <th>Example</th> |
| * </tr> |
| * <tr> |
| * <td>Move</td> |
| * <td>M/m</td> |
| * <td>x y</td> |
| * <td><code>M 10 20</code> - Move line to 10, 20.</td> |
| * </tr> |
| * <tr> |
| * <td>Line</td> |
| * <td>L/l</td> |
| * <td>x y</td> |
| * <td><code>L 50 30</code> - Line to 50, 30.</td> |
| * </tr> |
| * <tr> |
| * <td>Horizontal line</td> |
| * <td>H/h</td> |
| * <td>x</td> |
| * <td><code>H 40</code> = Horizontal line to 40.</td> |
| * </tr> |
| * <tr> |
| * <td>Vertical line</td> |
| * <td>V/v</td> |
| * <td>y</td> |
| * <td><code>V 100</code> - Vertical line to 100.</td> |
| * </tr> |
| * <tr> |
| * <td>QuadraticBezier</td> |
| * <td>Q/q</td> |
| * <td>controlX controlY x y</td> |
| * <td><code>Q 110 45 90 30</code> - Curve to 90, 30 with the control point at 110, 45.</td> |
| * </tr> |
| * <tr> |
| * <td>CubicBezier</td> |
| * <td>C/c</td> |
| * <td>control1X control1Y control2X control2Y x y</td> |
| * <td><code>C 45 50 20 30 10 20</code> - Curve to 10, 20 with the first control point at 45, 50 and the second control point at 20, 30.</td> |
| * </tr> |
| * <tr> |
| * <td>Close path</td> |
| * <td>Z/z</td> |
| * <td>n/a</td> |
| * <td>Closes off the path.</td> |
| * </tr> |
| * </table> |
| * </p> |
| * |
| * @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 <code>GraphicsPathWinding.EVEN_ODD</code> or <code>GraphicsPathWinding.NON_ZERO</code>. |
| * |
| * @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.<PathSegment> = 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.<PathSegment>(); |
| return; |
| } |
| |
| var newSegments:Vector.<PathSegment> = new Vector.<PathSegment>(); |
| 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.<PathSegment>(); |
| 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.<PathSegment>; |
| |
| /** |
| * A Vector of the actual path segments. May be empty, but always non-null. |
| */ |
| public function get data():Vector.<PathSegment> |
| { |
| 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 <code>segments</code> |
| * 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 <code>draw()</code> method uses |
| * this value to determine where to draw to. |
| * |
| * @param y The current location of the pen along the y axis. The <code>draw()</code> 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 <code>Graphics.moveTo()</code> method |
| * from the <code>draw()</code> 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. |
| * |
| * <p>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.</p> |
| * |
| * <p>For details on the fixed midpoint algorithm, see:<br/> |
| * http://timotheegroleau.com/Flash/articles/cubic_bezier_in_flash.htm</p> |
| * |
| * |
| * @langversion 3.0 |
| * @playerversion Flash 10 |
| * @playerversion AIR 1.5 |
| * @productversion Flex 4 |
| */ |
| class CubicBezierSegment extends PathSegment |
| { |
| |
| //-------------------------------------------------------------------------- |
| // |
| // Constructor |
| // |
| //-------------------------------------------------------------------------- |
| |
| /** |
| * Constructor. |
| * |
| * <p>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.</p> |
| * |
| * @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. |
| * |
| * <p>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.</p> |
| * |
| * @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 <code>Graphics.curveTo()</code> 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); |
| } |
| } |