| /* |
| * |
| * 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 com.adobe.internal.fxg.swf; |
| |
| import java.util.ArrayList; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.regex.Matcher; |
| import java.util.regex.Pattern; |
| |
| import com.adobe.fxg.FXGException; |
| import com.adobe.internal.fxg.dom.AbstractFXGNode; |
| import com.adobe.internal.fxg.dom.PathNode; |
| import com.adobe.internal.fxg.dom.strokes.AbstractStrokeNode; |
| |
| import flash.swf.SwfConstants; |
| import flash.swf.builder.types.Point; |
| import flash.swf.types.CurvedEdgeRecord; |
| import flash.swf.types.LineStyle; |
| import flash.swf.types.Rect; |
| import flash.swf.types.ShapeRecord; |
| import flash.swf.types.StraightEdgeRecord; |
| import flash.swf.types.StyleChangeRecord; |
| |
| /** |
| * A collection of utilities to help create SWF Shapes and ShapeRecords. |
| * |
| * @author Peter Farland |
| * @author Sujata Das |
| * @author Min Plunkett |
| */ |
| public class ShapeHelper implements SwfConstants |
| { |
| private static final Pattern charNumberPattern = Pattern.compile("([A-Za-z])([0-9\\-\\.])", Pattern.UNICODE_CASE); |
| private static final Pattern numberCharPattern = Pattern.compile("([0-9\\.])([A-Za-z\\-])", Pattern.UNICODE_CASE); |
| private static final Pattern charCharPattern = Pattern.compile("([A-Za-z\\-])([A-Za-z\\-])", Pattern.UNICODE_CASE); |
| private static final Pattern scientificPattern = Pattern.compile("([0-9])( )([eE])( )([0-9\\-])", Pattern.UNICODE_CASE); |
| private static final Pattern commaPattern = Pattern.compile(",", Pattern.UNICODE_CASE); |
| private static final Pattern spacePattern = Pattern.compile("\\s+", Pattern.UNICODE_CASE); |
| |
| /** |
| * 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.setLinestyle(0); |
| 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 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 |
| * @param startx the startx |
| * @param starty the starty |
| * @param topLeftRadiusX the top left radius x |
| * @param topLeftRadiusY the top left radius y |
| * @param topRightRadiusX the top right radius x |
| * @param topRightRadiusY the top right radius y |
| * @param bottomLeftRadiusX the bottom left radius x |
| * @param bottomLeftRadiusY the bottom left radius y |
| * @param bottomRightRadiusX the bottom right radius x |
| * @param bottomRightRadiusY the bottom right radius y |
| * |
| * @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) |
| { |
| |
| 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 = 0.923879532511; |
| double c1 = 0.382683432365; |
| double c3 = 0.707106781187; |
| |
| double rx = bottomRightRadiusX; |
| double ry = bottomRightRadiusY; |
| |
| double tx = rx / 0.923879532511; |
| double ty = ry / 0.923879532511; |
| |
| 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 / 0.923879532511; |
| ty = ry / 0.923879532511; |
| 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 / 0.923879532511; |
| ty = ry / 0.923879532511; |
| 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 / 0.923879532511; |
| ty = ry / 0.923879532511; |
| 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 / 0.923879532511; |
| ty = ry / 0.923879532511; |
| 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 |
| * @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) |
| { |
| if (shapeRecords != null && shapeRecords.size() > 0) |
| { |
| ShapeRecord firstRecord = shapeRecords.get(0); |
| if (firstRecord instanceof StyleChangeRecord) |
| { |
| StyleChangeRecord scr = (StyleChangeRecord)firstRecord; |
| |
| if (fillStyle0Index > 0) |
| scr.setFillStyle0(fillStyle0Index); |
| |
| if (fillStyle1Index > 0) |
| scr.setFillStyle1(fillStyle1Index); |
| |
| if (lineStyleIndex > 0) |
| scr.setLinestyle(lineStyleIndex); |
| } |
| } |
| } |
| |
| |
| /** |
| * Sets the style information for the all the StyleChangeRecords in a list |
| * of ShapeRecords. |
| * |
| * @param shapeRecords |
| * @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) |
| { |
| |
| 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; |
| |
| if (fillStyle0Index > 0) |
| scr.setFillStyle0(fillStyle0Index); |
| |
| if (fillStyle1Index > 0) |
| scr.setFillStyle1(fillStyle1Index); |
| |
| if ((!scr.stateLineStyle) && (lineStyleIndex > 0)) |
| scr.setLinestyle(lineStyleIndex); |
| |
| } |
| } |
| } |
| } |
| |
| /** |
| * Replaces the style information for the all the StyleChangeRecords in a list |
| * of ShapeRecords. |
| * |
| * @param shapeRecords |
| * @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) |
| { |
| |
| 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(); |
| |
| if (fillStyle0Index > 0) |
| new_scr.setFillStyle0(fillStyle0Index); |
| |
| if (fillStyle1Index > 0) |
| new_scr.setFillStyle1(fillStyle1Index); |
| |
| if ((!old_scr.stateLineStyle) && (lineStyleIndex > 0)) |
| new_scr.setLinestyle(lineStyleIndex); |
| else |
| new_scr.setLinestyle(old_scr.linestyle); |
| |
| if (old_scr.stateMoveTo) |
| new_scr.setMove(old_scr.moveDeltaX, old_scr.moveDeltaY); |
| |
| 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; |
| } |
| |
| /** 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. */ |
| private static final int MAX_EDGE_SIZE = 65535; |
| |
| /** |
| * Straight edge. |
| * |
| * @param xFrom the x from |
| * @param yFrom the y from |
| * @param xTo the x to |
| * @param yTo the y to |
| * |
| * @return the list< shape record> |
| */ |
| 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 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. |
| * @param startX the start x |
| * @param startY the start y |
| * |
| * @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.controlDeltaX = dcx; |
| cer.controlDeltaY = dcy; |
| cer.anchorDeltaX = dax; |
| cer.anchorDeltaY = 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 "http://timotheegroleau.com/Flash/articles/cubic_bezier_in_flash.htm"} |
| */ |
| 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. |
| * |
| * @param node the node |
| * @param fill the fill |
| * |
| * @return the list< shape record> |
| */ |
| public static List<ShapeRecord> path(PathNode node, boolean fill) |
| { |
| String data = node.data; |
| |
| List<ShapeRecord> shapeRecords = new ArrayList<ShapeRecord>(); |
| |
| if (data.length() == 0) |
| return shapeRecords; |
| |
| String temp = data; |
| // Split letter followed by number (i.e. "M3" becomes "M 3") |
| Matcher m = charNumberPattern.matcher(data); |
| if (m.find()) |
| { |
| temp = m.replaceAll("$1 $2"); |
| } |
| |
| // Split number followed by letter (i.e. "3M" becomes "3 M") |
| m = numberCharPattern.matcher(temp); |
| temp = m.replaceAll("$1 $2"); |
| |
| // Split letter followed by letter (i.e. "zM" becomes "z M") |
| m = charCharPattern.matcher(temp); |
| temp = m.replaceAll("$1 $2"); |
| |
| //support scientific notation for floats/doubles |
| m = scientificPattern.matcher(temp); |
| temp = m.replaceAll("$1$3$5"); |
| |
| // Replace commas with spaces |
| m = commaPattern.matcher(temp); |
| temp = m.replaceAll(" "); |
| |
| // Trim leading and trailing spaces |
| temp = temp.trim(); |
| |
| // Finally, split the string into an array |
| String[] args = spacePattern.split(temp); |
| |
| 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')) |
| throw new FXGException(node.getStartLine(), node.getStartColumn(), "InvalidPathData"); |
| |
| 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: |
| throw new FXGException(node.getStartLine(), node.getStartColumn(), "InvalidPathData"); |
| |
| } |
| |
| 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. |
| * |
| * @param records the records |
| * @param ls the ls |
| * @param strokeNode the stroke node |
| * |
| * @return bounding box rectangle. |
| */ |
| public static Rect getBounds(List<ShapeRecord> records, LineStyle ls, AbstractStrokeNode strokeNode) |
| { |
| if (records == null || records.size() == 0) |
| return new Rect(); |
| |
| 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.moveDeltaX; |
| y = scr.moveDeltaY; |
| if (firstMove) |
| { |
| x1 = x; |
| y1 = y; |
| x2 = x; |
| y2 = y; |
| firstMove = false; |
| } |
| } |
| else if (r instanceof StraightEdgeRecord) |
| { |
| StraightEdgeRecord ser = (StraightEdgeRecord)r; |
| x = x + ser.deltaX; |
| y = y + ser.deltaY; |
| } |
| 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.controlDeltaX + cer.anchorDeltaX; |
| y = y + cer.controlDeltaY + cer.anchorDeltaY; |
| } |
| |
| //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.xMin -= strokeExtents.xMax; |
| newRect.yMin -= strokeExtents.yMax; |
| newRect.xMax += strokeExtents.xMax; |
| newRect.yMax += strokeExtents.yMax; |
| |
| // If there are less than two segments, then or joint style is not |
| //"miterLimit" finish - return pathBBox. |
| if (records.size() < 2 || ls == null || !ls.hasMiterJoint()) |
| { |
| 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; |
| } |
| |
| /** |
| * Adds the joint to bounds. |
| * |
| * @param records the records |
| * @param ls the line style |
| * @param stroke the stroke |
| * @param halfWeight the half weight |
| * @param pathBBox the path b box |
| * |
| * @return the rect |
| */ |
| 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).moveDeltaX; |
| lastMoveY = ((StyleChangeRecord)startSegment).moveDeltaY; |
| |
| // 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.moveDeltaX; |
| coordinates[i][1] = scr.moveDeltaY; |
| } |
| else if (record instanceof StraightEdgeRecord) |
| { |
| StraightEdgeRecord ser = (StraightEdgeRecord)record; |
| coordinates[i][0] = coordinates[i-1][0] + ser.deltaX; |
| coordinates[i][1] = coordinates[i-1][1] + ser.deltaY; |
| } |
| else if (record instanceof CurvedEdgeRecord) |
| { |
| CurvedEdgeRecord cer = (CurvedEdgeRecord)record; |
| coordinates[i][0] = coordinates[i-1][0] + cer.controlDeltaX + cer.anchorDeltaX; |
| coordinates[i][1] = coordinates[i-1][1] + cer.controlDeltaY + cer.anchorDeltaY; |
| } |
| } |
| return coordinates; |
| } |
| |
| |
| /** |
| * Adds the miter limit stroke to bounds. |
| * |
| * @param segment1 the segment1 |
| * @param segment2 the segment2 |
| * @param miterLimit the miter limit |
| * @param halfWeight the half weight |
| * @param pathBBox the path b box |
| * @param xPrev the x prev |
| * @param yPrev the y prev |
| * @param x the x |
| * @param y the y |
| * |
| * @return the rect |
| */ |
| 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).deltaX, y+((StraightEdgeRecord)curSegment).deltaY); |
| tangentPoint.x = pt1.x - pt0.x; |
| tangentPoint.y = pt1.y - pt0.y; |
| } |
| else if (curSegment instanceof CurvedEdgeRecord) |
| { |
| Point pt1 = new Point(x+((CurvedEdgeRecord)curSegment).controlDeltaX, y+((CurvedEdgeRecord)curSegment).controlDeltaY); |
| Point pt2 = new Point(pt1.x+((CurvedEdgeRecord)curSegment).anchorDeltaX, pt1.y+((CurvedEdgeRecord)curSegment).anchorDeltaY); |
| 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). |
| * |
| * @param p the p |
| * @param length the length |
| * |
| * @return the point |
| */ |
| 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. |
| * |
| * @param p |
| * @return length |
| */ |
| 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) |
| { |
| Rect newRect = new Rect(); |
| if (rect == null) |
| { |
| newRect = new Rect(left, right, top, bottom); |
| return newRect; |
| } |
| |
| newRect.xMin = Math.min(rect.xMin, left); |
| newRect.yMin = Math.min(rect.yMin, top); |
| newRect.xMax = Math.max(rect.xMax, right); |
| newRect.yMax = Math.max(rect.yMax, bottom); |
| return newRect; |
| } |
| |
| private static Rect getStrokeExtents(AbstractStrokeNode stroke, LineStyle ls ) |
| { |
| // TODO (egeorgie): 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.width; |
| 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.controlDeltaX; |
| int y1 = y0 + curve.controlDeltaY; |
| int x2 = x1 + curve.anchorDeltaX; |
| int y2 = y1 + curve.anchorDeltaY; |
| |
| //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.controlDeltaX; |
| int y = y0 + curve.controlDeltaY; |
| |
| //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; |
| } |
| |
| } |