diff --git a/CHANGELOG.md b/CHANGELOG.md index 63bb755ea..cb91181de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -113,7 +113,8 @@ All notable changes to this project will be documented in this file. - [#2471] PDF export - ignore control characters - [#2471] SVG export with typeface - white-space:pre style - [#1826], [#2416] FLA export - shapes - missing fills when fillStyle0/1 has incorrect orientation -- [#2532], [#1011], [#2165] FLA export - shapes - missing fills on path crossings, small shapes +- [#2532], [#1011], [#2165] FLA export - shapes - missing fills on path crossings, small shapes, + Optimized speed of Shape fixer ### Changed - Icon of "Deobfuscation options" menu from pile of pills to medkit diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/math/BezierEdge.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/math/BezierEdge.java index 31abb585c..f306188b6 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/math/BezierEdge.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/math/BezierEdge.java @@ -67,6 +67,7 @@ public class BezierEdge implements Serializable { /** * Constructor. + * * @param points Points */ public BezierEdge(List points) { @@ -81,6 +82,7 @@ public class BezierEdge implements Serializable { /** * Is the edge empty? + * * @return True if the edge is empty, false otherwise. */ public boolean isEmpty() { @@ -89,6 +91,7 @@ public class BezierEdge implements Serializable { /** * Constructor. + * * @param x0 Start x * @param y0 Start y * @param x1 End x @@ -102,6 +105,7 @@ public class BezierEdge implements Serializable { /** * Constructor. + * * @param x0 Start x * @param y0 Start y * @param cx Control x @@ -118,6 +122,7 @@ public class BezierEdge implements Serializable { /** * Gets the begin point. + * * @return Begin point */ public Point2D getBeginPoint() { @@ -126,6 +131,7 @@ public class BezierEdge implements Serializable { /** * Gets the end point. + * * @return End point */ public Point2D getEndPoint() { @@ -134,6 +140,7 @@ public class BezierEdge implements Serializable { /** * Sets the begin point. + * * @param p Begin point */ public void setBeginPoint(Point2D p) { @@ -143,6 +150,7 @@ public class BezierEdge implements Serializable { /** * Sets the end point. + * * @param p End point */ public void setEndPoint(Point2D p) { @@ -152,6 +160,7 @@ public class BezierEdge implements Serializable { /** * Gets the point at specified position. + * * @param t Position * @return Point at position */ @@ -172,26 +181,31 @@ public class BezierEdge implements Serializable { } private void calcParams() { - double minX = Double.MAX_VALUE; - double minY = Double.MAX_VALUE; - double maxX = -Double.MAX_VALUE; - double maxY = -Double.MAX_VALUE; - for (Point2D p : points) { - if (p.getX() < minX) { - minX = p.getX(); - } - if (p.getX() > maxX) { - maxX = p.getX(); - } - if (p.getY() < minY) { - minY = p.getY(); - } - if (p.getY() > maxY) { - maxY = p.getY(); + if (points.size() == 2) { + double minX = Double.MAX_VALUE; + double minY = Double.MAX_VALUE; + double maxX = -Double.MAX_VALUE; + double maxY = -Double.MAX_VALUE; + for (Point2D p : points) { + if (p.getX() < minX) { + minX = p.getX(); + } + if (p.getX() > maxX) { + maxX = p.getX(); + } + if (p.getY() < minY) { + minY = p.getY(); + } + if (p.getY() > maxY) { + maxY = p.getY(); + } } + + this.bbox = new Rectangle2D.Double(minX, minY, maxX - minX, maxY - minY); + } else { + this.bbox = quadraticBBox(points.get(0), points.get(1), points.get(2)); } - this.bbox = new Rectangle2D.Double(minX, minY, maxX - minX, maxY - minY); this.hash = points.hashCode(); revPoints = new ArrayList<>(); @@ -212,6 +226,7 @@ public class BezierEdge implements Serializable { /** * Calculates the bounding box. + * * @return Bounding box */ public Rectangle2D bbox() { @@ -222,6 +237,7 @@ public class BezierEdge implements Serializable { /** * Calculates the area. + * * @return Area */ public double area() { @@ -258,6 +274,7 @@ public class BezierEdge implements Serializable { /** * Gets the intersections. + * * @param b2 Bezier edge * @return List of intersections */ @@ -282,6 +299,7 @@ public class BezierEdge implements Serializable { /** * Gets the intersections. Old version. + * * @param b2 Bezier edge 2 * @param t1Ref T1 reference * @param t2Ref T2 reference @@ -329,6 +347,7 @@ public class BezierEdge implements Serializable { /** * Checks if the edges intersect. + * * @param b2 Bezier edge 2 * @param t1Ref T1 reference * @param t2Ref T2 reference @@ -420,6 +439,7 @@ public class BezierEdge implements Serializable { /** * Gets the length. + * * @return Length */ public double length() { @@ -456,6 +476,7 @@ public class BezierEdge implements Serializable { /** * Converts the edge to SVG. + * * @return SVG string */ public String toSvg() { @@ -488,6 +509,7 @@ public class BezierEdge implements Serializable { /** * Splits the edge. + * * @param t Position * @param left Left edge * @param right Right edge @@ -502,9 +524,30 @@ public class BezierEdge implements Serializable { right.setVal(new BezierEdge(rightPoints)); } + public List split(List tList) { + List result = new ArrayList<>(); + + double prevT = 0.0; + BezierEdge remaining = this; + + for (double t : tList) { + double localT = (t - prevT) / (1.0 - prevT); + + Reference leftRef = new Reference<>(null); + Reference rightRef = new Reference<>(null); + remaining.split(localT, leftRef, rightRef); + result.add(leftRef.getVal()); + remaining = rightRef.getVal(); + prevT = t; + } + + result.add(remaining); + return result; + } /** * Reverses the edge. + * * @return Reversed edge */ public BezierEdge reverse() { @@ -536,9 +579,9 @@ public class BezierEdge implements Serializable { } calcParams(); } - - public static final double ROUND_VALUE = 1; - + + public static final double ROUND_VALUE = 2; + public void roundX() { for (int i = 0; i < this.points.size(); i++) { this.points.set(i, new Point2D.Double( @@ -576,6 +619,7 @@ public class BezierEdge implements Serializable { /** * Checks if the edge is equal to another edge in reverse order. + * * @param other Other edge * @return True if the edges are equal in reverse order, false otherwise */ @@ -594,163 +638,12 @@ public class BezierEdge implements Serializable { return true; } - /** - * Test main. - * @param args Arguments - */ - public static void main(String[] args) { - List t1 = new ArrayList<>(); - List t2 = new ArrayList<>(); - /*/BezierEdge be1 = new BezierEdge(0,0,100,100); - BezierEdge be2 = new BezierEdge(75,100,75,0); - - System.out.println("lines "+ be1+ " and "+be2); - - - - - System.out.println("hasIntersection = "+be1.intersects(be2, t1, t2)); - System.out.println("intersection points: "+t1+", "+t2); - - - BezierEdge be3 = new BezierEdge(0,0,100,0); - BezierEdge be4 = new BezierEdge(0,100,100,100); - - System.out.println("lines "+ be3+ " and "+be4); - - System.out.println("hasIntersection = "+be3.intersects(be4, t1, t2)); - System.out.println("intersection points: "+t1+", "+t2);*/ - - BezierEdge be5 = new BezierEdge(25, 0, 25, 100); - BezierEdge be6 = new BezierEdge(0, 50, 100, 50); - - //System.err.println("lines " + be5 + " and " + be6); - BezierEdge q1 = new BezierEdge(3469, 3124, 3320, 3148, 3355, 3215); - BezierEdge q2 = new BezierEdge(3442, 3191, 3316, 3146, 3317, 3071); - BezierEdge q3 = new BezierEdge(3310, 3222, 3450, 3172, 3300, 3181); - BezierEdge li = new BezierEdge(3423, 3040, 3277, 3164); - BezierEdge li2 = new BezierEdge(3399, 3095, 3365, 3039); - - List ints; - /*ints = q2.getIntersections(q1); - System.err.println("intersections is "+ints); - ints = q2.getIntersections(li); - System.err.println("intersections is "+ints); - ints = li2.getIntersections(li); - System.err.println("intersections is "+ints); - ints = q1.getIntersections(q3); - System.err.println("intersections is "+ints);*/ - - BezierEdge qa = new BezierEdge(-81.0, -78.0, -85.0, -76.0, -86.0, -66.0); - BezierEdge qb = new BezierEdge(-166.0, 37.0, -172.0, -21.0, -81.0, -78.0); - /*ints = qa.getIntersections(qb); - System.err.println("intersections is " + ints); - BezierEdge qc = new BezierEdge(-106.0,39.0, -104.0,33.0); - BezierEdge qd = new BezierEdge(-104.0,33.0,-105.0,36.0,-102.0,26.0); - ints = qc.getIntersections(qd); - System.err.println("intersections is " + ints); - - ints = Intersections.intersectLineLine(new Point2D.Double(0,0), new Point2D.Double(10,0), new Point2D.Double(2,0), new Point2D.Double(5,0), true); - System.err.println("intersections is " + ints); - BezierEdge qe = new BezierEdge(-104,33,-104.5,35,-104,32); - BezierEdge qf = new BezierEdge(-106,39 ,-104,33); - ints = qe.getIntersections(qf);*/ - BezierEdge qg = new BezierEdge(-66, 139, -67, 140, -61, 135); - BezierEdge qh = new BezierEdge(-64, 169, -66.5, 139.5, -66, 139); - - //Error Y values of bounds must be of opposite sign - BezierEdge qi = new BezierEdge(6369.0, 13040.0, 6380.0, 13030.0, 6427.0, 13018.0); - BezierEdge qj = new BezierEdge(6338.0, 13099.0, 6358.0, 13050.0, 6369.0, 13040.0); - - //Error Y values of bounds must be of opposite sign - BezierEdge qk = new BezierEdge(45334.0, 2421.0, 45348.0, 2373.0, 45348.0, 2330.0); - BezierEdge ql = new BezierEdge(45348.0, 2330.0, 45348.0, 2263.0, 45314.0, 2223.0); - - BezierEdge qm = new BezierEdge(-1957, 2676, -1957, 2676.5, -1957.5, 2676.5); - BezierEdge qn = new BezierEdge(-1957, 2675.5, -1957, 2676, -1957, 2676); - - /*List ps = new ArrayList<>(); - qm.intersects(qn, t1, t2, ps); - System.err.println("t1 is " + t1); - System.err.println("t2 is " + t2); - System.err.println("intersections is " + ps); - */ - /*Shape r1 = new Rectangle2D.Double(0, 0, 200, 100); - GeneralPath r2 = new GeneralPath(GeneralPath.WIND_EVEN_ODD); - r2.moveTo(0, 0); - r2.lineTo(100, 0); - r2.lineTo(150, 0); - r2.lineTo(200, 0); - r2.lineTo(200, 100); - r2.lineTo(0, 100); - r2.closePath(); - - GeneralPath sh1 = new GeneralPath(GeneralPath.WIND_EVEN_ODD); - sh1.moveTo(0, 0); - sh1.quadTo(100, 100, 0, 100); - sh1.lineTo(0, 0); - sh1.closePath(); - - BezierEdge bex = new BezierEdge(0, 0, 100, 100, 0, 100); - Reference bex1Ref = new Reference<>(null); - Reference bex2Ref = new Reference<>(null); - bex.split(0.2, bex1Ref, bex2Ref); - BezierEdge bex1 = bex1Ref.getVal(); - BezierEdge bex2 = bex2Ref.getVal(); - GeneralPath sh2 = new GeneralPath(GeneralPath.WIND_EVEN_ODD); - sh2.moveTo(bex1.getBeginPoint().getX(), bex1.getBeginPoint().getY()); - sh2.quadTo(10+bex1.points.get(1).getX(), bex1.points.get(1).getY(), bex1.getEndPoint().getX(), bex1.getEndPoint().getY()); - sh2.quadTo(bex2.points.get(1).getX(), bex2.points.get(1).getY(), bex2.getEndPoint().getX(), bex2.getEndPoint().getY()); - sh2.lineTo(0, 0); - sh2.closePath(); - - Area a1 = new Area(r1); - Area a2 = new Area(r2); - //a1.exclusiveOr(a2); - - - */ - /*GeneralPath p1 = new GeneralPath(GeneralPath.WIND_EVEN_ODD); - p1.moveTo(0, 0); - p1.lineTo(200, 0); - p1.lineTo(200, 200); - p1.lineTo(0, 200); - p1.lineTo(0, 0); - p1.closePath(); - p1.moveTo(50, 50); - p1.lineTo(150, 50); - p1.lineTo(150, 150); - p1.lineTo(50, 150); - p1.lineTo(50, 50); - p1.closePath(); - - System.err.println("cont:" + p1.contains(100, 100)); - System.err.println("cont:" + p1.contains(150, 150)); */ - //System.err.println("minDist = " + minDist+", maxDist = "+ maxDist); - //System.err.println("eArea = " + Areas.calcArea(a1)); - - /*Point2D c = new Point2D.Double( - (1 - t1.get(0)) * be5.points.get(0).getX() + t1.get(0) * be5.points.get(1).getX(), - (1 - t1.get(0)) * be5.points.get(0).getY() + t1.get(0) * be5.points.get(1).getY() - ); - - System.out.println("Intersection point: " + c); - - BezierEdge be = new BezierEdge(0, 0, 100, 50, 0, 100); - - System.out.println("be5.dist: " + be.length());*/ - //Rectangle2D out = new Rectangle2D.Double(); - //rectIntersection(new Rectangle2D.Double(0,0,50,50), new Rectangle2D.Double(0,50,50,50), out); - //System.out.println("out = "+out); - } - - public void shrinkToLine() { if (points.size() == 3) { - double det = (points.get(1).getX() - points.get(0).getX()) - * (points.get(2).getY() - points.get(0).getY()) - - (points.get(1).getY() - points.get(0).getY()) - * (points.get(2).getX() - points.get(0).getX()); + double det = (points.get(1).getX() - points.get(0).getX()) + * (points.get(2).getY() - points.get(0).getY()) + - (points.get(1).getY() - points.get(0).getY()) + * (points.get(2).getX() - points.get(0).getX()); if (det == 0) { points.remove(1); revPoints.remove(1); @@ -758,4 +651,175 @@ public class BezierEdge implements Serializable { } } } + + public Double findXCriticalT() { + double EPS = 1e-12; + + if (points.size() < 3) { + return null; + } + + // dx/dt = 2 * ((x1 - x0) + t * (x2 - 2*x1 + x0)) + final double a = (points.get(2).getX() - 2.0 * points.get(1).getX() + points.get(0).getX()); // coefficient of t + final double b = (points.get(1).getX() - points.get(0).getX()); // constant term + + // If a ~ 0, dx/dt is (almost) constant -> x(t) is (almost) linear -> x-monotone already. + if (Math.abs(a) < EPS) { + return null; + } + + double t = -b / a; // (x0 - x1) / (x0 - 2*x1 + x2) + + // Accept strictly inside (0,1); relax by EPS to be robust. + if (t > EPS && t < 1.0 - EPS) { + return t; + } + + return null; + } + + private static double quad(double p0, double p1, double p2, double t) { + double u = 1.0 - t; + return u * u * p0 + 2.0 * u * t * p1 + t * t * p2; + } + + public double yAt(double x) { + double EPS = 1e-12; + if (points.size() == 2) { + //line + double x0 = points.get(0).getX(); + double y0 = points.get(0).getY(); + double x1 = points.get(1).getX(); + double y1 = points.get(1).getY(); + double dx = x1 - x0; + + // Guard for near-vertical segments: return stable representative y + if (Math.abs(dx) < EPS) { + return (y0 + y1) * 0.5; + } + + double t = (x - x0) / dx; + if (t < 0) { + t = 0; + } else if (t > 1) { + t = 1; + } + return y0 + t * (y1 - y0); + } + //quad + final double A = (points.get(0).getX() - 2.0 * points.get(1).getX() + points.get(2).getX()); + final double B = 2.0 * (points.get(1).getX() - points.get(0).getX()); + final double C = points.get(0).getX() - x; // solve A t^2 + B t + C = 0 + + double t; + if (Math.abs(A) < EPS) { + t = (Math.abs(B) < EPS) ? 0.5 : (-C / B); + } else { + double disc = B * B - 4.0 * A * C; + if (disc < 0 && disc > -1e-14) { + disc = 0; + } + if (disc < 0) { + t = 0.5; + } else { + double sqrtD = Math.sqrt(disc); + double q = -0.5 * (B + Math.copySign(sqrtD, B)); + double t1 = q / A; + double t2 = (Math.abs(q) < EPS) ? t1 : (C / q); + boolean t1In = t1 > -1e-9 && t1 < 1.0 + 1e-9; + boolean t2In = t2 > -1e-9 && t2 < 1.0 + 1e-9; + if (t1In && !t2In) { + t = t1; + } else if (!t1In && t2In) { + t = t2; + } else if (t1In && t2In) { + t = (Math.abs(t1 - 0.5) < Math.abs(t2 - 0.5)) ? t1 : t2; + } else { + double c1 = Math.min(Math.max(t1, 0.0), 1.0); + double c2 = Math.min(Math.max(t2, 0.0), 1.0); + double x1 = quad(points.get(0).getX(), points.get(1).getX(), points.get(2).getX(), c1); + double x2 = quad(points.get(0).getX(), points.get(1).getX(), points.get(2).getX(), c2); + t = (Math.abs(x1 - x) <= Math.abs(x2 - x)) ? c1 : c2; + } + } + } + if (t < 0) { + t = 0; + } else if (t > 1) { + t = 1; + } + return quad(points.get(0).getY(), points.get(1).getY(), points.get(2).getY(), t); + } + + /** + * Compute B(t) for a quadratic Bezier defined by P0, P1, P2. + */ + private static Point2D.Double evalQuad(Point2D p0, Point2D p1, Point2D p2, double t) { + // All math in double; t assumed in [0,1], but we'll still accept slightly outside due to numeric noise. + double u = 1.0 - t; + double x = u * u * p0.getX() + 2 * u * t * p1.getX() + t * t * p2.getX(); + double y = u * u * p0.getY() + 2 * u * t * p1.getY() + t * t * p2.getY(); + return new Point2D.Double(x, y); + } + + /** + * Compute the bounding box of a quadratic Bezier (P0, P1, P2). We include + * endpoints and inner extrema where the derivative in x or y equals 0. + */ + public static Rectangle2D quadraticBBox(Point2D p0, Point2D p1, Point2D p2) { + // Collect candidate t values: endpoints and potential inner extrema for x and y. + List candidates = new ArrayList<>(); + candidates.add(0.0); + candidates.add(1.0); + + // Derivative B'(t) = 2( (1 - t)(P1 - P0) + t(P2 - P1) ) + // Setting x' = 0 gives linear equation in t with solution: + // t = (P0x - P1x) / (P0x - 2P1x + P2x), if denominator != 0 + double denomX = p0.getX() - 2.0 * p1.getX() + p2.getX(); + double denomY = p0.getY() - 2.0 * p1.getY() + p2.getY(); + + // Small epsilon to avoid floating point issues around 0 denominators + final double EPS = 1e-12; + + if (Math.abs(denomX) > EPS) { + double tx = (p0.getX() - p1.getX()) / denomX; + if (tx > 0.0 && tx < 1.0) { + candidates.add(tx); + } + } + if (Math.abs(denomY) > EPS) { + double ty = (p0.getY() - p1.getY()) / denomY; + if (ty > 0.0 && ty < 1.0) { + candidates.add(ty); + } + } + + // Evaluate all candidate points and take min/max + double minX = Double.POSITIVE_INFINITY; + double minY = Double.POSITIVE_INFINITY; + double maxX = Double.NEGATIVE_INFINITY; + double maxY = Double.NEGATIVE_INFINITY; + + for (double t : candidates) { + // Clamp t just in case of tiny numeric drifts + double tt = Math.max(0.0, Math.min(1.0, t)); + Point2D.Double pt = evalQuad(p0, p1, p2, tt); + double x = pt.x; + double y = pt.y; + if (x < minX) { + minX = x; + } + if (y < minY) { + minY = y; + } + if (x > maxX) { + maxX = x; + } + if (y > maxY) { + maxY = y; + } + } + + return new Rectangle2D.Double(minX, minY, maxX - minX, maxY - minY); + } } diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/xfl/XFLConverter.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/xfl/XFLConverter.java index 025c9844d..619350d76 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/xfl/XFLConverter.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/xfl/XFLConverter.java @@ -252,7 +252,7 @@ public class XFLConverter { */ private final boolean DEBUG_EXPORT_LAYER_DEPTHS = false; - private static final DecimalFormat EDGE_DECIMAL_FORMAT = new DecimalFormat("0.##", DecimalFormatSymbols.getInstance(Locale.ENGLISH)); + private static final DecimalFormat EDGE_DECIMAL_FORMAT = new DecimalFormat("0.#", DecimalFormatSymbols.getInstance(Locale.ENGLISH)); private static final double SMALL_DIVISOR = 20; @@ -311,9 +311,9 @@ public class XFLConverter { if (value % 1 == 0) { return "" + (int) value; } - //value = Math.round(value * 1000.0) / 1000.0; - //return EDGE_DECIMAL_FORMAT.format(value); - return numEdgeToString(value); + //value = Math.round(value * 2.0) / 2.0; + return EDGE_DECIMAL_FORMAT.format(value); + //return numEdgeToString(value); } private static String convertShapeEdge(MATRIX mat, ShapeRecordAdvanced record, double x, double y) { diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/xfl/shapefixer/PathOrientation.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/xfl/shapefixer/PathArea.java similarity index 97% rename from libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/xfl/shapefixer/PathOrientation.java rename to libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/xfl/shapefixer/PathArea.java index 3df4df0ce..2e3bbed5a 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/xfl/shapefixer/PathOrientation.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/xfl/shapefixer/PathArea.java @@ -9,10 +9,11 @@ import java.util.ArrayList; import java.util.List; /** - * Path orientation detector. + * Path area calculator and orientation detector. + * * @author JPEXS */ -public final class PathOrientation { +public final class PathArea { public enum Orientation { CLOCKWISE, COUNTER_CLOCKWISE, DEGENERATE, OPEN_CONTOUR @@ -33,7 +34,7 @@ public final class PathOrientation { double[] coords = new double[6]; List current = new ArrayList<>(); - + double startX = 0; double startY = 0; double lastX = 0; @@ -94,7 +95,6 @@ public final class PathOrientation { hasOpen = true; } - } /** @@ -121,9 +121,9 @@ public final class PathOrientation { areaRef.setVal(0.0); return; } - + resultRef.setVal(area2 > 0 ? Orientation.CLOCKWISE : Orientation.COUNTER_CLOCKWISE); - areaRef.setVal(Math.abs(area2/2)); + areaRef.setVal(Math.abs(area2 / 2)); } public static void orientationSingleClosed(Shape shape, Reference orientationRef, Reference areaRef) { diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/xfl/shapefixer/ShapeFixer.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/xfl/shapefixer/ShapeFixer.java index cd1354c74..4ffb2368a 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/xfl/shapefixer/ShapeFixer.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/xfl/shapefixer/ShapeFixer.java @@ -27,9 +27,21 @@ import com.jpexs.decompiler.flash.types.shaperecords.StraightEdgeRecord; import com.jpexs.decompiler.flash.types.shaperecords.StyleChangeRecord; import com.jpexs.helpers.Reference; import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; +import java.util.Map; import java.util.Objects; +import java.util.Set; +import java.util.TreeMap; +import java.util.TreeSet; +import java.util.stream.Collectors; /** * Shape fixer. This will walk a shape and split crossed edges so FLA editor can @@ -97,6 +109,277 @@ public class ShapeFixer { ) { } + private static class BezierEdgeWrapper { + + BezierEdge be; + int layer; + int shapeIndex; + int edgeIndex; + + public BezierEdgeWrapper(BezierEdge be, int layer, int shapeIndex, int edgeIndex) { + this.be = be; + this.layer = layer; + this.shapeIndex = shapeIndex; + this.edgeIndex = edgeIndex; + } + + double minX() { + return bbox().getMinX(); + } + + double maxX() { + return bbox().getMaxX(); + } + + double minY() { + return bbox().getMinY(); + } + + double maxY() { + return bbox().getMaxY(); + } + + Rectangle2D bbox() { + return be.bbox(); + } + } + + static final class Event implements Comparable { + + final Type type; + final double x; + final BezierEdgeWrapper e; + + enum Type { + START, END + } + + public Event(Type type, double x, BezierEdgeWrapper e) { + this.type = type; + this.x = x; + this.e = e; + } + + @Override + public int compareTo(Event o) { + int cx = Double.compare(this.x, o.x); + if (cx != 0) { + return cx; + } + int ct = this.type.ordinal() - o.type.ordinal(); + if (ct != 0) { + return ct; + } + int ce = System.identityHashCode(this.e) - System.identityHashCode(o.e); + return ce; + } + } + + static final class Sweep { + + Map> splitPoints = new LinkedHashMap<>(); + final java.util.Comparator statusCmp = (e1, e2) -> { + int cMinY = Double.compare(e1.minY(), e2.minY()); + if (cMinY != 0) { + return cMinY; + } + return Integer.compare(System.identityHashCode(e1), System.identityHashCode(e2)); + }; + final java.util.TreeSet status = new TreeSet<>(statusCmp); + //final java.util.Set status = new HashSet<>(); + final java.util.PriorityQueue pq = new java.util.PriorityQueue<>(); + + // eps values for numeric robustness + static final double EPS = 1e-9; + + void addEdge(BezierEdgeWrapper e) { + pq.add(new Event(Event.Type.START, e.minX(), e)); + pq.add(new Event(Event.Type.END, e.maxX(), e)); + } + + void run() { + //int total = pq.size(); + //int cnt = 0; + while (!pq.isEmpty()) { + /*if (cnt % 1000 == 0) { + System.err.println("Percent done: " + (Math.round((cnt * 100.0 / total) * 100.0) / 100.0)); + } + cnt++;*/ + Event ev = pq.poll(); + + switch (ev.type) { + case START: + BezierEdgeWrapper beMaxY = new BezierEdgeWrapper(null, 0, 0, 0) { + @Override + double minY() { + return ev.e.maxY(); + } + }; + + for (BezierEdgeWrapper e2 : status.headSet(beMaxY, true)) { + /*if (e2.minY() > maxY) { + break; + }*/ + checkPair(ev.e, e2); + } + status.add(ev.e); + break; + case END: + status.remove(ev.e); + break; + } + } + } + + private void checkPair(BezierEdgeWrapper e1, BezierEdgeWrapper e2) { + if (e1 == null || e2 == null) { + return; + } + + List t1Ref = new ArrayList<>(); + List t2Ref = new ArrayList<>(); + List intPoint = new ArrayList<>(); + if (!e1.be.intersects(e2.be, t1Ref, t2Ref, intPoint)) { + return; + } + + if (!splitPoints.containsKey(e1)) { + splitPoints.put(e1, new ArrayList<>()); + } + if (!splitPoints.containsKey(e2)) { + splitPoints.put(e2, new ArrayList<>()); + } + splitPoints.get(e1).addAll(t1Ref); + splitPoints.get(e2).addAll(t2Ref); + } + } + + private void handleBewList(List bewList, List> shapes) { + Map> bewMap = bewList.stream() + .collect(Collectors.groupingBy(b -> b.layer)); + + for (Map.Entry> entry : bewMap.entrySet()) { + + Set bewsToIgnore = new LinkedHashSet<>(); + + Map existingEdges = new HashMap<>(); + + //eliminate duplicates + for (BezierEdgeWrapper bew1 : entry.getValue()) { + BezierEdge be = bew1.be; + BezierEdge rev = bew1.be.reverse(); + + BezierEdgeWrapper prevBew = existingEdges.get(be); + if (prevBew != null) { + bewsToIgnore.add(prevBew); + } + existingEdges.put(be, bew1); + + BezierEdgeWrapper prevRevBew = existingEdges.get(rev); + if (prevRevBew != null) { + bewsToIgnore.add(prevRevBew); + } + existingEdges.put(rev, bew1); + } + + //eliminate duplicates + /*for (BezierEdgeWrapper bew1 : entry.getValue()) { + for (BezierEdgeWrapper bew2 : entry.getValue()) { + if (bew1 != bew2) { + if (bew1.beOriginal.equals(bew2.beOriginal) + || bew1.beOriginal.equalsReverse(bew2.beOriginal)) { + bewsToIgnore.add(bew1); + } + } + } + }*/ + boolean useSweep = true; + + Map> splitPointsMap = new LinkedHashMap<>(); + + if (useSweep) { + Sweep sweep = new Sweep(); + for (BezierEdgeWrapper bew : entry.getValue()) { + if (bewsToIgnore.contains(bew)) { + continue; + } + sweep.addEdge(bew); + } + sweep.run(); + splitPointsMap = sweep.splitPoints; + } else { + + for (BezierEdgeWrapper bew1 : entry.getValue()) { + for (BezierEdgeWrapper bew2 : entry.getValue()) { + if (bew1 != bew2) { + List t1Ref = new ArrayList<>(); + List t2Ref = new ArrayList<>(); + List intPoints = new ArrayList<>(); + if (bew1.be.intersects(bew2.be, t1Ref, t2Ref, intPoints)) { + if (!splitPointsMap.containsKey(bew1)) { + splitPointsMap.put(bew1, new ArrayList<>()); + } + splitPointsMap.get(bew1).addAll(t1Ref); + + if (!splitPointsMap.containsKey(bew2)) { + splitPointsMap.put(bew2, new ArrayList<>()); + } + splitPointsMap.get(bew2).addAll(t2Ref); + } + } + } + } + } + + List splittedBewList = new ArrayList<>(splitPointsMap.keySet()); + + splittedBewList.sort((BezierEdgeWrapper o1, BezierEdgeWrapper o2) -> { + int dShapeIndex = o1.shapeIndex - o2.shapeIndex; + if (dShapeIndex != 0) { + return dShapeIndex; + } + int dEIndex = o1.edgeIndex - o2.edgeIndex; + if (dEIndex != 0) { + return dEIndex; + } + return System.identityHashCode(o1) - System.identityHashCode(o2); + }); + + for (int i = splittedBewList.size() - 1; i >= 0; i--) { + BezierEdgeWrapper bew = splittedBewList.get(i); + + List splitT = splitPointsMap.get(bew); + splitT.sort((a, b) -> Double.compare(a, b)); + + BezierEdge be = bew.be; + List realSplitT = new ArrayList<>(); + for (double t : splitT) { + if (t == 0.0 || t == 1.0) { + continue; + } + + realSplitT.add(t); + } + + if (realSplitT.isEmpty()) { + continue; + } + + List splitted = be.split(realSplitT); + shapes.get(bew.shapeIndex).remove(bew.edgeIndex); + int pos = 0; + for (BezierEdge bes : splitted) { + bes.roundX(); + if (bes.isEmpty()) { + continue; + } + shapes.get(bew.shapeIndex).add(bew.edgeIndex + pos, bes); + pos++; + } + } + } + } + private void detectOverlappingEdges( List> shapes, List fillStyles0, @@ -104,26 +387,79 @@ public class ShapeFixer { List lineStyles, List layers ) { + + /*if (true) { + detectOverlappingEdgesOld(shapes, fillStyles0, fillStyles1, lineStyles, layers); + return; + }*/ + + List allBewList = new ArrayList<>(); + for (int i1 = 0; i1 < shapes.size(); i1++) { + int layer = layers.get(i1); + for (int j1 = 0; j1 < shapes.get(i1).size(); j1++) { + BezierEdge be = shapes.get(i1).get(j1); + allBewList.add(new BezierEdgeWrapper(be, layer, i1, j1)); + } + } + + List strokesBewList = new ArrayList<>(); + List fillsBewList = new ArrayList<>(); + + for (BezierEdgeWrapper bew : allBewList) { + if (fillStyles0.get(bew.shapeIndex) == 0 && fillStyles1.get(bew.shapeIndex) == 0) { + strokesBewList.add(bew); + } else { + fillsBewList.add(bew); + } + } + + handleBewList(strokesBewList, shapes); + handleBewList(fillsBewList, shapes); + + for (int i1 = 0; i1 < shapes.size(); i1++) { + for (int j1 = 0; j1 < shapes.get(i1).size(); j1++) { + BezierEdge be1 = shapes.get(i1).get(j1); + be1.shrinkToLine(); + } + } + } + + //Old unoptimized overlapping + private void detectOverlappingEdgesOld( + List> shapes, + List fillStyles0, + List fillStyles1, + List lineStyles, + List layers + ) { + /*Map splitPoints = new TreeMap<>(new Comparator() { + @Override + public int compare(BezierEdge o1, BezierEdge o2) { + int dSize = o1.points.size() - o2.points.size(); + if (dSize != 0) { + return dSize; + } + for (int i = 0; i < o1.points.size(); i++) { + int dX = Double.compare(o1.points.get(i).getX(), o2.points.get(i).getX()); + if (dX != 0) { + return dX; + } + int dY = Double.compare(o1.points.get(i).getY(), o2.points.get(i).getY()); + if (dY != 0) { + return dY; + } + } + return 0; + } + + });*/ + //------------------- detecting overlapping edges -------------------- List splittedPairs = new ArrayList<>(); - //long t1 = System.currentTimeMillis(); - //int oldpct = 0; loopi1: for (int i1 = 0; i1 < shapes.size(); i1++) { int layer = layers.get(i1); - /*if (i1 % 10 == 0) { - int pct = i1 * 100 / shapes.size(); - if (oldpct != pct) { - //System.err.println("pct: " + pct + "%"); - if (pct == 10) { - long t2 = System.currentTimeMillis(); - long dt = t2 - t1; - System.err.println("Time per 10%: " + Helper.formatTimeSec(dt)); - } - } - oldpct = pct; - }*/ loopj1: for (int j1 = 0; j1 < shapes.get(i1).size(); j1++) { @@ -229,16 +565,6 @@ public class ShapeFixer { } } - /*if (t1Ref.size() == 2) { - if (intPoints.get(0).distance(intPoints.get(1)) < 1/256f) { - t1Ref.remove(1); - t2Ref.remove(1); - intPoints.remove(1); - } - } - if (t1Ref.size() == 3) { - - }*/ if (t1Ref.size() == 1) { if ((t1Ref.get(0) == 0 || t1Ref.get(0) == 1) && (t2Ref.get(0) == 0 || t2Ref.get(0) == 1)) { @@ -300,15 +626,13 @@ public class ShapeFixer { be2L.setEndPoint(intP); be2R.setBeginPoint(intP); - /*be1L.roundX(); - be1R.roundX(); - be2L.roundX(); - be2R.roundX();*/ be1L.roundX(); be1R.roundX(); be2L.roundX(); be2R.roundX(); + //splitPoints.put(be1, t1Ref.get(splitPointIndex)); + //splitPoints.put(be2, t2Ref.get(splitPointIndex)); if (i1 == i2) { if (j1 < j2) { shapes.get(i1).remove(j2); @@ -565,4 +889,37 @@ public class ShapeFixer { } return null; } + + public static void main(String[] args) { + + Map splitPoints = new TreeMap<>(new Comparator() { + @Override + public int compare(BezierEdge o1, BezierEdge o2) { + int dSize = o1.points.size() - o2.points.size(); + if (dSize != 0) { + return dSize; + } + for (int i = 0; i < o1.points.size(); i++) { + int dX = Double.compare(o1.points.get(i).getX(), o2.points.get(i).getX()); + if (dX != 0) { + return dX; + } + int dY = Double.compare(o1.points.get(i).getY(), o2.points.get(i).getY()); + if (dY != 0) { + return dY; + } + } + return 0; + } + + }); + + ShapeFixer f = new ShapeFixer(); + List> shapes = new ArrayList<>(); + List beList = new ArrayList<>(); + beList.add(new BezierEdge(10000.0, 4980.0, 10040.0, 5060.0)); + beList.add(new BezierEdge(10040.0, 5040.0, 9900.0, 4900.0, 9900.0, 4860.0)); + shapes.add(beList); + f.detectOverlappingEdges(shapes, Arrays.asList(1), Arrays.asList(2), Arrays.asList(0), Arrays.asList(0)); + } } diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/xfl/shapefixer/SwitchedFillSidesFixer.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/xfl/shapefixer/SwitchedFillSidesFixer.java index 8beb825d7..3cff5f514 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/xfl/shapefixer/SwitchedFillSidesFixer.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/xfl/shapefixer/SwitchedFillSidesFixer.java @@ -97,16 +97,11 @@ public class SwitchedFillSidesFixer { public Polygon(List list, int fillStyle) { this.list = list; - /*double polyArea = polygonArea(list); - if (polyArea < 0) { - ccw = true; - }*/ path = toPath(); - //this.ccw = PathOrientation.orientationSingleClosed(path) == PathOrientation.Orientation.COUNTER_CLOCKWISE; - Reference orientationRef = new Reference<>(null); + Reference orientationRef = new Reference<>(null); Reference areaRef = new Reference<>(0.0); - PathOrientation.orientationSingleClosed(path, orientationRef, areaRef); - this.ccw = orientationRef.getVal() == PathOrientation.Orientation.COUNTER_CLOCKWISE; + PathArea.orientationSingleClosed(path, orientationRef, areaRef); + this.ccw = orientationRef.getVal() == PathArea.Orientation.COUNTER_CLOCKWISE; this.area = areaRef.getVal(); this.areaObj = new Area(path); this.bbox = this.areaObj.getBounds2D(); @@ -168,12 +163,14 @@ public class SwitchedFillSidesFixer { // Simple uniform grid over bbox domain private final double cellSize; private final Map> cells = new HashMap<>(); - private final double minX, minY; + private final double minX; + private final double minY; GridIndex(Collection polys, double cellSize) { this.cellSize = cellSize; // Compute global origin (minX/minY) to keep keys small - double minx = Double.POSITIVE_INFINITY, miny = Double.POSITIVE_INFINITY; + double minx = Double.POSITIVE_INFINITY; + double miny = Double.POSITIVE_INFINITY; for (Polygon w : polys) { Rectangle2D b = w.bbox; if (b.getMinX() < minx) { @@ -237,7 +234,7 @@ public class SwitchedFillSidesFixer { void accept(int gx, int gy); } - public static void buildContainment(List polygons) { + private static void buildContainment(List polygons) { Map> byStyle = polygons.stream() .collect(java.util.stream.Collectors.groupingBy(w -> w.fillStyle)); @@ -337,35 +334,6 @@ public class SwitchedFillSidesFixer { polygons.add(new Polygon(list, listFills.get(i))); } - /*for (Polygon outer : polygons) { - for (Polygon inner : polygons) { - if (outer != inner && inner.fillStyle == outer.fillStyle) { - boolean cont = outer.contains(inner); - - if (cont) { - if (inner.children.contains(outer)) { - inner.children.remove(outer); - } - outer.children.add(inner); - } - } - } - } - - loopmod: - while (true) { - for (Polygon poly : polygons) { - for (int c = 0; c < poly.children.size(); c++) { - for (int c2 = 0; c2 < poly.children.size(); c2++) { - if (poly.children.get(c).children.contains(poly.children.get(c2))) { - poly.children.remove(c2); - continue loopmod; - } - } - } - } - break; - }*/ buildContainment(polygons); for (Polygon poly : polygons) { @@ -403,11 +371,10 @@ public class SwitchedFillSidesFixer { BezierEdge beRev = be.reverse(); int localFs = globalToLocalFillStyleMap.get(fillStyleIdx); - BezierEdge search = new BezierEdge(180.0, -3040.0, 480.0, -3400.0); - + //BezierEdge search = new BezierEdge(180.0, -3040.0, 480.0, -3400.0); boolean print = false; - if (be.equals(search)) { + /*if (be.equals(search)) { System.err.println("xxx"); System.err.println("" + polygon); print = true; @@ -415,8 +382,7 @@ public class SwitchedFillSidesFixer { if (be.equals(search.reverse())) { System.err.println("yyy"); print = true; - } - + }*/ if (print) { System.err.println("localFS: " + localFs); System.err.println("filled: " + polygon.filled);