| /* |
| * |
| * 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 org.apache.royale.compiler.internal.fxg.swf; |
| |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.Iterator; |
| import java.util.List; |
| |
| import org.apache.royale.compiler.internal.fxg.dom.AbstractFXGNode; |
| import org.apache.royale.compiler.internal.fxg.dom.PathNode; |
| import org.apache.royale.compiler.internal.fxg.dom.strokes.AbstractStrokeNode; |
| import org.apache.royale.compiler.problems.FXGInvalidPathDataProblem; |
| import org.apache.royale.compiler.problems.ICompilerProblem; |
| |
| import org.apache.royale.swf.ISWFConstants; |
| import org.apache.royale.swf.types.CurvedEdgeRecord; |
| import org.apache.royale.swf.types.LineStyle; |
| import org.apache.royale.swf.types.LineStyle2; |
| import org.apache.royale.swf.types.Rect; |
| import org.apache.royale.swf.types.ShapeRecord; |
| import org.apache.royale.swf.types.StraightEdgeRecord; |
| import org.apache.royale.swf.types.StyleChangeRecord; |
| import org.apache.royale.swf.types.Styles; |
| |
| /** |
| * A collection of utilities to help create SWF Shapes and ShapeRecords. |
| */ |
| public class ShapeHelper implements ISWFConstants |
| { |
| /** |
| * A value object for an x and y pair. |
| */ |
| private static class Point |
| { |
| public Point() |
| { |
| x = 0.0; |
| y = 0.0; |
| } |
| |
| public Point(double x, double y) |
| { |
| this.x = x; |
| this.y = y; |
| } |
| |
| public double x; |
| public double y; |
| } |
| |
| /** |
| * Creates a List of ShapeRecord to draw a line from the given |
| * origin (startX, startY) to the specified coordinates (in pixels). |
| * |
| * @param startX The origin x coordinate in pixels. |
| * @param startY The origin y coordinate in pixels. |
| * @param endX The end x coordinate in pixels. |
| * @param endY The end y coordinate in pixels. |
| * @return list of ShapeRecords representing the rectangle. |
| */ |
| public static List<ShapeRecord> line(double startX, double startY, double endX, double endY) |
| { |
| List<ShapeRecord> shapeRecords = new ArrayList<ShapeRecord>(); |
| shapeRecords.add(move(startX, startY)); |
| shapeRecords.addAll(straightEdge(startX, startY, endX, endY)); |
| return shapeRecords; |
| } |
| |
| |
| /** |
| * Creates a List of ShapeRecord to draw a line that represents an implicit closepath |
| * origin (startX, startY) to the specified coordinates (in pixels). |
| * |
| * @param startX The origin x coordinate in pixels. |
| * @param startY The origin y coordinate in pixels. |
| * @param endX The end x coordinate in pixels. |
| * @param endY The end y coordinate in pixels. |
| * @return list of ShapeRecords representing the rectangle. |
| */ |
| public static List<ShapeRecord> implicitClosepath(double startX, double startY, double endX, double endY) |
| { |
| List<ShapeRecord> shapeRecords = new ArrayList<ShapeRecord>(); |
| StyleChangeRecord scr = move(startX, startY); |
| scr.defaultStyles(false, false, true); |
| shapeRecords.add(scr); |
| shapeRecords.addAll(straightEdge(startX, startY, endX, endY)); |
| return shapeRecords; |
| } |
| |
| /** |
| * Creates a List of ShapeRecord to draw a rectangle from the given |
| * origin (startX, startY) for the specified width and height (in pixels). |
| * |
| * @param startX The origin x coordinate in pixels. |
| * @param startY The origin y coordinate in pixels. |
| * @param width The rectangle width in pixels. |
| * @param height The rectangle width in pixels. |
| * @return list of ShapeRecords representing the rectangle. |
| */ |
| public static List<ShapeRecord> rectangle(double startX, double startY, double width, double height) |
| { |
| List<ShapeRecord> shapeRecords = new ArrayList<ShapeRecord>(); |
| shapeRecords.add(move(startX, startY)); |
| shapeRecords.addAll(straightEdge(startX, startY, width, startY)); |
| shapeRecords.addAll(straightEdge(width, startY, width, height)); |
| shapeRecords.addAll(straightEdge(width, height, startX, height)); |
| shapeRecords.addAll(straightEdge(startX, height, startX, startY)); |
| return shapeRecords; |
| } |
| |
| /** |
| * Creates a List of ShapeRecord to draw a rectangle from the given |
| * origin (startX, startY) for the specified width and height (in pixels) |
| * and radiusX and radiusY for rounded corners. |
| * |
| * @param startx The origin x coordinate in pixels. |
| * @param starty The origin y coordinate in pixels. |
| * @param width The rectangle width in pixels. |
| * @param height The rectangle width in pixels. |
| * @param radiusX The radiusX for rounded corner in pixels |
| * @param radiusY The radius for rounded corner in pixels |
| * @return list of ShapeRecords representing the rectangle. |
| */ |
| public static List<ShapeRecord> rectangle(double startx, double starty, |
| double width, double height, double radiusX, double radiusY, |
| double topLeftRadiusX, double topLeftRadiusY, double topRightRadiusX, |
| double topRightRadiusY, double bottomLeftRadiusX, double bottomLeftRadiusY, |
| double bottomRightRadiusX, double bottomRightRadiusY) |
| { |
| |
| //YAY MAGIC NUMBERS! |
| final double SIN_67_5 = 0.923879532511;//sin .75 * 90 |
| final double SIN_22_5 = 0.382683432365;//sin .25 * 90 |
| final double SIN_45_0 = 0.707106781187;//sin .50 * 90 |
| |
| List<ShapeRecord> shapeRecords = new ArrayList<ShapeRecord>(); |
| |
| if (radiusX == 0.0) |
| { |
| radiusY = radiusX = 0; |
| } |
| else if (radiusY == 0.0) |
| { |
| radiusY = radiusX; |
| } |
| |
| if ( radiusX > width/2.0 ) |
| radiusX = width/2.0; |
| if ( radiusY > height/2.0 ) |
| radiusY = height/2.0; |
| |
| double[] topLeftRadius = getCornerRadius(topLeftRadiusX, topLeftRadiusY, radiusX, radiusY, width, height); |
| topLeftRadiusX = topLeftRadius[0]; |
| topLeftRadiusY = topLeftRadius[1]; |
| |
| double[] topRightRadius = getCornerRadius(topRightRadiusX, topRightRadiusY, radiusX, radiusY, width, height); |
| topRightRadiusX = topRightRadius[0]; |
| topRightRadiusY = topRightRadius[1]; |
| |
| double[] bottomLeftRadius = getCornerRadius(bottomLeftRadiusX, bottomLeftRadiusY, radiusX, radiusY, width, height); |
| bottomLeftRadiusX = bottomLeftRadius[0]; |
| bottomLeftRadiusY = bottomLeftRadius[1]; |
| |
| double[] bottomRightRadius = getCornerRadius(bottomRightRadiusX, bottomRightRadiusY, radiusX, radiusY, width, height); |
| bottomRightRadiusX = bottomRightRadius[0]; |
| bottomRightRadiusY = bottomRightRadius[1]; |
| |
| double c0 = SIN_67_5; |
| double c1 = SIN_22_5; |
| double c3 = SIN_45_0; |
| |
| double rx = bottomRightRadiusX; |
| double ry = bottomRightRadiusY; |
| |
| double tx = rx / SIN_67_5; |
| double ty = ry / SIN_67_5; |
| |
| double dx, currentx; |
| double dy, currenty; |
| |
| dx = startx + width - rx; |
| dy = starty + height - ry; |
| shapeRecords.add(move( (dx + rx), dy )); |
| currentx = (dx + rx); |
| currenty = dy; |
| if ( bottomRightRadiusX != 0.0 ) |
| { |
| shapeRecords.add(curvedEdge(currentx, currenty, (dx + c0 * tx), (dy + c1 * ty), (dx + c3 * rx), (dy + c3 * ry) )); |
| shapeRecords.add(curvedEdge((dx + c3 * rx), (dy + c3 * ry), (dx + c1 * tx), (dy + c0 * ty), dx, (dy + ry)) ); |
| currentx = dx; |
| currenty = dy + ry; |
| } |
| |
| rx = bottomLeftRadiusX; |
| ry = bottomLeftRadiusY; |
| tx = rx / SIN_67_5; |
| ty = ry / SIN_67_5; |
| dx = startx + rx; |
| dy = starty + height - ry; |
| shapeRecords.addAll(straightEdge(currentx, currenty, dx, (dy + ry) )); |
| currentx = dx; |
| currenty = dy + ry; |
| if ( bottomLeftRadiusX != 0.0 ) |
| { |
| shapeRecords.add(curvedEdge(currentx, currenty, (dx - c1 * tx), (dy + c0 * ty), (dx - c3 * rx), (dy + c3 * ry) )); |
| shapeRecords.add(curvedEdge((dx - c3 * rx), (dy + c3 * ry), (dx - c0 * tx), (dy + c1 * ty), (dx - rx), dy )); |
| currentx = dx - rx; |
| currenty = dy; |
| } |
| |
| rx = topLeftRadiusX; |
| ry = topLeftRadiusY; |
| tx = rx / SIN_67_5; |
| ty = ry / SIN_67_5; |
| dx = startx + rx; |
| dy = starty + ry; |
| shapeRecords.addAll(straightEdge(currentx, currenty, (dx - rx), dy )); |
| currentx = dx - rx; |
| currenty = dy; |
| if ( topLeftRadiusX != 0.0 ) |
| { |
| shapeRecords.add(curvedEdge(currentx, currenty, (dx - c0 * tx), (dy - c1 * ty), (dx - c3 * rx), (dy - c3 * ry) )); |
| shapeRecords.add(curvedEdge((dx - c3 * rx), (dy - c3 * ry), (dx - c1 * tx), (dy - c0 * ty), dx, (dy - ry) )); |
| currentx = dx; |
| currenty = dy - ry; |
| } |
| |
| rx = topRightRadiusX; |
| ry = topRightRadiusY; |
| tx = rx / SIN_67_5; |
| ty = ry / SIN_67_5; |
| dx = startx + width - rx; |
| dy = starty + ry; |
| shapeRecords.addAll(straightEdge(currentx, currenty, dx, (dy - ry) )); |
| currentx = dx; |
| currenty = dy - ry; |
| if ( topRightRadiusX != 0.0 ) |
| { |
| shapeRecords.add(curvedEdge(currentx, currenty, (dx + c1 * tx), (dy - c0 * ty), (dx + c3 * rx), (dy - c3 * ry) )); |
| shapeRecords.add(curvedEdge((dx + c3 * rx), (dy - c3 * ry), (dx + c0 * tx), (dy - c1 * ty), (dx + rx), dy )); |
| currentx = (dx + rx); |
| currenty = dy; |
| } |
| |
| rx = bottomRightRadiusX; |
| ry = bottomRightRadiusY; |
| tx = rx / SIN_67_5; |
| ty = ry / SIN_67_5; |
| dx = startx + width - rx; |
| dy = starty + height - ry; |
| shapeRecords.addAll(straightEdge(currentx, currenty, (dx + rx), dy )); |
| |
| return shapeRecords; |
| |
| } |
| |
| /** |
| * Creates a List of ShapeRecord to draw a rectangle from the |
| * origin (0.0, 0.0) for the specified width and height (in pixels). |
| * |
| * @param width The rectangle width in pixels. |
| * @param height The rectangle width in pixels. |
| * @return list of ShapeRecords representing the rectangle. |
| */ |
| public static List<ShapeRecord> rectangle(double width, double height) |
| { |
| return rectangle(0.0, 0.0, width, height); |
| } |
| |
| /** |
| * Sets the style information for the first StyleChangeRecord in a list |
| * of ShapeRecords. |
| * |
| * @param shapeRecords A list of shape records. |
| * @param lineStyleIndex The ShapeWithStyle LineStyle index (starting at 1) |
| * or 0 if none. |
| * @param fillStyle0Index The ShapeWithStyle FillStyle index (starting at 1) |
| * or 0 if none. |
| * @param fillStyle1Index The ShapeWithStyle FillStyle index (starting at 1) |
| * or 0 if none. |
| */ |
| public static void setStyles(List<ShapeRecord> shapeRecords, |
| int lineStyleIndex, int fillStyle0Index, int fillStyle1Index, Styles styles) |
| { |
| int fs0 = fillStyle0Index == 0 ? -1 : fillStyle0Index; |
| int fs1 = fillStyle1Index == 0 ? -1 : fillStyle1Index; |
| int ls = lineStyleIndex == 0 ? -1 : lineStyleIndex; |
| if (shapeRecords != null && shapeRecords.size() > 0) |
| { |
| ShapeRecord firstRecord = shapeRecords.get(0); |
| if (firstRecord instanceof StyleChangeRecord) |
| { |
| StyleChangeRecord scr = (StyleChangeRecord)firstRecord; |
| scr.setDefinedStyles(fs0, fs1, ls, styles); |
| } |
| } |
| } |
| |
| |
| /** |
| * Sets the style information for the all the StyleChangeRecords in a list |
| * of ShapeRecords. |
| * |
| * @param shapeRecords A list of shape records. |
| * @param lineStyleIndex The ShapeWithStyle LineStyle index (starting at 1) |
| * or 0 if none. |
| * @param fillStyle0Index The ShapeWithStyle FillStyle index (starting at 1) |
| * or 0 if none. |
| * @param fillStyle1Index The ShapeWithStyle FillStyle index (starting at 1) |
| * or 0 if none. |
| */ |
| public static void setPathStyles(List<ShapeRecord> shapeRecords, |
| int lineStyleIndex, int fillStyle0Index, int fillStyle1Index, Styles styles) |
| { |
| int fs0 = fillStyle0Index == 0 ? -1 : fillStyle0Index; |
| int fs1 = fillStyle1Index == 0 ? -1 : fillStyle1Index; |
| int ls = lineStyleIndex == 0 ? -1 : lineStyleIndex; |
| |
| if (shapeRecords != null && shapeRecords.size() > 0) |
| { |
| for (int i = 0; i < shapeRecords.size(); i++) |
| { |
| ShapeRecord record = shapeRecords.get(i); |
| if (record instanceof StyleChangeRecord) |
| { |
| StyleChangeRecord scr = (StyleChangeRecord)record; |
| |
| scr.setDefinedStyles(fs0, fs1, (!scr.isStateLineStyle() && ls > 0) ? ls : -1, styles); |
| } |
| } |
| } |
| } |
| |
| /** |
| * Replaces the style information for the all the StyleChangeRecords in a list |
| * of ShapeRecords. |
| * |
| * @param shapeRecords A list of shape records. |
| * @param lineStyleIndex The ShapeWithStyle LineStyle index (starting at 1) |
| * or 0 if none. |
| * @param fillStyle0Index The ShapeWithStyle FillStyle index (starting at 1) |
| * or 0 if none. |
| * @param fillStyle1Index The ShapeWithStyle FillStyle index (starting at 1) |
| * or 0 if none. |
| */ |
| public static void replaceStyles(List<ShapeRecord> shapeRecords, |
| int lineStyleIndex, int fillStyle0Index, int fillStyle1Index, Styles styleContext) |
| { |
| |
| int fs0 = fillStyle0Index == 0 ? -1 : fillStyle0Index; |
| int fs1 = fillStyle1Index == 0 ? -1 : fillStyle1Index; |
| int ls = lineStyleIndex == 0 ? -1 : lineStyleIndex; |
| |
| if (shapeRecords != null && shapeRecords.size() > 0) |
| { |
| for (int i = 0; i < shapeRecords.size(); i++) |
| { |
| ShapeRecord record = shapeRecords.get(i); |
| if (record instanceof StyleChangeRecord) |
| { |
| StyleChangeRecord old_scr = (StyleChangeRecord) record; |
| StyleChangeRecord new_scr = new StyleChangeRecord(); |
| int old_scrLineStyleIndex = 0; |
| |
| if (old_scr.getLinestyle() != null) |
| old_scrLineStyleIndex = styleContext.getLineStyles().indexOf(old_scr.getLinestyle()); |
| |
| new_scr.setDefinedStyles(fs0, fs1, ((!old_scr.isStateLineStyle() && (ls > 0)) ? ls: old_scrLineStyleIndex), styleContext); |
| |
| if (old_scr.isStateMoveTo()) |
| new_scr.setMove(old_scr.getMoveDeltaX(), old_scr.getMoveDeltaY()); |
| |
| shapeRecords.set(i, new_scr); |
| |
| } |
| } |
| } |
| } |
| |
| /** |
| * Creates a StyleChangeRecord to represent a move command without changing |
| * style information. All coordinates are to be specified in pixels and will |
| * be converted to twips. |
| * |
| * @param x The x coordinate in pixels. |
| * @param y The y coordinate in pixels. |
| * @return StyleChangeRecord recording the move and styles. |
| */ |
| public static StyleChangeRecord move(double x, double y) |
| { |
| x *= TWIPS_PER_PIXEL; |
| y *= TWIPS_PER_PIXEL; |
| |
| int moveX = (int)x; |
| int moveY = (int)y; |
| |
| StyleChangeRecord scr = new StyleChangeRecord(); |
| |
| scr.setMove(moveX, moveY); |
| |
| return scr; |
| } |
| |
| |
| private static final int MAX_EDGE_SIZE = 65535; |
| |
| /** |
| * Creates a StraightEdgeRecord to represent a line as the delta between |
| * the pair of coordinates (xFrom,yFrom) and (xTo,yTo). All coordinates |
| * are to be specified in pixels and will be converted to twips. |
| * |
| * @param xFrom The start x coordinate in pixels. |
| * @param yFrom The start y coordinate in pixels. |
| * @param xTo The end x coordinate in pixels. |
| * @param yTo The end y coordinate in pixels. |
| */ |
| public static List<ShapeRecord> straightEdge(double xFrom, double yFrom, double xTo, double yTo) |
| { |
| List<ShapeRecord> shapeRecords = new ArrayList<ShapeRecord>(); |
| |
| xFrom *= TWIPS_PER_PIXEL; |
| yFrom *= TWIPS_PER_PIXEL; |
| xTo *= TWIPS_PER_PIXEL; |
| yTo *= TWIPS_PER_PIXEL; |
| |
| int dx = (int)xTo - (int)xFrom; |
| int dy = (int)yTo - (int)yFrom; |
| int abs_dx = Math.abs(dx); |
| int abs_dy = Math.abs(dy); |
| |
| int numSegments = 1; |
| if ((abs_dx > MAX_EDGE_SIZE) && (abs_dx > abs_dy)) |
| { |
| numSegments = abs_dx/MAX_EDGE_SIZE + 1; |
| } |
| else if ((abs_dy > MAX_EDGE_SIZE) && (abs_dy > abs_dx)) |
| { |
| numSegments = abs_dy/MAX_EDGE_SIZE + 1; |
| } |
| else |
| { |
| StraightEdgeRecord ser = new StraightEdgeRecord(dx, dy); |
| shapeRecords.add(ser); |
| return shapeRecords; |
| } |
| |
| int xSeg = dx/numSegments; |
| int ySeg = dy/numSegments; |
| for (int i=0; i < numSegments; i++) |
| { |
| if (i == numSegments-1) |
| { |
| //make up for any rounding errors |
| int lastx = dx - xSeg*(numSegments-1); |
| int lasty = dy - ySeg*(numSegments-1); |
| StraightEdgeRecord ser = new StraightEdgeRecord(lastx, lasty); |
| shapeRecords.add(ser); |
| } |
| else |
| { |
| StraightEdgeRecord ser = new StraightEdgeRecord(xSeg, ySeg); |
| shapeRecords.add(ser); |
| } |
| } |
| |
| return shapeRecords; |
| } |
| |
| /** |
| * Creates a CurvedEdgeRecord to represent a quadratic curve by calculating |
| * the deltas between the start coordinates and the control point |
| * coordinates, and between the control point coordinates and the anchor |
| * coordinates. All coordinates are to be specified in pixels and will be |
| * converted to twips. |
| * |
| * @param startX The start x coordinate in pixels. |
| * @param startY The start y coordinate in pixels. |
| * @param controlX The control point x coordinate in pixels. |
| * @param controlY The control point y coordinate in pixels. |
| * @param anchorX The anchor x coordinate in pixels. |
| * @param anchorY The anchor y coordinate in pixels. |
| * @return CurvedEdgeRecord representing a quadratic curve. |
| */ |
| public static CurvedEdgeRecord curvedEdge(double startX, double startY, |
| double controlX, double controlY, double anchorX, double anchorY) |
| { |
| startX *= TWIPS_PER_PIXEL; |
| startY *= TWIPS_PER_PIXEL; |
| controlX *= TWIPS_PER_PIXEL; |
| controlY *= TWIPS_PER_PIXEL; |
| anchorX *= TWIPS_PER_PIXEL; |
| anchorY *= TWIPS_PER_PIXEL; |
| |
| int dcx = (int)controlX - (int)startX; |
| int dcy = (int)controlY - (int)startY; |
| int dax = (int)anchorX - (int)controlX; |
| int day = (int)anchorY - (int)controlY; |
| |
| CurvedEdgeRecord cer = new CurvedEdgeRecord(); |
| cer.setControlDeltaX(dcx); |
| cer.setControlDeltaY(dcy); |
| cer.setAnchorDeltaX(dax); |
| cer.setAnchorDeltaY(day); |
| return cer; |
| } |
| |
| /** |
| * Approximates a cubic Bezier as a series of 4 quadratic CurvedEdgeRecord |
| * with the method outlined by Timothee Groleau in ActionScript (which was |
| * based on Helen Triolo's approach of using Casteljau's approximation). |
| * |
| * Using a fixed level of 4 quadratic curves should be a fast way of |
| * achieving a reasonable approximation of the original curve. |
| * |
| * All coordinates are to be specified in pixels and will be converted to |
| * twips. |
| * |
| * @param startX The start x coordinate in pixels. |
| * @param startY The start y coordinate in pixels. |
| * @param control1X The first control point x coordinate in pixels. |
| * @param control1Y The first control point y coordinate in pixels. |
| * @param control2X The second control point x coordinate in pixels. |
| * @param control2Y The second control point y coordinate in pixels. |
| * @param anchorX The anchor x coordinate in pixels. |
| * @param anchorY The anchor y coordinate in pixels. |
| * @return a List of 4 CurvedEdgeRecords approximating the cubic Bezier. |
| * |
| * {@link <a href="http://timotheegroleau.com/Flash/articles/cubic_bezier_in_flash.htm">Cubic Bezier Curves in Flash</a>} |
| */ |
| public static List<ShapeRecord> cubicToQuadratic(final double startX, final double startY, |
| final double control1X, final double control1Y, |
| final double control2X, final double control2Y, |
| final double anchorX, final double anchorY) |
| { |
| // First, calculate useful base points |
| double ratio = 3.0 / 4.0; |
| double pax = startX + ((control1X - startX) * ratio); |
| double pay = startY + ((control1Y - startY) * ratio); |
| double pbx = anchorX + ((control2X - anchorX) * ratio); |
| double pby = anchorY + ((control2Y - anchorY) * ratio); |
| |
| // Get 1/16 of the [anchor, start] segment |
| double dx = (anchorX - startX) / 16.0; |
| double dy = (anchorY - startY) / 16.0; |
| |
| // Calculate control point 1 |
| ratio = 3.0 / 8.0; |
| double c1x = startX + ((control1X - startX) * ratio); |
| double c1y = startY + ((control1Y - startY) * ratio); |
| |
| // Calculate control point 2 |
| double c2x = pax + ((pbx - pax) * ratio); |
| double c2y = pay + ((pby - pay) * ratio); |
| c2x = c2x - dx; |
| c2y = c2y - dy; |
| |
| // Calculate control point 3 |
| double c3x = pbx + ((pax - pbx) * ratio); |
| double c3y = pby + ((pay - pby) * ratio); |
| c3x = c3x + dx; |
| c3y = c3y + dy; |
| |
| // Calculate control point 4 |
| double c4x = anchorX + ((control2X - anchorX) * ratio); |
| double c4y = anchorY + ((control2Y - anchorY) * ratio); |
| |
| // Calculate the 3 anchor points (as midpoints of the control segments) |
| double a1x = (c1x + c2x) / 2.0; |
| double a1y = (c1y + c2y) / 2.0; |
| |
| double a2x = (pax + pbx) / 2.0; |
| double a2y = (pay + pby) / 2.0; |
| |
| double a3x = (c3x + c4x) / 2.0; |
| double a3y = (c3y + c4y) / 2.0; |
| |
| // Create the four quadratic sub-segments |
| List<ShapeRecord> shapeRecords = new ArrayList<ShapeRecord>(4); |
| shapeRecords.add(curvedEdge(startX, startY, c1x, c1y, a1x, a1y)); |
| shapeRecords.add(curvedEdge(a1x, a1y, c2x, c2y, a2x, a2y)); |
| shapeRecords.add(curvedEdge(a2x, a2y, c3x, c3y, a3x, a3y)); |
| shapeRecords.add(curvedEdge(a3x, a3y, c4x, c4y, anchorX, anchorY)); |
| return shapeRecords; |
| } |
| |
| /** |
| * Note this utility was ported to Java from the ActionScript class |
| * 'flex.graphics.Path' - specifically its 'data' property setter function. |
| */ |
| public static List<ShapeRecord> path(PathNode node, boolean fill, Collection<ICompilerProblem> problems) |
| { |
| String data = node.data; |
| |
| List<ShapeRecord> shapeRecords = new ArrayList<ShapeRecord>(); |
| |
| if (data.length() == 0) |
| return shapeRecords; |
| |
| // Split letter followed by number (i.e. "M3" becomes "M 3") |
| String temp = data.replaceAll("([A-Za-z])([0-9\\-\\.])", "$1 $2"); |
| |
| // Split number followed by letter (i.e. "3M" becomes "3 M") |
| temp = temp.replaceAll("([0-9\\.])([A-Za-z\\-])", "$1 $2"); |
| |
| // Split letter followed by letter (i.e. "zM" becomes "z M") |
| temp = temp.replaceAll("([A-Za-z\\-])([A-Za-z\\-])", "$1 $2"); |
| |
| //support scientific notation for floats/doubles |
| temp = temp.replaceAll("([0-9])( )([eE])( )([0-9\\-])", "$1$3$5"); |
| |
| // Replace commas with spaces |
| temp = temp.replace(',', ' '); |
| |
| // Trim leading and trailing spaces |
| temp = temp.trim(); |
| |
| // Finally, split the string into an array |
| String[] args = temp.split("\\s+"); |
| |
| char ic = 0; |
| char prevIc = 0; |
| double lastMoveX = 0.0; |
| double lastMoveY = 0.0; |
| double prevX = 0.0; |
| double prevY = 0.0; |
| double x = 0.0; |
| double y = 0.0; |
| double controlX = 0.0; |
| double controlY = 0.0; |
| double control2X = 0.0; |
| double control2Y = 0.0; |
| boolean firstMove = true; |
| |
| for (int i = 0; i < args.length; ) |
| { |
| boolean relative = false; |
| char c = args[i].toCharArray()[0]; |
| if (c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z') |
| { |
| ic = c; |
| i++; |
| } |
| |
| if ((firstMove) && (ic != 'm') && (ic != 'M')) |
| { |
| problems.add(new FXGInvalidPathDataProblem(node.getDocumentPath(), |
| node.getStartLine(), node.getStartColumn())); |
| return Collections.<ShapeRecord>emptyList(); |
| } |
| |
| switch (ic) |
| { |
| case 'm': |
| relative = true; |
| case 'M': |
| if (firstMove) { |
| x = Double.parseDouble(args[i++]); |
| y = Double.parseDouble(args[i++]); |
| shapeRecords.add(move(x, y)); |
| firstMove = false; |
| } |
| else |
| { |
| //add an implicit closepath, if needed |
| if (fill && (Math.abs(prevX-lastMoveX) > AbstractFXGNode.EPSILON || Math.abs(prevY-lastMoveY) > AbstractFXGNode.EPSILON)) |
| { |
| if (node.stroke == null) |
| shapeRecords.addAll(straightEdge(prevX, prevY, lastMoveX, lastMoveY)); |
| else |
| shapeRecords.addAll(implicitClosepath(prevX, prevY, lastMoveX, lastMoveY)); |
| } |
| x = Double.parseDouble(args[i++]) + (relative ? prevX : 0); |
| y = Double.parseDouble(args[i++]) + (relative ? prevY : 0); |
| shapeRecords.add(move(x, y)); |
| } |
| lastMoveX = x; |
| lastMoveY = y; |
| ic = (relative) ? 'l' : 'L'; |
| break; |
| |
| case 'l': |
| relative = true; |
| case 'L': |
| x = Double.parseDouble(args[i++]) + (relative ? prevX : 0); |
| y = Double.parseDouble(args[i++]) + (relative ? prevY : 0); |
| shapeRecords.addAll(straightEdge(prevX, prevY, x, y)); |
| break; |
| |
| case 'h': |
| relative = true; |
| case 'H': |
| x = Double.parseDouble(args[i++]) + (relative ? prevX : 0); |
| y = prevY; |
| shapeRecords.addAll(straightEdge(prevX, prevY, x, y)); |
| break; |
| |
| case 'v': |
| relative = true; |
| case 'V': |
| x = prevX; |
| y = Double.parseDouble(args[i++]) + (relative ? prevY : 0); |
| shapeRecords.addAll(straightEdge(prevX, prevY, x, y)); |
| break; |
| |
| case 'q': |
| relative = true; |
| case 'Q': |
| controlX = Double.parseDouble(args[i++]) + (relative ? prevX : 0); |
| controlY = Double.parseDouble(args[i++]) + (relative ? prevY : 0); |
| x = Double.parseDouble(args[i++]) + (relative ? prevX : 0); |
| y = Double.parseDouble(args[i++]) + (relative ? prevY : 0); |
| shapeRecords.add(curvedEdge(prevX, prevY, controlX, controlY, x, y)); |
| break; |
| |
| case 't': |
| relative = true; |
| case 'T': |
| // control is a reflection of the previous control point |
| if ((prevIc == 'T') || (prevIc == 't') || (prevIc == 'q') || (prevIc == 'Q')) |
| { |
| controlX = prevX + (prevX - controlX); |
| controlY = prevY + (prevY - controlY); |
| } |
| else |
| { |
| controlX = prevX; |
| controlY = prevY; |
| } |
| x = Double.parseDouble(args[i++]) + (relative ? prevX : 0); |
| y = Double.parseDouble(args[i++]) + (relative ? prevY : 0); |
| shapeRecords.add(curvedEdge(prevX, prevY, controlX, controlY, x, y)); |
| break; |
| |
| case 'c': |
| relative = true; |
| case 'C': |
| controlX = Double.parseDouble(args[i++]) + (relative ? prevX : 0); |
| controlY = Double.parseDouble(args[i++]) + (relative ? prevY : 0); |
| control2X = Double.parseDouble(args[i++]) + (relative ? prevX : 0); |
| control2Y = Double.parseDouble(args[i++]) + (relative ? prevY : 0); |
| x = Double.parseDouble(args[i++]) + (relative ? prevX : 0); |
| y = Double.parseDouble(args[i++]) + (relative ? prevY : 0); |
| shapeRecords.addAll(cubicToQuadratic(prevX, prevY, controlX, controlY, control2X, control2Y, x, y)); |
| break; |
| |
| case 's': |
| relative = true; |
| case 'S': |
| // Control1 is a reflection of the previous control2 point |
| if ((prevIc == 'S') || (prevIc == 's') || (prevIc == 'c') || (prevIc == 'C')) |
| { |
| controlX = prevX + (prevX - control2X); |
| controlY = prevY + (prevY - control2Y); |
| } |
| else |
| { |
| controlX = prevX; |
| controlY = prevY; |
| } |
| control2X = Double.parseDouble(args[i++]) + (relative ? prevX : 0); |
| control2Y = Double.parseDouble(args[i++]) + (relative ? prevY : 0); |
| x = Double.parseDouble(args[i++]) + (relative ? prevX : 0); |
| y = Double.parseDouble(args[i++]) + (relative ? prevY : 0); |
| shapeRecords.addAll(cubicToQuadratic(prevX, prevY, controlX, controlY, control2X, control2Y, x, y)); |
| break; |
| |
| case 'z': |
| case 'Z': |
| shapeRecords.addAll(straightEdge(prevX, prevY, lastMoveX, lastMoveY)); |
| x = lastMoveX; |
| y = lastMoveY; |
| break; |
| |
| default: |
| problems.add(new FXGInvalidPathDataProblem(node.getDocumentPath(), |
| node.getStartLine(), node.getStartColumn())); |
| return Collections.<ShapeRecord>emptyList(); |
| |
| } |
| |
| prevX = x; |
| prevY = y; |
| prevIc = ic; |
| } |
| |
| //do an implicit closepath, if needed |
| if (fill && (Math.abs(prevX-lastMoveX) > AbstractFXGNode.EPSILON) || (Math.abs(prevY-lastMoveY) > AbstractFXGNode.EPSILON)) |
| { |
| if (node.stroke == null) |
| shapeRecords.addAll(straightEdge(prevX, prevY, lastMoveX, lastMoveY)); |
| else |
| shapeRecords.addAll(implicitClosepath(prevX, prevY, lastMoveX, lastMoveY)); |
| } |
| return shapeRecords; |
| } |
| |
| |
| /** |
| * Utility method that calculates the minimum bounding rectangle that |
| * encloses a list of ShapeRecords, taking into account the possible maximum |
| * stroke width of any of the supplied linestyles. |
| * |
| * @return bounding box rectangle. |
| */ |
| public static Rect getBounds(List<ShapeRecord> records, LineStyle ls, AbstractStrokeNode strokeNode) |
| { |
| if (records == null || records.size() == 0) |
| { |
| assert false: "null records"; |
| return new Rect(0,0); |
| } |
| int x1 = 0; |
| int y1 = 0; |
| int x2 = 0; |
| int y2 = 0; |
| int x = 0; |
| int y = 0; |
| boolean firstMove = true; |
| |
| Iterator<ShapeRecord> iterator = records.iterator(); |
| while (iterator.hasNext()) |
| { |
| ShapeRecord r = iterator.next(); |
| |
| if (r == null) |
| continue; |
| |
| if (r instanceof StyleChangeRecord) |
| { |
| StyleChangeRecord scr = (StyleChangeRecord)r; |
| x = scr.getMoveDeltaX(); |
| y = scr.getMoveDeltaY(); |
| scr.setMove(x, y); |
| |
| if (firstMove) |
| { |
| x1 = x; |
| y1 = y; |
| x2 = x; |
| y2 = y; |
| firstMove = false; |
| } |
| } |
| else if (r instanceof StraightEdgeRecord) |
| { |
| StraightEdgeRecord ser = (StraightEdgeRecord)r; |
| x = x + ser.getDeltaX(); |
| y = y + ser.getDeltaY(); |
| } |
| else if (r instanceof CurvedEdgeRecord) |
| { |
| CurvedEdgeRecord cer = (CurvedEdgeRecord)r; |
| |
| Rect currRect = new Rect(x1, x2, y1, y2); |
| if (!curveControlPointInsideCurrentRect(x, y, cer, currRect)) |
| { |
| Rect curvBounds = computeCurveBounds(x, y, cer); |
| |
| if (curvBounds.xMin() < x1) x1 = curvBounds.xMin(); |
| if (curvBounds.yMin() < y1) y1 = curvBounds.yMin(); |
| if (curvBounds.xMax() > x2) x2 = curvBounds.xMax(); |
| if (curvBounds.yMax() > y2) y2 = curvBounds.yMax(); |
| } |
| |
| x = x + cer.getControlDeltaX() + cer.getAnchorDeltaX(); |
| y = y + cer.getControlDeltaY() + cer.getAnchorDeltaY(); |
| } |
| |
| //update x1, y1 to min values and x2, y2 to max values |
| if (x < x1) x1 = x; |
| if (y < y1) y1 = y; |
| if (x > x2) x2 = x; |
| if (y > y2) y2 = y; |
| } |
| |
| Rect newRect = new Rect(x1, x2, y1, y2); |
| |
| if (ls == null) |
| { |
| return newRect; |
| } |
| |
| // Inflate the bounding box from all sides with half of the stroke |
| // weight - pathBBox.inflate(weight/2, weight/2). |
| Rect strokeExtents = getStrokeExtents(strokeNode, ls); |
| |
| newRect = new Rect(newRect.xMin() - strokeExtents.xMax(), newRect.xMax() + strokeExtents.xMax(), |
| newRect.yMin() - strokeExtents.yMax(), newRect.yMax() + strokeExtents.yMax()); |
| |
| |
| // If there are less than two segments, then or joint style is not |
| //"miterLimit" finish - return pathBBox. |
| if (ls instanceof LineStyle2) |
| { |
| if (records.size() < 2 || ls == null || ((LineStyle2)ls).getJoinStyle() != LineStyle2.JS_MITER_JOIN) |
| { |
| return newRect; |
| } |
| } |
| // Use strokeExtents to get the transformed stroke weight. |
| double halfWeight = (strokeExtents.xMax() - strokeExtents.xMin())/2.0; |
| newRect = addJoint2Bounds(records, ls, strokeNode, halfWeight, newRect); |
| return newRect; |
| } |
| |
| public static Rect addJoint2Bounds(List<ShapeRecord> records, LineStyle ls, AbstractStrokeNode stroke, double halfWeight, Rect pathBBox) |
| { |
| Rect newRect = pathBBox; |
| int count = records.size(); |
| int start = 0; |
| int end = 0; |
| int lastMoveX = 0; |
| int lastMoveY = 0; |
| int lastOpenSegment = 0; |
| int x = 0, y = 0; |
| |
| // Add miterLimit effect to the bounds. |
| double miterLimit = stroke.miterLimit; |
| // Miter limit is always at least 1 |
| if (miterLimit < 1) miterLimit = 1; |
| |
| int[][] cooridinates = getCoordinates(records); |
| |
| while (true) |
| { |
| // Find a segment with a valid tangent or stop at a MoveSegment |
| while (start < count && !(records.get(start) instanceof StyleChangeRecord)) |
| { |
| x = cooridinates[start-1][0]; |
| y = cooridinates[start-1][1]; |
| if (tangentIsValid(records.get(start), x, y)) |
| break; |
| |
| start++; |
| } |
| |
| if (start >= count) |
| break; // No more segments with valid tangents |
| |
| ShapeRecord startSegment = records.get(start); |
| if (startSegment instanceof StyleChangeRecord) |
| { |
| // remember the last move segment |
| lastOpenSegment = start + 1; |
| lastMoveX = ((StyleChangeRecord)startSegment).getMoveDeltaX(); |
| lastMoveY = ((StyleChangeRecord)startSegment).getMoveDeltaY(); |
| |
| // 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. |
| int startSegmentX = cooridinates[start][0]; |
| int startSegmentY = cooridinates[start][1]; |
| if ((start == count - 1 || records.get(start + 1) instanceof StyleChangeRecord) && |
| startSegmentX == lastMoveX && |
| startSegmentY == lastMoveY) |
| { |
| end = lastOpenSegment; |
| } |
| else |
| { |
| end = start + 1; |
| } |
| |
| // Find a segment with a valid tangent or stop at a MoveSegment |
| while (end < count && !(records.get(end) instanceof StyleChangeRecord)) |
| { |
| if (tangentIsValid(records.get(end), startSegmentX, startSegmentY)) |
| break; |
| |
| end++; |
| } |
| |
| if (end >= count) |
| break; // No more segments with valid tangents |
| |
| ShapeRecord endSegment = records.get(end); |
| if (!(endSegment instanceof StyleChangeRecord)) |
| { |
| newRect = addMiterLimitStrokeToBounds( |
| startSegment, |
| endSegment, |
| miterLimit, |
| halfWeight, |
| newRect, x, y, startSegmentX, startSegmentY); |
| } |
| |
| // 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 newRect; |
| } |
| |
| private static int[][] getCoordinates(List<ShapeRecord> records) |
| { |
| int[][] coordinates = new int[records.size()][2]; |
| ShapeRecord record; |
| for(int i=0; i<records.size(); i++) |
| { |
| record = records.get(i); |
| if (record instanceof StyleChangeRecord) |
| { |
| StyleChangeRecord scr = (StyleChangeRecord)record; |
| coordinates[i][0] = scr.getMoveDeltaX(); |
| coordinates[i][1] = scr.getMoveDeltaY(); |
| } |
| else if (record instanceof StraightEdgeRecord) |
| { |
| StraightEdgeRecord ser = (StraightEdgeRecord)record; |
| coordinates[i][0] = coordinates[i-1][0] + ser.getDeltaX(); |
| coordinates[i][1] = coordinates[i-1][1] + ser.getDeltaY(); |
| } |
| else if (record instanceof CurvedEdgeRecord) |
| { |
| CurvedEdgeRecord cer = (CurvedEdgeRecord)record; |
| coordinates[i][0] = coordinates[i-1][0] + cer.getControlDeltaX() + cer.getAnchorDeltaX(); |
| coordinates[i][1] = coordinates[i-1][1] + cer.getControlDeltaY() + cer.getAnchorDeltaY(); |
| } |
| } |
| return coordinates; |
| } |
| |
| |
| public static Rect addMiterLimitStrokeToBounds(ShapeRecord segment1, |
| ShapeRecord segment2, double miterLimit, double halfWeight, Rect pathBBox, |
| int xPrev, int yPrev, int x, int y) |
| { |
| // The tip of the joint |
| Point jointPoint = new Point(x, y); |
| |
| //If a joint lies miterLimit*strokeWeight/2 away from pathBox, |
| //it is considered an inner joint and has no effect on bounds. So stop |
| //processing in this case. |
| if (isInnerJoint(jointPoint, pathBBox, miterLimit, halfWeight)) |
| { |
| return pathBBox; |
| } |
| |
| // End tangent for segment1: |
| Point t0 = getTangent(segment1, false /*start*/, xPrev, yPrev); |
| |
| // Start tangent for segment2: |
| Point t1 = getTangent(segment2, true /*start*/, x, y); |
| |
| // Valid tangents? |
| if (getPointLength(t0) == 0 || getPointLength(t1) == 0) |
| { |
| return pathBBox; |
| } |
| |
| // 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. |
| t0 = normalize(t0, 1); |
| t0.x = -t0.x; |
| t0.y = -t0.y; |
| t1 = normalize(t1, 1); |
| |
| // Find the vector from t0 to the midPoint from t0 to t1 |
| Point halfT0T1 = new Point((t1.x - t0.x) * 0.5, (t1.y - t0.y) * 0.5); |
| |
| // sin(A/2) == halfT0T1.length / t1.length() |
| double sinHalfAlpha = getPointLength(halfT0T1); |
| 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 pathBBox; |
| } |
| |
| // Find the vector of the bisect |
| Point bisect = new Point(-0.5 * (t0.x + t1.x), -0.5 * (t0.y + t1.y)); |
| double bisectLength = getPointLength(bisect); |
| if (bisectLength == 0) |
| { |
| // 180 degrees, nothing to contribute |
| return pathBBox; |
| } |
| |
| Rect newRect = pathBBox; |
| // 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. |
| |
| bisect = normalize(bisect, 1); |
| halfT0T1 = normalize(halfT0T1, (halfWeight - miterLimit * halfWeight * sinHalfAlpha) / bisectLength); |
| |
| Point pt0 = new Point(jointPoint.x + miterLimit * halfWeight * bisect.x + halfT0T1.x, |
| jointPoint.y + miterLimit * halfWeight * bisect.y + halfT0T1.y); |
| |
| Point pt1 = new Point(jointPoint.x + miterLimit * halfWeight * bisect.x - halfT0T1.x, |
| jointPoint.y + miterLimit * halfWeight * bisect.y - halfT0T1.y); |
| |
| // Add it to the rectangle: |
| newRect = rectUnion((int)StrictMath.rint(pt0.x), (int)StrictMath.rint(pt0.y), |
| (int)StrictMath.rint(pt0.x), (int)StrictMath.rint(pt0.y), newRect); |
| newRect = rectUnion((int)StrictMath.rint(pt1.x), (int)StrictMath.rint(pt1.y), |
| (int)StrictMath.rint(pt1.x), (int)StrictMath.rint(pt1.y), newRect); |
| } |
| else |
| { |
| // miter limit is not reached, add the tip of the stroke |
| bisect = normalize(bisect, 1); |
| Point strokeTip = new Point(jointPoint.x + bisect.x * halfWeight / sinHalfAlpha, |
| jointPoint.y + bisect.y * halfWeight / sinHalfAlpha); |
| |
| // Add it to the rectangle: |
| newRect = rectUnion((int)StrictMath.rint(strokeTip.x), (int)StrictMath.rint(strokeTip.y), |
| (int)StrictMath.rint(strokeTip.x), (int)StrictMath.rint(strokeTip.y), newRect); |
| } |
| return newRect; |
| } |
| |
| /** |
| * Returns true when a joint is an inner joint (lies |
| * miterLimit*strokeWeight/2 away from pathBox). |
| * @param jointPoint |
| * @param miterLimit |
| * @param weight |
| * @return |
| */ |
| private static boolean isInnerJoint(Point jointPoint, Rect pathBBox, double miterLimit, double halfWeight) |
| { |
| //If a joint lies miterLimit*strokeWeight/2 away from pathBox, |
| //it is considered an inner joint and has no effect on bounds. |
| if ((jointPoint.x - pathBBox.xMin())>miterLimit*halfWeight && |
| (pathBBox.xMax() - jointPoint.x)>miterLimit*halfWeight && |
| (jointPoint.y - pathBBox.yMin())>miterLimit*halfWeight && |
| (pathBBox.yMax() - jointPoint.y)>miterLimit*halfWeight) |
| { |
| return true; |
| } |
| else |
| { |
| return false; |
| } |
| } |
| |
| /** |
| * Returns true when we have a valid tangent for curSegment. Pass |
| * prevSegment to know what the starting point of curSegment is. |
| * @param prevSegment |
| * @param curSegment |
| * @param matrix |
| * @return true where there is a valid tangent for curSegment. Returns false |
| * otherwise. |
| */ |
| private static boolean tangentIsValid(ShapeRecord curSegment, int x, int y) |
| { |
| // Check the start tangent only. If it's valid, |
| // then there is a valid end tangent as well. |
| Point tangentPoint = getTangent(curSegment, true, x, y); |
| return (tangentPoint.x != 0 || tangentPoint.y != 0); |
| } |
| |
| private static Point getTangent(ShapeRecord curSegment, boolean start, int x, int y) |
| { |
| Point tangentPoint = new Point(); |
| Point pt0 = new Point(x, y); |
| |
| if (curSegment instanceof StraightEdgeRecord) |
| { |
| Point pt1 = new Point(x+((StraightEdgeRecord)curSegment).getDeltaX(), y+((StraightEdgeRecord)curSegment).getDeltaY()); |
| tangentPoint.x = pt1.x - pt0.x; |
| tangentPoint.y = pt1.y - pt0.y; |
| } |
| else if (curSegment instanceof CurvedEdgeRecord) |
| { |
| Point pt1 = new Point(x+((CurvedEdgeRecord)curSegment).getControlDeltaX(), y+((CurvedEdgeRecord)curSegment).getControlDeltaY()); |
| Point pt2 = new Point(pt1.x+((CurvedEdgeRecord)curSegment).getAnchorDeltaX(), pt1.y+((CurvedEdgeRecord)curSegment).getAnchorDeltaY()); |
| tangentPoint = getQTangent(pt0.x, pt0.y, pt1.x, pt1.y, pt2.x, pt2.y, start); |
| } |
| return tangentPoint; |
| } |
| |
| private static Point getQTangent(double x0, double y0, double x1, double y1, double x2, double y2, boolean start) |
| { |
| Point tangentPoint = new Point(); |
| if (start) |
| { |
| if (x0 == x1 && y0 == y1) |
| { |
| tangentPoint.x = x2 - x0; |
| tangentPoint.y = y2 - y0; |
| } |
| else |
| { |
| tangentPoint.x = x1 - x0; |
| tangentPoint.y = y1 - y0; |
| } |
| } |
| else |
| { |
| if (x2 == x1 && y2 == y1) |
| { |
| tangentPoint.x = x2 - x0; |
| tangentPoint.y = y2 - y0; |
| } |
| else |
| { |
| tangentPoint.x = x2 - x1; |
| tangentPoint.y = y2 - y1; |
| } |
| } |
| return tangentPoint; |
| } |
| |
| /** |
| * Normalize a point. Scales the line segment between (0,0) and the current |
| * point to a set length. For example, if the current point is (0,5), and |
| * you normalize it to 1, the point returned is at (0,1). |
| */ |
| public static Point normalize(Point p, double length) |
| { |
| double len = Math.sqrt(p.x * p.x + p.y * p.y); |
| length = length/len; |
| return new Point(p.x * length, p.y * length); |
| } |
| |
| /** |
| * Get length of a point. |
| */ |
| public static double getPointLength(Point p) |
| { |
| double length; |
| if (p.x == 0) |
| { |
| length = p.y; |
| } |
| else |
| { |
| length = Math.sqrt(p.x*p.x + p.y*p.y); |
| } |
| return length; |
| } |
| /** |
| * @return Returns the union of <code>rect</code> and |
| * <code>Rectangle(left, top, right - left, bottom - top)</code>. |
| * Note that if rect is non-null, it will be updated to reflect the return value. |
| */ |
| private static Rect rectUnion(int left, int top, int right, int bottom, Rect rect) |
| { |
| if (rect == null) |
| { |
| return new Rect(left, right, top, bottom); |
| } |
| return new Rect(Math.min(rect.xMin(), left), Math.max(rect.xMax(), right), |
| Math.min(rect.yMin(), top), Math.max(rect.yMax(), bottom)); |
| } |
| |
| private static Rect getStrokeExtents(AbstractStrokeNode stroke, LineStyle ls ) |
| { |
| // TODO: currently we take only scale into account, |
| // but depending on joint style, cap style, etc. we need to take |
| // the whole matrix into account as well as examine every line segment. |
| if (stroke == null) |
| { |
| return new Rect(0, 0, 0 , 0); |
| } |
| |
| int xMin, xMax, yMin, yMax; |
| // Stroke with weight 0 or scaleMode "none" is always drawn |
| // at "hairline" thickness, which is exactly one pixel. |
| int lineWidth = ls.getWidth(); |
| if (lineWidth == 0) |
| { |
| xMin = (int)StrictMath.rint(-0.5 * TWIPS_PER_PIXEL); |
| xMax = (int)StrictMath.rint(0.5 * TWIPS_PER_PIXEL); |
| yMin = (int)StrictMath.rint(-0.5 * TWIPS_PER_PIXEL); |
| yMax = (int)StrictMath.rint(0.5 * TWIPS_PER_PIXEL); |
| } |
| else |
| { |
| xMin = (int)StrictMath.rint(-lineWidth * 0.5); |
| xMax = (int)StrictMath.rint(lineWidth * 0.5); |
| yMin = (int)StrictMath.rint(-lineWidth * 0.5); |
| yMax = (int)StrictMath.rint(lineWidth * 0.5); |
| } |
| return new Rect(xMin, xMax, yMin, yMax); |
| } |
| |
| |
| private static Rect computeCurveBounds(int x0, int y0, CurvedEdgeRecord curve) |
| { |
| int x1 = x0 + curve.getControlDeltaX(); |
| int y1 = y0 + curve.getControlDeltaY(); |
| int x2 = x1 + curve.getAnchorDeltaX(); |
| int y2 = y1 + curve.getAnchorDeltaY(); |
| |
| //initialize xmin, ymin, xmax, ymax to the anchor points of curve |
| int xmin = x0, xmax = x0; |
| int ymin = y0, ymax = y0; |
| if (x2 < xmin) xmin = x2; |
| if (y2 < ymin) ymin = y2; |
| if (x2 > xmax) xmax = x2; |
| if (y2 > ymax) ymax = y2; |
| |
| //compute t at extrema point for x and the corresponding x, y values |
| double t = computeTExtrema(x0, x1, x2); |
| if (Double.isNaN(t)) |
| { |
| //use control point |
| if (x1 < xmin) xmin = x1; |
| if (y1 < ymin) ymin = y1; |
| if (x1 > xmax) xmax = x1; |
| if (y1 > ymax) ymax = y1; |
| } |
| else if ((t > 0) && (t < 1)) |
| { |
| int x, y; |
| x = computeValueForCurve(x0, x1, x2, t); |
| y = computeValueForCurve(y0, y1, y2, t); |
| if (x < xmin) xmin = x; |
| if (y < ymin) ymin = y; |
| if (x > xmax) xmax = x; |
| if (y > ymax) ymax = y; |
| } |
| |
| //compute t at extrema point for y and the corresponding x, y values |
| t = computeTExtrema(y0, y1, y2); |
| if (Double.isNaN(t)) |
| { |
| //use control point |
| if (x1 < xmin) xmin = x1; |
| if (y1 < ymin) ymin = y1; |
| if (x1 > xmax) xmax = x1; |
| if (y1 > ymax) ymax = y1; |
| } |
| else if ((t > 0) && (t < 1)) |
| { |
| int x, y; |
| x = computeValueForCurve(x0, x1, x2, t); |
| y = computeValueForCurve(y0, y1, y2, t); |
| if (x < xmin) xmin = x; |
| if (y < ymin) ymin = y; |
| if (x > xmax) xmax = x; |
| if (y > ymax) ymax = y; |
| } |
| |
| Rect r = new Rect(xmin, xmax, ymin, ymax); |
| return r; |
| } |
| |
| private static boolean curveControlPointInsideCurrentRect(int x0, int y0, CurvedEdgeRecord curve, Rect currRect) |
| { |
| int x = x0 + curve.getControlDeltaX(); |
| int y = y0 + curve.getControlDeltaY(); |
| |
| //initialize xmin, ymin, xmax, ymax to the control points of curve |
| int xmin = x0, xmax = x0; |
| int ymin = y0, ymax = y0; |
| if (x < xmin) xmin = x; |
| if (y < ymin) ymin = y; |
| if (x > xmax) xmax = x; |
| if (y > ymax) ymax = y; |
| |
| if ((currRect.xMin() < xmin) && (currRect.xMax() > xmax) && (currRect.yMin() < ymin) && (currRect.yMax() > ymax)) |
| return true; |
| else |
| return false; |
| } |
| |
| //compute value for quadratic bezier curve at t |
| // the quadratic bezier curve is p0*(1-t)^2 + 2*p1*(1-t)*t + p2*t^2 |
| private static int computeValueForCurve(int p0, int p1, int p2, double t) |
| { |
| return (int)(p0*(1-t)*(1-t) + 2*p1*(1-t)*t + p2*t*t); |
| |
| } |
| |
| //compute the extrema which corresponds to derivative equal to 0 |
| private static double computeTExtrema(int p0, int p1, int p2) |
| { |
| // the quadratic bezier curve is p0*(1-t)^2 + 2*p1*(1-t)*t + p2*t^2, |
| // its first derivative (with respect to t) is 2*(p0 - 2*p1 + p2)*t + 2*(p1 - p0), |
| // which is zero for t = (p0 - p1)/(p0 - 2*p1 + p2) |
| |
| int denom = (p0 - 2*p1 + p2); |
| if (denom == 0) |
| { |
| //cannot compute the derivative - use the control point for extrema |
| return Double.NaN; |
| } |
| else |
| { |
| double t = (p0 - p1)/(double) denom; |
| return t; |
| } |
| |
| } |
| |
| private static double[] getCornerRadius(double cornerRadiusX, double cornerRadiusY, |
| double radiusX, double radiusY, double width, double height) |
| { |
| double[] newRadius = new double[2]; |
| if (Double.isNaN(cornerRadiusX)) |
| { |
| cornerRadiusX = radiusX; |
| if (Double.isNaN(cornerRadiusY)) |
| cornerRadiusY = radiusY; |
| else |
| cornerRadiusY = cornerRadiusX; |
| } |
| else if (Double.isNaN(cornerRadiusY)) |
| { |
| cornerRadiusY = cornerRadiusX; |
| } |
| if ( cornerRadiusX > width/2.0 ) |
| cornerRadiusX = width/2.0; |
| if ( cornerRadiusY > height/2.0 ) |
| cornerRadiusY = height/2.0; |
| |
| newRadius[0] = cornerRadiusX; |
| newRadius[1] = cornerRadiusY; |
| return newRadius; |
| } |
| |
| } |