From 313590ebfd51a628034ff85271b9dc3a6e65d59a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jindra=20Pet=C5=99=C3=ADk?= Date: Sat, 4 Oct 2025 14:40:11 +0200 Subject: [PATCH] Shape fixer 2 WIP --- .../decompiler/flash/math/BezierEdge.java | 95 +- .../decompiler/flash/math/Intersections.java | 6 + .../flash/math/OverlapInterval.java | 22 + .../decompiler/flash/math/QuadOverlap.java | 216 ++++ .../decompiler/flash/xfl/XFLConverter.java | 57 +- .../flash/xfl/shapefixer/Layer.java | 26 + .../shapefixer/OverlappingEdgesSplitter.java | 452 ++++++++ .../decompiler/flash/xfl/shapefixer/Path.java | 203 ++++ .../flash/xfl/shapefixer/ShapeFixer.java | 2 +- .../flash/xfl/shapefixer/ShapeFixer2.java | 967 ++++++++++++++++++ .../shapefixer/SwitchedFillSidesFixer.java | 189 +++- 11 files changed, 2184 insertions(+), 51 deletions(-) create mode 100644 libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/math/OverlapInterval.java create mode 100644 libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/math/QuadOverlap.java create mode 100644 libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/xfl/shapefixer/Layer.java create mode 100644 libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/xfl/shapefixer/OverlappingEdgesSplitter.java create mode 100644 libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/xfl/shapefixer/Path.java create mode 100644 libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/xfl/shapefixer/ShapeFixer2.java 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 d8332d896..9eea5521d 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 @@ -23,6 +23,7 @@ import java.io.Serializable; import java.text.DecimalFormat; import java.text.DecimalFormatSymbols; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Locale; import java.util.Objects; @@ -157,6 +158,15 @@ public class BezierEdge implements Serializable { points.set(points.size() - 1, p); calcParams(); } + + public void setControlPoint(Point2D p) { + if(points.size() == 2) { + points.add(1, p); + } else { + points.set(1, p); + } + calcParams(); + } /** * Gets the point at specified position. @@ -222,6 +232,10 @@ public class BezierEdge implements Serializable { break; } } + + if (points.size() == 3 && Double.isNaN(points.get(1).getX())) { + System.err.println("xxx"); + } } /** @@ -279,9 +293,9 @@ public class BezierEdge implements Serializable { * @return List of intersections */ public List getIntersections(BezierEdge b2) { - if (!Intersections.rectIntersection(bbox, b2.bbox)) { + /*if (!Intersections.rectIntersection(bbox, b2.bbox)) { return new ArrayList<>(); - } + }*/ if (points.size() == 2) { if (b2.points.size() == 2) { return Intersections.intersectLineLine(points.get(0), points.get(1), b2.points.get(0), b2.points.get(1), true); @@ -578,7 +592,7 @@ public class BezierEdge implements Serializable { )); } calcParams(); - } + } public static final double ROUND_VALUE = 2; @@ -591,7 +605,7 @@ public class BezierEdge implements Serializable { } calcParams(); } - + public void roundN(double n) { for (int i = 0; i < this.points.size(); i++) { this.points.set(i, new Point2D.Double( @@ -832,4 +846,77 @@ public class BezierEdge implements Serializable { return new Rectangle2D.Double(minX, minY, maxX - minX, maxY - minY); } + + public boolean isQuad() { + return points.size() == 3; + } + + public Point2D midPoint() { + if (!isQuad()) { + return new Point2D.Double((points.get(0).getX() + points.get(1).getX()) * 0.5, (points.get(0).getY() + points.get(1).getY()) * 0.5); + } else { + double t = 0.5; + double mt = 1 - t; + double mx = mt * mt * points.get(0).getX() + 2 * mt * t * points.get(1).getX() + t * t * points.get(2).getX(); + double my = mt * mt * points.get(0).getY() + 2 * mt * t * points.get(1).getY() + t * t * points.get(2).getY(); + return new Point2D.Double(mx, my); + } + } + + public Point2D unitNormal() { + double dx; + double dy; + if (!isQuad()) { + dx = points.get(1).getX() - points.get(0).getX(); + dy = points.get(1).getY() - points.get(0).getY(); + } else { + // derivative at t=0.5 + double t = 0.5; + double mt = 1 - t; + dx = 2 * mt * (points.get(1).getX() - points.get(0).getX()) + 2 * t * (points.get(2).getX() - points.get(1).getX()); + dy = 2 * mt * (points.get(1).getY() - points.get(0).getY()) + 2 * t * (points.get(2).getY() - points.get(1).getY()); + } + // left-hand normal = (-dy, dx) + double nx = -dy, ny = dx; + double len = Math.hypot(nx, ny); + if (len == 0) { + return new Point2D.Double(0, 0); + } + return new Point2D.Double(nx / len, ny / len); + } + + public BezierEdge toQuad() { + if (!isQuad()) { + return new BezierEdge(Arrays.asList(getBeginPoint(), midPoint() ,getEndPoint())); + } + return this; + } + + public Point2D getControlPoint() { + if (isQuad()) { + return points.get(1); + } + return midPoint(); + } + + public List overlap(BezierEdge other) { + BezierEdge q1 = this.toQuad(); + BezierEdge q2 = other.toQuad(); + return QuadOverlap.findQuadraticOverlaps( + (Point2D.Double) q1.getBeginPoint(), (Point2D.Double) q1.getControlPoint(), (Point2D.Double) q1.getEndPoint(), + (Point2D.Double) q2.getBeginPoint(), (Point2D.Double) q2.getControlPoint(), (Point2D.Double) q2.getEndPoint(), 10 + ); + } + + public static void main(String[] args) { + BezierEdge be1 = new BezierEdge(1000,0,1000,1000); + BezierEdge be2 = new BezierEdge(1000.25, -500, 1000.25, 2000); + + List intervals = be1.overlap(be2); + for (OverlapInterval i : intervals) { + System.err.println("" + be1.pointAt(i.t0)+" to " + be1.pointAt(i.t1)+" on BE1 AND " + + be2.pointAt(i.u0)+" to " + be2.pointAt(i.u1)+" on BE2" + ); + } + } } diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/math/Intersections.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/math/Intersections.java index f0b3ac91f..3277c049b 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/math/Intersections.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/math/Intersections.java @@ -500,6 +500,12 @@ public class Intersections { //No Intersection } } else if (ua_t == 0 || ub_t == 0) { + + if (true) { + //no overlapping + return result; + } + if (!addCoincident) { return result; } diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/math/OverlapInterval.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/math/OverlapInterval.java new file mode 100644 index 000000000..5a047c346 --- /dev/null +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/math/OverlapInterval.java @@ -0,0 +1,22 @@ +package com.jpexs.decompiler.flash.math; + +import java.util.Locale; + +/** + * + * @author JPEXS + */ +public class OverlapInterval { + public final double t0, t1; // on curve A + public final double u0, u1; // on curve B + public OverlapInterval(double t0, double t1, double u0, double u1) { + // normalize so t0<=t1 and u0<=u1 + this.t0 = Math.min(t0, t1); + this.t1 = Math.max(t0, t1); + this.u0 = Math.min(u0, u1); + this.u1 = Math.max(u0, u1); + } + @Override public String toString() { + return String.format(Locale.ENGLISH, "A:[%.6f, %.6f], B:[%.6f, %.6f]", t0, t1, u0, u1); + } +} \ No newline at end of file diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/math/QuadOverlap.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/math/QuadOverlap.java new file mode 100644 index 000000000..aec6e689f --- /dev/null +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/math/QuadOverlap.java @@ -0,0 +1,216 @@ +package com.jpexs.decompiler.flash.math; + +/** + * + * @author JPEXS + */ +import java.awt.geom.Point2D; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public final class QuadOverlap { + + /** Polyline with parameter values for each vertex. */ + private static class ParamPoly { + final List pts = new ArrayList<>(); + final List params = new ArrayList<>(); + } + + /** Public entry: find overlapping sub-curve intervals within tolerance. */ + public static List findQuadraticOverlaps( + Point2D.Double A0, Point2D.Double A1, Point2D.Double A2, + Point2D.Double B0, Point2D.Double B1, Point2D.Double B2, + double tol) { + + // Tolerances: tune as needed + final double flatTol = tol * 0.5; // Hausdorff bound for flattening + final double distTol = tol; // max allowed normal offset between near-collinear segments + final double angleTol = 0.01; // radians; ~0.057° — treat as parallel if below + + ParamPoly polyA = flattenQuad(A0, A1, A2, flatTol); + ParamPoly polyB = flattenQuad(B0, B1, B2, flatTol); + + List raw = overlapPolylines(polyA, polyB, distTol, angleTol); + return mergeIntervals(raw, 1e-6, 1e-6); + } + + // ---------- Step 1: Adaptive flattening with param tracking ---------- + + /** Flatten quadratic Bézier via recursive de Casteljau, recording t at each vertex. */ + private static ParamPoly flattenQuad(Point2D.Double P0, Point2D.Double P1, Point2D.Double P2, double flatTol) { + ParamPoly out = new ParamPoly(); + // Start with first vertex + out.pts.add(new Point2D.Double(P0.x, P0.y)); + out.params.add(0.0); + // Recursive subdivision stack + subdivideQuad(P0, P1, P2, 0.0, 1.0, flatTol, out); + // Ensure last vertex + if (out.params.get(out.params.size() - 1) < 1.0 - 1e-15) { + out.pts.add(new Point2D.Double(P2.x, P2.y)); + out.params.add(1.0); + } + return out; + } + + /** Subdivide until control polygon is flat enough. */ + private static void subdivideQuad(Point2D.Double P0, Point2D.Double P1, Point2D.Double P2, + double t0, double t1, double flatTol, ParamPoly out) { + if (quadFlatEnough(P0, P1, P2, flatTol)) { + // Append end point and parameter + out.pts.add(new Point2D.Double(P2.x, P2.y)); + out.params.add(t1); + return; + } + // de Casteljau split at t=0.5 + SplitRes s = splitQuadHalf(P0, P1, P2); + double tm = (t0 + t1) * 0.5; + subdivideQuad(P0, s.P01, s.P012, t0, tm, flatTol, out); + subdivideQuad(s.P012, s.P12, P2, tm, t1, flatTol, out); + } + + /** Flatness test: distance of control point to baseline. */ + private static boolean quadFlatEnough(Point2D.Double P0, Point2D.Double P1, Point2D.Double P2, double tol) { + double d = distPointToSegment(P1, P0, P2); + return d <= tol; + } + + private static class SplitRes { + Point2D.Double P01, P12, P012; + } + + /** Split quadratic at 0.5 via de Casteljau. */ + private static SplitRes splitQuadHalf(Point2D.Double P0, Point2D.Double P1, Point2D.Double P2) { + Point2D.Double P01 = lerp(P0, P1, 0.5); + Point2D.Double P12 = lerp(P1, P2, 0.5); + Point2D.Double P012 = lerp(P01, P12, 0.5); + SplitRes r = new SplitRes(); + r.P01 = P01; r.P12 = P12; r.P012 = P012; + return r; + } + + private static Point2D.Double lerp(Point2D.Double a, Point2D.Double b, double t) { + return new Point2D.Double(a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t); + } + + // ---------- Step 2: Segment-segment near-coincidence with param mapping ---------- + + private static List overlapPolylines(ParamPoly A, ParamPoly B, double distTol, double angleTol) { + List out = new ArrayList<>(); + for (int i = 0; i + 1 < A.pts.size(); i++) { + Point2D.Double a0 = A.pts.get(i), a1 = A.pts.get(i + 1); + double ta0 = A.params.get(i), ta1 = A.params.get(i + 1); + + // Precompute direction and length for A + double ax = a1.x - a0.x, ay = a1.y - a0.y; + double alen = Math.hypot(ax, ay); + if (alen == 0) continue; + double ux = ax / alen, uy = ay / alen; // unit dir + + for (int j = 0; j + 1 < B.pts.size(); j++) { + Point2D.Double b0 = B.pts.get(j), b1 = B.pts.get(j + 1); + double ub0 = B.params.get(j), ub1 = B.params.get(j + 1); + + double bx = b1.x - b0.x, by = b1.y - b0.y; + double blen = Math.hypot(bx, by); + if (blen == 0) continue; + double vx = bx / blen, vy = by / blen; + + // Angle test: |sin(theta)| = |ux*vy - uy*vx| + double sinTh = Math.abs(ux * vy - uy * vx); + if (sinTh > angleTol) continue; + + // Normal offset (distance between supporting lines) test + // Compute signed distance of b0 to line A + double nx = -uy, ny = ux; // left normal of A + double off = ((b0.x - a0.x) * nx + (b0.y - a0.y) * ny); + if (Math.abs(off) > distTol) continue; + + // Project endpoints onto A direction to get 1D intervals + double a0s = 0.0, a1s = alen; + double b0s = (b0.x - a0.x) * ux + (b0.y - a0.y) * uy; + double b1s = (b1.x - a0.x) * ux + (b1.y - a0.y) * uy; + + // Normalize B's interval direction (ensure b0s <= b1s) + double bbMin = Math.min(b0s, b1s); + double bbMax = Math.max(b0s, b1s); + + double s0 = Math.max(a0s, bbMin); + double s1 = Math.min(a1s, bbMax); + if (s1 <= s0) continue; // no overlap along the axis + + // Convert 1D s back to points on A-line, then to params on A/B segments + // Fraction along A segment: + double fa0 = clamp01((s0 - a0s) / (a1s - a0s)); + double fa1 = clamp01((s1 - a0s) / (a1s - a0s)); + + // For B: we need to know which endpoint was smaller + boolean bIncreasing = b0s <= b1s; + double fb0 = (s0 - (bIncreasing ? b0s : b1s)) / (Math.abs(b1s - b0s)); + double fb1 = (s1 - (bIncreasing ? b0s : b1s)) / (Math.abs(b1s - b0s)); + fb0 = clamp01(fb0); fb1 = clamp01(fb1); + + // Map fractions to global t/u via linear interpolation of per-vertex params + double tStart = lerp(ta0, ta1, fa0); + double tEnd = lerp(ta0, ta1, fa1); + double uStart = lerp(ub0, ub1, bIncreasing ? fb0 : (1.0 - fb0)); + double uEnd = lerp(ub0, ub1, bIncreasing ? fb1 : (1.0 - fb1)); + + out.add(new OverlapInterval(tStart, tEnd, uStart, uEnd)); + } + } + return out; + } + + private static double clamp01(double x) { + if (x < 0) return 0; + if (x > 1) return 1; + return x; + } + + private static double lerp(double a, double b, double t) { + return a + (b - a) * t; + } + + // ---------- Step 3: Merge touching intervals ---------- + + /** Merge intervals that touch within tolerances. */ + private static List mergeIntervals(List in, double tTol, double uTol) { + if (in.isEmpty()) return in; + // Sort by t0 then u0 + Collections.sort(in, (p, q) -> { + int c = Double.compare(p.t0, q.t0); + if (c != 0) return c; + return Double.compare(p.u0, q.u0); + }); + List out = new ArrayList<>(); + OverlapInterval cur = in.get(0); + for (int i = 1; i < in.size(); i++) { + OverlapInterval nx = in.get(i); + if (Math.abs(nx.t0 - cur.t1) <= tTol && Math.abs(nx.u0 - cur.u1) <= uTol) { + // Extend + cur = new OverlapInterval(cur.t0, Math.max(cur.t1, nx.t1), cur.u0, Math.max(cur.u1, nx.u1)); + } else { + out.add(cur); + cur = nx; + } + } + out.add(cur); + return out; + } + + // ---------- Geometry helpers ---------- + + /** Distance of point C to segment AB. */ + private static double distPointToSegment(Point2D.Double C, Point2D.Double A, Point2D.Double B) { + double vx = B.x - A.x, vy = B.y - A.y; + double wx = C.x - A.x, wy = C.y - A.y; + double vv = vx * vx + vy * vy; + if (vv == 0) return Math.hypot(wx, wy); + double t = (wx * vx + wy * vy) / vv; + if (t <= 0) return Math.hypot(wx, wy); + if (t >= 1) return Math.hypot(C.x - B.x, C.y - B.y); + double px = A.x + t * vx, py = A.y + t * vy; + return Math.hypot(C.x - px, C.y - py); + } +} 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 b72f787dc..0e7fb27c1 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 @@ -154,6 +154,7 @@ import com.jpexs.decompiler.flash.types.sound.SoundFormat; import com.jpexs.decompiler.flash.xfl.shapefixer.CurvedEdgeRecordAdvanced; import com.jpexs.decompiler.flash.xfl.shapefixer.MorphShapeFixer; import com.jpexs.decompiler.flash.xfl.shapefixer.ShapeFixer; +import com.jpexs.decompiler.flash.xfl.shapefixer.ShapeFixer2; import com.jpexs.decompiler.flash.xfl.shapefixer.ShapeRecordAdvanced; import com.jpexs.decompiler.flash.xfl.shapefixer.StraightEdgeRecordAdvanced; import com.jpexs.decompiler.flash.xfl.shapefixer.StyleChangeRecordAdvanced; @@ -409,10 +410,10 @@ public class XFLConverter { y = rec.changeY(y); } //hack for morphshapes. TODO: make this better - if (close && (Double.compare(lastMoveToX, x) != 0 || Double.compare(lastMoveToY, y) != 0)) { + /*if (close && (Double.compare(lastMoveToX, x) != 0 || Double.compare(lastMoveToY, y) != 0)) { StraightEdgeRecordAdvanced ser = new StraightEdgeRecordAdvanced(lastMoveToX - x, lastMoveToY - y); ret.append(convertShapeEdge(mat, ser, x, y)); - } + */ } private static String getScaleMode(ILINESTYLE lineStyle) { @@ -702,8 +703,8 @@ public class XFLConverter { List shapeRecordsAdvanced; - ShapeFixer fixer = morphshape ? new MorphShapeFixer() : new ShapeFixer(); - Logger.getLogger(ShapeFixer.class.getName()).log(Level.FINE, "Fixing character {0}...", characterId); + ShapeFixer2 fixer = new ShapeFixer2(); //morphshape ? new MorphShapeFixer() : new ShapeFixer(); + Logger.getLogger(ShapeFixer2.class.getName()).log(Level.FINE, "Fixing character {0}...", characterId); if (small) { shapeRecords = Helper.deepCopy(shapeRecords); @@ -779,6 +780,27 @@ public class XFLConverter { int lastFillStyle1 = fillStyle1; int lastFillStyle0 = fillStyle0; int lastStrokeStyle = strokeStyle; + + if (scr.stateFillStyle0) { + int fillStyle0_new = scr.fillStyle0; + /*if (morphshape) { //??? + fillStyle1 = fillStyle0_new; + } else {*/ + fillStyle0 = fillStyle0_new; + //} + } + if (scr.stateFillStyle1) { + int fillStyle1_new = scr.fillStyle1; + /*if (morphshape) { + fillStyle0 = fillStyle1_new; + } else {*/ + fillStyle1 = fillStyle1_new; + //} + } + if (scr.stateLineStyle) { + strokeStyle = scr.lineStyle; + } + if (scr.stateNewStyles) { XFLXmlWriter fillsNewStr = new XFLXmlWriter(); XFLXmlWriter strokesNewStr = new XFLXmlWriter(); @@ -844,26 +866,7 @@ public class XFLConverter { currentLayer.writeCharactersRaw(fillsNewStr.toString()); currentLayer.writeCharactersRaw(strokesNewStr.toString()); currentLayer.writeStartElement("edges"); - } - if (scr.stateFillStyle0) { - int fillStyle0_new = scr.fillStyle0; - if (morphshape) { //??? - fillStyle1 = fillStyle0_new; - } else { - fillStyle0 = fillStyle0_new; - } - } - if (scr.stateFillStyle1) { - int fillStyle1_new = scr.fillStyle1; - if (morphshape) { - fillStyle0 = fillStyle1_new; - } else { - fillStyle1 = fillStyle1_new; - } - } - if (scr.stateLineStyle) { - strokeStyle = scr.lineStyle; - } + } if (!edges.isEmpty()) { if ((fillStyle0 > 0) || (fillStyle1 > 0) || (strokeStyle > 0)) { currentLayer.writeStartElement("Edge"); @@ -888,7 +891,11 @@ public class XFLConverter { edges.clear(); } } - edges.add(edge); + if (edge instanceof StyleChangeRecordAdvanced && !((StyleChangeRecordAdvanced) edge).stateMoveTo) { + //ignore + } else { + edges.add(edge); + } x = edge.changeX(x); y = edge.changeY(y); } diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/xfl/shapefixer/Layer.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/xfl/shapefixer/Layer.java new file mode 100644 index 000000000..c41bb2f84 --- /dev/null +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/xfl/shapefixer/Layer.java @@ -0,0 +1,26 @@ +/* + * Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license + * Click nbfs://nbhost/SystemFileSystem/Templates/Classes/Class.java to edit this template + */ +package com.jpexs.decompiler.flash.xfl.shapefixer; + +import com.jpexs.decompiler.flash.types.FILLSTYLEARRAY; +import com.jpexs.decompiler.flash.types.LINESTYLEARRAY; +import java.util.ArrayList; +import java.util.List; + +/** + * + * @author JPEXS + */ +public class Layer { + FILLSTYLEARRAY fillStyleArray = null; + LINESTYLEARRAY lineStyleArray = null; + List paths = new ArrayList<>(); + + public void round(boolean wasSmall) { + for (Path p : paths) { + p.round(wasSmall); + } + } +} diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/xfl/shapefixer/OverlappingEdgesSplitter.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/xfl/shapefixer/OverlappingEdgesSplitter.java new file mode 100644 index 000000000..a5fd6af92 --- /dev/null +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/xfl/shapefixer/OverlappingEdgesSplitter.java @@ -0,0 +1,452 @@ +package com.jpexs.decompiler.flash.xfl.shapefixer; + +import com.jpexs.decompiler.flash.math.BezierEdge; +import com.jpexs.decompiler.flash.math.Intersections; +import com.jpexs.decompiler.flash.math.OverlapInterval; +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.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; +import java.util.stream.Collectors; + +/** + * + * @author JPEXS + */ +public class OverlappingEdgesSplitter { + private static class BezierEdgeWrapper { + + BezierEdge be; + int layer; + int pathIndex; + int edgeIndex; + + public BezierEdgeWrapper(BezierEdge be, int layer, int shapeIndex, int edgeIndex) { + this.be = be; + this.layer = layer; + this.pathIndex = 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 class BezierEdgePair { + BezierEdgeWrapper be1; + BezierEdgeWrapper be2; + + public BezierEdgePair(BezierEdgeWrapper be1, BezierEdgeWrapper be2) { + this.be1 = be1; + this.be2 = be2; + } + } + + static final class Sweep { + + Map> splitPoints = new LinkedHashMap<>(); + Map> splitPoints2D = new LinkedHashMap<>(); + Map> splitPointsControl = new LinkedHashMap<>(); + //Map> overlapIntervals = 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 (!Intersections.rectIntersection(e1.bbox(), e2.bbox())) { + return; + } + + boolean hasIntersections = e1.be.intersects(e2.be, t1Ref, t2Ref, intPoint); + List overlapIntervals = e1.be.overlap(e2.be); + + if (!hasIntersections && overlapIntervals.isEmpty()) { + return; + } + + + + if (!splitPoints.containsKey(e1)) { + splitPoints.put(e1, new ArrayList<>()); + } + if (!splitPoints.containsKey(e2)) { + splitPoints.put(e2, new ArrayList<>()); + } + + if (!splitPoints2D.containsKey(e1)) { + splitPoints2D.put(e1, new ArrayList<>()); + } + if (!splitPoints2D.containsKey(e2)) { + splitPoints2D.put(e2, new ArrayList<>()); + } + if (!splitPointsControl.containsKey(e1)) { + splitPointsControl.put(e1, new ArrayList<>()); + } + if (!splitPointsControl.containsKey(e2)) { + splitPointsControl.put(e2, new ArrayList<>()); + } + splitPoints.get(e1).addAll(t1Ref); + splitPoints.get(e2).addAll(t2Ref); + splitPoints2D.get(e1).addAll(intPoint); + splitPoints2D.get(e2).addAll(intPoint); + for (int i = 0; i < intPoint.size(); i++) { + splitPointsControl.get(e1).add(null); + splitPointsControl.get(e2).add(null); + } + + + if (!overlapIntervals.isEmpty()) { + for (OverlapInterval interval : overlapIntervals) { + if (interval.t0 == interval.t1) { + continue; + } + BezierEdge middle; + if (interval.t0 == 1.0) { + if (interval.t1 == 0.0) { + middle = e1.be; + } else { + middle = e1.be.split(Arrays.asList(interval.t1)).get(0); + } + } else { + List splitted = e1.be.split(Arrays.asList(interval.t0, interval.t1)); + middle = splitted.get(1); + } + + splitPoints.get(e1).add(interval.t0); + splitPoints.get(e1).add(interval.t1); + splitPoints.get(e2).add(interval.u0); + splitPoints.get(e2).add(interval.u1); + + + System.err.println("Overlapping " + e1.be.toSvg()+" AND " + e2.be.toSvg()+" by " + middle.toSvg()); + + splitPoints2D.get(e1).add(middle.getBeginPoint()); + splitPoints2D.get(e1).add(middle.getEndPoint()); + splitPointsControl.get(e1).add(middle.getControlPoint()); + splitPointsControl.get(e1).add(middle.getControlPoint()); + + splitPoints2D.get(e2).add(middle.getBeginPoint()); + splitPoints2D.get(e2).add(middle.getEndPoint()); + splitPointsControl.get(e2).add(middle.getControlPoint()); + splitPointsControl.get(e2).add(middle.getControlPoint()); + } + } + + } + } + + private static class TPoint { + double t; + Point2D point; + Point2D controlPoint; + + public TPoint(double t, Point2D point, Point2D controlPoint) { + this.t = t; + this.point = point; + this.controlPoint = controlPoint; + } + + + } + + private void handleBewList(List bewList, List layers) { + 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<>(); + Map> splitPoints2DMap = new LinkedHashMap<>(); + Map> splitPointsControlMap = 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; + splitPoints2DMap = sweep.splitPoints2D; + splitPointsControlMap = sweep.splitPointsControl; + } 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.pathIndex - o2.pathIndex; + 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); + List splitPoint = splitPoints2DMap.get(bew); + List splitControls = splitPointsControlMap.get(bew); + + List splits = new ArrayList<>(); + for (int j = 0; j < splitT.size(); j++) { + splits.add(new TPoint(splitT.get(j), splitPoint.get(j), splitControls.get(j))); + } + + splits.sort((a, b) -> Double.compare(a.t, b.t)); + + BezierEdge be = bew.be; + List realSplitT = new ArrayList<>(); + for (TPoint tp : splits) { + if (tp.t == 0.0 || tp.t == 1.0) { + continue; + } + + realSplitT.add(tp.t); + } + + if (realSplitT.isEmpty()) { + continue; + } + + List splitted = be.split(realSplitT); + if (splits.get(0).t != 0.0) { + splits.add(0, new TPoint(0.0, bew.be.getBeginPoint(), null)); + } + if (splits.get(splits.size() - 1).t != 1.0) { + splits.add(new TPoint(1.0, bew.be.getEndPoint(), null)); + } + + int p = 0; + for (int j = 0; j < splits.size(); j++) { + if (splits.get(j).t == 0.0 || splits.get(j).t == 1.0) { + continue; + } + splitted.get(p).setBeginPoint(splits.get(j - 1).point); + splitted.get(p).setEndPoint(splits.get(j).point); + if (splits.get(j - 1).controlPoint == splits.get(j).controlPoint && splits.get(j).controlPoint != null) { + splitted.get(p).setControlPoint(splits.get(j).controlPoint); + } + p++; + } + layers.get(bew.layer).paths.get(bew.pathIndex).edges.remove(bew.edgeIndex); + int pos = 0; + for (BezierEdge bes : splitted) { + layers.get(bew.layer).paths.get(bew.pathIndex).edges.add(bew.edgeIndex + pos, bes); + pos++; + } + } + } + } + + public void splitOverlappingEdges( + List layers + ) { + List strokesBewList = new ArrayList<>(); + List fillsBewList = new ArrayList<>(); + + for (int layer = 0; layer < layers.size(); layer++) { + for (int p = 0; p < layers.get(layer).paths.size(); p++) { + Path path = layers.get(layer).paths.get(p); + for (int e = 0; e < path.edges.size(); e++) { + BezierEdge be = path.edges.get(e); + BezierEdgeWrapper bew = new BezierEdgeWrapper(be, layer, p, e); + if (path.fillStyle0 == 0 && path.fillStyle1 == 0) { + strokesBewList.add(bew); + } else { + fillsBewList.add(bew); + } + } + } + } + + handleBewList(strokesBewList, layers); + handleBewList(fillsBewList, layers); + + for (int layer = 0; layer < layers.size(); layer++) { + for (int p = 0; p < layers.get(layer).paths.size(); p++) { + Path path = layers.get(layer).paths.get(p); + for (int e = 0; e < path.edges.size(); e++) { + BezierEdge be1 = path.edges.get(e); + be1.shrinkToLine(); + } + } + } + } +} diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/xfl/shapefixer/Path.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/xfl/shapefixer/Path.java new file mode 100644 index 000000000..b18f79a9b --- /dev/null +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/xfl/shapefixer/Path.java @@ -0,0 +1,203 @@ +/* + * Click nbfs://nbhost/SystemFileSystem/Templates/Licenses/license-default.txt to change this license + * Click nbfs://nbhost/SystemFileSystem/Templates/Classes/Class.java to edit this template + */ +package com.jpexs.decompiler.flash.xfl.shapefixer; + +import com.jpexs.decompiler.flash.math.BezierEdge; +import com.jpexs.helpers.Reference; +import java.awt.geom.Area; +import java.awt.geom.GeneralPath; +import java.awt.geom.PathIterator; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.util.ArrayList; +import java.util.List; + +/** + * + * @author JPEXS + */ +public class Path { + + private static final double EPS = 1e-9; + + public List edges = new ArrayList<>(); + public int fillStyle0 = 0; + public int fillStyle1 = 0; + public int lineStyle = 0; + + public Area area = null; + + public Double areaValue = null; + public boolean counterClockWise = false; + public Rectangle2D bbox = null; + + public List children = new ArrayList<>(); + public Path parent = null; + public boolean filled = false; + + private void calculateOrientation() { + Reference orientationRef = new Reference<>(null); + Reference areaRef = new Reference<>(0.0); + PathArea.orientationSingleClosed(area, orientationRef, areaRef); + areaValue = areaRef.getVal(); + this.counterClockWise = orientationRef.getVal() == PathArea.Orientation.COUNTER_CLOCKWISE; + this.bbox = area.getBounds2D(); + } + + public boolean contains(Path other) { + if (other.area.isEmpty()) { + return false; + } + Area diff = new Area(other.area); + diff.subtract(area); + return diff.isEmpty(); + } + + public boolean contains(Point2D point) { + if (area == null) { + toArea(); + } + return area.contains(point); + } + + + public boolean contains(double x, double y) { + if (area == null) { + toArea(); + } + return area.contains(x, y); + } + + public void round(boolean wasSmall) { + for (int e = 0; e < edges.size(); e++) { + BezierEdge be = edges.get(e); + /*if (wasSmall) { + be.roundN(2); //this value works best for #1011, why? Also for #2532 it is okay. + } else { + be.roundN(100); //this value works best for #2165, it's not multiplied by 20 like not small :-( + }*/ + be.roundN(1); + if (be.isEmpty()) { + edges.remove(e); + e--; + } + } + } + + public void fromArea() { + calculateOrientation(); + List newEdges = new ArrayList<>(); + PathIterator it = area.getPathIterator(null); + double[] c = new double[6]; + + double startX = 0.0; + double startY = 0.0; // subpath start (for closing) + double prevX = 0.0; + double prevY = 0.0; // previous "current point" + + while (!it.isDone()) { + int type = it.currentSegment(c); + + switch (type) { + case PathIterator.SEG_MOVETO: { + // Start of a new subpath + startX = prevX = c[0]; + startY = prevY = c[1]; + break; + } + case PathIterator.SEG_LINETO: { + // Line from (prevX, prevY) to (x, y) + double x = c[0], y = c[1]; + newEdges.add(new BezierEdge(prevX, prevY, x, y)); + prevX = x; + prevY = y; + break; + } + case PathIterator.SEG_QUADTO: { + // Quadratic from (prevX, prevY) with control (cx, cy) to (x, y) + double cx = c[0], cy = c[1]; + double x = c[2], y = c[3]; + newEdges.add(new BezierEdge(prevX, prevY, cx, cy, x, y)); + prevX = x; + prevY = y; + break; + } + case PathIterator.SEG_CUBICTO: { + // Area may contain cubics if the original shape had them. + // We only support lines and quadratics here. + throw new IllegalArgumentException("Cubic Bezier segments (SEG_CUBICTO) are not supported."); + } + case PathIterator.SEG_CLOSE: { + // Close current subpath: add a final edge back to start if not already there + if (!almostEqual(prevX, startX) || !almostEqual(prevY, startY)) { + newEdges.add(new BezierEdge(prevX, prevY, startX, startY)); + prevX = startX; + prevY = startY; + } + break; + } + default: + // Should not happen for AWT paths + throw new IllegalStateException("Unknown PathIterator segment type: " + type); + } + + it.next(); + } + this.edges = newEdges; + } + + private boolean almostEqual(double a, double b) { + return Math.abs(a - b) <= EPS; + } + + public void toArea() { + GeneralPath gp = new GeneralPath(); + double x = Double.POSITIVE_INFINITY; + double y = Double.POSITIVE_INFINITY; + boolean empty = true; + for (BezierEdge be : edges) { + if (be.isEmpty()) { + continue; + } + if (x != be.getBeginPoint().getX() || y != be.getBeginPoint().getY()) { + gp.moveTo(be.getBeginPoint().getX(), be.getBeginPoint().getY()); + } + if (be.isQuad()) { + gp.quadTo(be.points.get(1).getX(), be.points.get(1).getY(), be.getEndPoint().getX(), be.getEndPoint().getY()); + } else { + gp.lineTo(be.getEndPoint().getX(), be.getEndPoint().getY()); + } + x = be.getEndPoint().getX(); + y = be.getEndPoint().getY(); + empty = false; + } + try { + this.area = empty ? new Area() : new Area(gp); + } catch (InternalError ie) { + System.err.println("INTERNAL error on PATH " + toString()); + this.area = new Area(); + } + calculateOrientation(); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + Point2D lastPoint = null; + for (BezierEdge e : edges) { + if (lastPoint == null || !lastPoint.equals(e.getBeginPoint())) { + sb.append("M ").append(e.getBeginPoint().getX()).append(" ").append(e.getBeginPoint().getY()).append(" "); + } + if (e.isQuad()) { + sb.append("Q ").append(e.points.get(1).getX()).append(" ").append(e.points.get(1).getY()).append(" "); + } else { + sb.append("L "); + } + sb.append(e.getEndPoint().getX()).append(" ").append(e.getEndPoint().getY()).append(" "); + lastPoint = e.getEndPoint(); + } + return sb.toString().trim(); + } + } 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 d327fc11d..d944d292c 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 @@ -870,7 +870,7 @@ public class ShapeFixer { if (Configuration.flaExportFixShapes.get()) { SwitchedFillSidesFixer switchedFillSidesFixer = new SwitchedFillSidesFixer(); - switchedFillSidesFixer.fixSwitchedFills(shapeNum, records, baseFillStyles, baseLineStyles, shapes, fillStyles0, fillStyles1, layers); + switchedFillSidesFixer.fixSwitchedFills(shapeNum, records, baseFillStyles, baseLineStyles, shapes, fillStyles0, fillStyles1, lineStyles, layers); detectOverlappingEdges(shapes, fillStyles0, fillStyles1, lineStyles, layers, wasSmall); } diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/xfl/shapefixer/ShapeFixer2.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/xfl/shapefixer/ShapeFixer2.java new file mode 100644 index 000000000..63056be6a --- /dev/null +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/xfl/shapefixer/ShapeFixer2.java @@ -0,0 +1,967 @@ +package com.jpexs.decompiler.flash.xfl.shapefixer; + +import com.jpexs.decompiler.flash.SWF; +import com.jpexs.decompiler.flash.configuration.Configuration; +import com.jpexs.decompiler.flash.exporters.commonshape.Matrix; +import com.jpexs.decompiler.flash.exporters.shape.CurvedEdge; +import com.jpexs.decompiler.flash.exporters.shape.IEdge; +import com.jpexs.decompiler.flash.exporters.shape.ShapeExporterBase; +import com.jpexs.decompiler.flash.math.BezierEdge; +import com.jpexs.decompiler.flash.tags.base.ShapeTag; +import com.jpexs.decompiler.flash.types.ColorTransform; +import com.jpexs.decompiler.flash.types.FILLSTYLEARRAY; +import com.jpexs.decompiler.flash.types.GRADRECORD; +import com.jpexs.decompiler.flash.types.LINESTYLEARRAY; +import com.jpexs.decompiler.flash.types.RGB; +import com.jpexs.decompiler.flash.types.SHAPEWITHSTYLE; +import com.jpexs.decompiler.flash.types.shaperecords.CurvedEdgeRecord; +import com.jpexs.decompiler.flash.types.shaperecords.EndShapeRecord; +import com.jpexs.decompiler.flash.types.shaperecords.SHAPERECORD; +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.Area; +import java.awt.geom.GeneralPath; +import java.awt.geom.PathIterator; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.util.ArrayList; +import java.util.Collection; +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.Set; + +/** + * + * @author JPEXS + */ +public class ShapeFixer2 { + + private List splitToLayers( + List records, + FILLSTYLEARRAY baseFillStyles, + LINESTYLEARRAY baseLineStyles + ) { + List result = new ArrayList<>(); + + Layer currentLayer = new Layer(); + currentLayer.fillStyleArray = baseFillStyles; + currentLayer.lineStyleArray = baseLineStyles; + + List currentEdges = new ArrayList<>(); + + int fillStyle0 = 0; + int fillStyle1 = 0; + int lineStyle = 0; + int x = 0; + int y = 0; + for (SHAPERECORD rec : records) { + assert (rec != null); + if (rec instanceof StyleChangeRecord) { + StyleChangeRecord scr = (StyleChangeRecord) rec; + if (scr.stateMoveTo + || scr.stateNewStyles + || scr.stateFillStyle0 + || scr.stateFillStyle1 + || scr.stateLineStyle) { + if (!currentEdges.isEmpty()) { + Path path = new Path(); + path.edges = currentEdges; + path.fillStyle0 = fillStyle0; + path.fillStyle1 = fillStyle1; + path.lineStyle = lineStyle; + currentLayer.paths.add(path); + currentEdges = new ArrayList<>(); + } + } + if (scr.stateNewStyles) { + if (!currentLayer.paths.isEmpty()) { + result.add(currentLayer); + } + currentLayer = new Layer(); + currentLayer.fillStyleArray = scr.fillStyles; + currentLayer.lineStyleArray = scr.lineStyles; + fillStyle0 = 0; + fillStyle1 = 0; + lineStyle = 0; + } + if (scr.stateFillStyle0) { + fillStyle0 = scr.fillStyle0; + } + if (scr.stateFillStyle1) { + fillStyle1 = scr.fillStyle1; + } + if (scr.stateLineStyle) { + lineStyle = scr.lineStyle; + } + } + if (rec instanceof StraightEdgeRecord) { + int x2 = rec.changeX(x); + int y2 = rec.changeY(y); + BezierEdge be = new BezierEdge(x, y, x2, y2); + currentEdges.add(be); + } + if (rec instanceof CurvedEdgeRecord) { + CurvedEdgeRecord cer = (CurvedEdgeRecord) rec; + int cx = x + cer.controlDeltaX; + int cy = y + cer.controlDeltaY; + int ax = cx + cer.anchorDeltaX; + int ay = cy + cer.anchorDeltaY; + BezierEdge be = new BezierEdge(x, y, cx, cy, ax, ay); + currentEdges.add(be); + } + if (rec instanceof EndShapeRecord) { + if (!currentEdges.isEmpty()) { + Path path = new Path(); + path.edges = currentEdges; + path.fillStyle0 = fillStyle0; + path.fillStyle1 = fillStyle1; + path.lineStyle = lineStyle; + currentLayer.paths.add(path); + result.add(currentLayer); + currentEdges = new ArrayList<>(); + } + } + x = rec.changeX(x); + y = rec.changeY(y); + } + return result; + } + + private List combineLayers(List layers, FILLSTYLEARRAY baseFillStyles, LINESTYLEARRAY baseLineStyles) { + List ret = new ArrayList<>(); + double dx; + double dy; + for (int i = 0; i < layers.size(); i++) { + Layer layer = layers.get(i); + if (layer.paths.isEmpty()) { + continue; + } + if (layer.fillStyleArray != baseFillStyles && layer.lineStyleArray != baseLineStyles) { + StyleChangeRecordAdvanced scr = new StyleChangeRecordAdvanced(); + scr.stateNewStyles = true; + scr.fillStyles = layer.fillStyleArray; + scr.lineStyles = layer.lineStyleArray; + scr.stateFillStyle0 = true; + scr.fillStyle0 = 0; + scr.stateFillStyle1 = true; + scr.fillStyle1 = 0; + scr.stateLineStyle = true; + scr.lineStyle = 0; + ret.add(scr); + } + for (Path path : layer.paths) { + if (path.edges.isEmpty()) { + continue; + } + StyleChangeRecordAdvanced scr = new StyleChangeRecordAdvanced(); + scr.stateMoveTo = true; + dx = scr.moveDeltaX = path.edges.get(0).points.get(0).getX(); + dy = scr.moveDeltaY = path.edges.get(0).points.get(0).getY(); + + scr.stateFillStyle0 = true; + scr.fillStyle0 = path.fillStyle0; + scr.stateFillStyle1 = true; + scr.fillStyle1 = path.fillStyle1; + scr.stateLineStyle = true; + scr.lineStyle = path.lineStyle; + ret.add(scr); + for (BezierEdge be : path.edges) { + if (!be.getBeginPoint().equals(new Point2D.Double(dx, dy))) { + StyleChangeRecordAdvanced sm = new StyleChangeRecordAdvanced(); + sm.stateMoveTo = true; + sm.moveDeltaX = be.getBeginPoint().getX(); + sm.moveDeltaY = be.getBeginPoint().getY(); + ret.add(sm); + } + dx = be.getEndPoint().getX(); + dy = be.getEndPoint().getY(); + + ShapeRecordAdvanced sra = bezierToAdvancedRecord(be); + ret.add(sra); + } + } + } + return ret; + } + + private ShapeRecordAdvanced bezierToAdvancedRecord(BezierEdge be) { + if (be.points.size() == 2) { + StraightEdgeRecordAdvanced ser = new StraightEdgeRecordAdvanced(); + ser.deltaX = be.points.get(1).getX() - be.points.get(0).getX(); + ser.deltaY = be.points.get(1).getY() - be.points.get(0).getY(); + return ser; + } + if (be.points.size() == 3) { + CurvedEdgeRecordAdvanced cer = new CurvedEdgeRecordAdvanced(); + cer.controlDeltaX = be.points.get(1).getX() - be.points.get(0).getX(); + cer.controlDeltaY = be.points.get(1).getY() - be.points.get(0).getY(); + cer.anchorDeltaX = be.points.get(2).getX() - be.points.get(1).getX(); + cer.anchorDeltaY = be.points.get(2).getY() - be.points.get(1).getY(); + return cer; + } + return null; + } + + public List fix( + List records, + int shapeNum, + FILLSTYLEARRAY baseFillStyles, + LINESTYLEARRAY baseLineStyles, + boolean wasSmall + ) { + + List layers = splitToLayers(records, baseFillStyles, baseLineStyles); + + getSingleFillLayers(layers, records, baseFillStyles, baseLineStyles, shapeNum); + + /*for (Layer layer : layers) { + subtractAreas(layer); + } + + removeEmpty(layers);*/ + + OverlappingEdgesSplitter splitter = new OverlappingEdgesSplitter(); + splitter.splitOverlappingEdges(layers); + + for (Layer layer : layers) { + detectEdgeFills(layer); + } + + for (Layer layer : layers) { + layer.round(wasSmall); + } + + removeEmpty(layers); + + /*for (Layer layer : layers) { + fixFillSides(layer); + }*/ + + System.err.println("============="); + for (int i = 0; i < layers.size(); i++) { + for (int j = 0; j < layers.get(i).paths.size(); j++) { + Path p = layers.get(i).paths.get(j); + System.err.println(p.toString()); + //System.err.println("FS0: " + p.fillStyle0); + //System.err.println("FS1: " + p.fillStyle1); + //System.err.println("LS: " + p.lineStyle); + //System.err.println("----------"); + } + } + //System.exit(0); + return combineLayers(layers, baseFillStyles, baseLineStyles); + } + + private void removeEmpty(List layers) { + for (int i = 0; i < layers.size(); i++) { + for (int j = 0; j < layers.get(i).paths.size(); j++) { + for (int e = 0; e < layers.get(i).paths.get(j).edges.size(); e++) { + if (layers.get(i).paths.get(j).edges.get(e).isEmpty()) { + layers.get(i).paths.get(j).edges.remove(e); + e--; + } + } + if (layers.get(i).paths.get(j).edges.isEmpty()) { + layers.get(i).paths.remove(j); + j--; + if (layers.get(i).paths.isEmpty()) { + layers.remove(i); + i--; + } + } + } + } + } + + private void detectEdgeFills(Layer layer) { + + double epsBase = 1e-4; + + List bboxes = new ArrayList<>(); + for (Path path : layer.paths){ + path.toArea(); + bboxes.add(path.bbox); + } + Set allEdges = new HashSet<>(); + List styledEdges = new ArrayList<>(); + for (Path path : layer.paths) { + for (BezierEdge be : path.edges) { + if (allEdges.contains(be) || allEdges.contains(be.reverse())) { + continue; + } + Point2D mid = be.midPoint(); + Point2D n = be.unitNormal(); + double len = Math.hypot(be.getEndPoint().getX() - be.getBeginPoint().getX(), be.getEndPoint().getY() - be.getBeginPoint().getY()); + double eps = Math.max(epsBase, 1e-4 * len); + + double xL = mid.getX() - n.getX()*eps; + double yL = mid.getY() - n.getY()*eps; + double xR = mid.getX() + n.getX()*eps; + double yR = mid.getY() + n.getY()*eps; + + int leftPath = topmostPathAt(xL, yL, layer.paths, bboxes); + int rightPath = topmostPathAt(xR, yR, layer.paths, bboxes); + + int leftFill = leftPath == -1 ? 0 : layer.paths.get(leftPath).fillStyle0; + int rightFill = rightPath == -1 ? 0 : layer.paths.get(rightPath).fillStyle0; + + int leftLine = leftPath == -1 ? 0 : layer.paths.get(leftPath).lineStyle; + int rightLine = rightPath == -1 ? 0 : layer.paths.get(rightPath).lineStyle; + + int newLine = (leftLine == path.lineStyle || rightLine == path.lineStyle) ? path.lineStyle : 0; + + if (leftFill == rightFill && newLine == 0) { + continue; + } + + allEdges.add(be); + + StyledEdge styledEdge = new StyledEdge(be, leftFill, rightFill, newLine); + styledEdges.add(styledEdge); + } + } + layer.paths.clear(); + + int lastFs0 = -1; + int lastFs1 = -1; + int lastLs = -1; + Path currentPath = null; + for (StyledEdge edge : styledEdges) { + if (edge.fillStyle0 != lastFs0 || edge.fillStyle1 != lastFs1 || edge.lineStyle != lastLs) { + currentPath = new Path(); + currentPath.fillStyle0 = edge.fillStyle0; + currentPath.fillStyle1 = edge.fillStyle1; + currentPath.lineStyle = edge.lineStyle; + layer.paths.add(currentPath); + } + + assert(currentPath != null); + currentPath.edges.add(edge.edge); + + lastFs0 = edge.fillStyle0; + lastFs1 = edge.fillStyle1; + lastLs = edge.lineStyle; + } + } + + private static int topmostPathAt( + double x, double y, + List paths, + List bboxes + ) { + + for (int i = paths.size() - 1; i >= 0; i--) { + Rectangle2D bb = bboxes.get(i); + if (!bb.contains(x, y)) { + continue; + } + // Use contains on the shape with its own winding rule + if (paths.get(i).contains(x, y)) { + return i; + } + } + return -1; + } + + private static class StyledEdge { + BezierEdge edge; + int fillStyle0 = 0; + int fillStyle1 = 0; + int lineStyle = 0; + + public StyledEdge(BezierEdge edge) { + this.edge = edge; + } + + public StyledEdge(BezierEdge edge, int fillStyle0, int fillStyle1, int lineStyle) { + this.edge = edge; + this.fillStyle0 = fillStyle0; + this.fillStyle1 = fillStyle1; + this.lineStyle = lineStyle; + } + + } + + private void fixFillSides(Layer layer) { + List paths = layer.paths; + + buildContainment(paths); + + for (Path poly : paths) { + for (Path child : poly.children) { + child.parent = poly; + } + } + + for (Path path : paths) { + int depth = 0; + + Path parent = path.parent; + while (parent != null) { + parent = parent.parent; + depth++; + } + path.filled = depth % 2 == 0; + } + + for (Path path: paths) { + boolean clockwise = !path.counterClockWise; + int fillStyle = path.fillStyle0; + path.fillStyle0 = 0; + if (path.filled == clockwise) { + path.fillStyle1 = fillStyle; + } else { + path.fillStyle0 = fillStyle; + } + } + + List newPaths = new ArrayList<>(); + Map> edge2Fs0 = new HashMap<>(); + Map> edge2Fs1 = new HashMap<>(); + + for (Path path : paths) { + for (BezierEdge edge : path.edges) { + if (path.fillStyle0 == 0 && path.fillStyle1 == 0) { + continue; + } + BezierEdge edgeRev = edge.reverse(); + + + if (!edge2Fs0.containsKey(edge)) { + edge2Fs0.put(edge, new ArrayList<>()); + } + edge2Fs0.get(edge).add(path.fillStyle0); + + if (!edge2Fs1.containsKey(edgeRev)) { + edge2Fs1.put(edgeRev, new ArrayList<>()); + } + edge2Fs1.get(edgeRev).add(path.fillStyle0); + + if (!edge2Fs1.containsKey(edge)) { + edge2Fs1.put(edge, new ArrayList<>()); + } + edge2Fs1.get(edge).add(path.fillStyle1); + + + if (!edge2Fs0.containsKey(edgeRev)) { + edge2Fs0.put(edgeRev, new ArrayList<>()); + } + edge2Fs0.get(edgeRev).add(path.fillStyle1); + } + } + + Set existingEdges = new LinkedHashSet<>(); + + for (int p = paths.size() - 1; p >= 0; p--) { + Path path = paths.get(p); + int lastFs0 = -1; + int lastFs1 = -1; + for (int e = path.edges.size() - 1; e >= 0; e--) { + BezierEdge edge = path.edges.get(e); + + List fs0List = edge2Fs0.get(edge); + List fs1List = edge2Fs1.get(edge); + + if (p == 1 && e == 34) { + System.err.println("yyy"); + } + + for (int i = 0; i < fs0List.size(); i++) { + Integer fs = fs0List.get(i); + if (fs1List.contains(fs)) { + fs0List.remove(i); + fs1List.remove(fs); + i--; + } + } + + + int fs0 = 0; + if (!fs0List.isEmpty()) { + fs0 = fs0List.get(fs0List.size() - 1); + } + int fs1 = 0; + if (!fs1List.isEmpty()) { + fs1 = fs1List.get(fs1List.size() - 1); + } + + if (fs0 == 0 && fs1 == 0 && path.lineStyle == 0) { + System.err.println("no fill or linestyle - " + edge.toSvg() + " original fs = " + path.fillStyle0+", ls = " + path.lineStyle); + path.edges.remove(e); + continue; + } + + BezierEdge edgeRev = edge.reverse(); + + if (existingEdges.contains(edge) || existingEdges.contains(edgeRev)) { + path.edges.remove(e); + continue; + } + + existingEdges.add(edge); + existingEdges.add(edgeRev); + + if (lastFs0 > -1 && lastFs1 > -1 && fs0 != lastFs0 || fs1 != lastFs1) { + Path newPath = new Path(); + newPath.edges = new ArrayList<>(); + newPath.fillStyle0 = lastFs0; + newPath.fillStyle1 = lastFs1; + newPath.lineStyle = path.lineStyle; + for (int n = e + 1; e + 1 < path.edges.size(); n++) { + newPath.edges.add(path.edges.remove(e + 1)); + } + if (!newPath.edges.isEmpty()) { + paths.add(p + 1, newPath); + } + } + + lastFs0 = fs0; + lastFs1 = fs1; + } + if (lastFs0 > -1 && lastFs1 > -1) { + path.fillStyle0 = lastFs0; + path.fillStyle1 = lastFs1; + } + if (path.edges.isEmpty()) { + paths.remove(p); + } + } + } + + private void subtractAreas(Layer layer) { + for (Path path : layer.paths) { + path.toArea(); + } + + for (int p1 = 0; p1 < layer.paths.size(); p1++) { + System.err.println("FROM " + layer.paths.get(p1)); + for (int p2 = p1 + 1; p2 < layer.paths.size(); p2++) { + System.err.println("Subtract " + layer.paths.get(p2)); + layer.paths.get(p1).area.subtract(layer.paths.get(p2).area); + } + layer.paths.get(p1).fromArea(); + System.err.println("Result: " + layer.paths.get(p1)); + System.err.println(""); + } + } + + private void getSingleFillLayers(List layers, List records, FILLSTYLEARRAY baseFillStyles, LINESTYLEARRAY baseLineStyles, int shapeNum) { + SHAPEWITHSTYLE shp = new SHAPEWITHSTYLE(); + shp.shapeRecords = records; + shp.fillStyles = baseFillStyles; + shp.lineStyles = baseLineStyles; + + List> fillList = new ArrayList<>(); + + SWF swf = new SWF(); + new ShapeExporterBase(ShapeTag.WIND_EVEN_ODD, shapeNum, swf, shp, null) { + @Override + protected void handleFillPaths(List> fillPaths) { + fillList.addAll(fillPaths); + } + + @Override + public void beginShape() { + } + + @Override + public void endShape() { + } + + @Override + public void beginFills() { + } + + @Override + public void endFills() { + } + + @Override + public void beginLines() { + } + + @Override + public void endLines(boolean close) { + } + + @Override + public void beginFill(RGB color) { + } + + @Override + public void beginGradientFill(int type, GRADRECORD[] gradientRecords, Matrix matrix, int spreadMethod, int interpolationMethod, float focalPointRatio) { + } + + @Override + public void beginBitmapFill(int bitmapId, Matrix matrix, boolean repeat, boolean smooth, ColorTransform colorTransform) { + } + + @Override + public void endFill() { + } + + @Override + public void lineStyle(double thickness, RGB color, boolean pixelHinting, String scaleMode, int startCaps, int endCaps, int joints, float miterLimit, boolean noClose) { + } + + @Override + public void lineGradientStyle(int type, GRADRECORD[] gradientRecords, Matrix matrix, int spreadMethod, int interpolationMethod, float focalPointRatio) { + } + + @Override + public void lineBitmapStyle(int bitmapId, Matrix matrix, boolean repeat, boolean smooth, ColorTransform colorTransform) { + } + + @Override + public void moveTo(double x, double y) { + } + + @Override + public void lineTo(double x, double y) { + } + + @Override + public void curveTo(double controlX, double controlY, double anchorX, double anchorY) { + } + }; + + Map globalToLocalFillStyleMap = new LinkedHashMap<>(); + Map globalToLocalLineStyleMap = new LinkedHashMap<>(); + int lastFs = 0; + int lastLs = 0; + globalToLocalFillStyleMap.put(0, 0); + globalToLocalLineStyleMap.put(0, 0); + for (int i = 0; i < baseFillStyles.fillStyles.length; i++) { + lastFs++; + globalToLocalFillStyleMap.put(lastFs, lastFs); + } + for (int i = 0; i < (shapeNum == 4 ? baseLineStyles.lineStyles2.length : baseLineStyles.lineStyles.length); i++) { + lastLs++; + globalToLocalLineStyleMap.put(lastLs, lastLs); + } + for (SHAPERECORD rec : records) { + if (rec instanceof StyleChangeRecord) { + StyleChangeRecord scr = (StyleChangeRecord) rec; + if (scr.stateNewStyles) { + for (int i = 0; i < scr.fillStyles.fillStyles.length; i++) { + lastFs++; + globalToLocalFillStyleMap.put(lastFs, i + 1); + } + for (int i = 0; i < (shapeNum == 4 ? scr.lineStyles.lineStyles2.length : scr.lineStyles.lineStyles.length); i++) { + lastLs++; + globalToLocalLineStyleMap.put(lastLs, i + 1); + } + } + } + } + + //assert(layers.size() == fillList.size()); + for (int i = 0; i < fillList.size(); i++) { + if (fillList.get(i).isEmpty()) { + fillList.remove(i); + i--; + continue; + } + } + + for (int layer = 0; layer < layers.size(); layer++) { + Layer layerObj = layers.get(layer); + layerObj.paths.clear(); + int fillStyleIdx = Integer.MAX_VALUE; + int lineStyleIdx = Integer.MAX_VALUE; + List currentList = new ArrayList<>(); + List> allLists = new ArrayList<>(); + List listFills = new ArrayList<>(); + List listLines = new ArrayList<>(); + int lastToX = Integer.MAX_VALUE; + int lastToY = Integer.MAX_VALUE; + int lastMoveToX = Integer.MAX_VALUE; + int lastMoveToY = Integer.MAX_VALUE; + for (int i = 0; i < fillList.get(layer).size(); i++) { + IEdge e = fillList.get(layer).get(i); + if (fillStyleIdx != e.getFillStyleIdx() + || lineStyleIdx != e.getLineStyleIdx() + || (e.getFromX() != lastToX) || (e.getFromY() != lastToY) + || (e.getFromX() == lastMoveToX && e.getFromY() == lastMoveToY)) { + if (fillStyleIdx != Integer.MAX_VALUE) { + allLists.add(currentList); + listFills.add(fillStyleIdx); + listLines.add(lineStyleIdx); + currentList = new ArrayList<>(); + } + fillStyleIdx = e.getFillStyleIdx(); + lineStyleIdx = e.getLineStyleIdx(); + lastMoveToX = e.getFromX(); + lastMoveToY = e.getFromY(); + } + currentList.add(e); + lastToX = e.getToX(); + lastToY = e.getToY(); + } + if (!currentList.isEmpty()) { + allLists.add(currentList); + listFills.add(fillStyleIdx); + listLines.add(lineStyleIdx); + } + + /*List closedPaths = new ArrayList<>(); + for (int i = 0; i < allLists.size(); i++) { + List list = allLists.get(i); + closedPaths.add(new ClosedPath(list, listFills.get(i))); + } + + buildContainment(closedPaths); + + for (ClosedPath poly : closedPaths) { + for (ClosedPath child : poly.children) { + child.parent = poly; + } + } + + for (ClosedPath poly : closedPaths) { + int depth = 0; + + ClosedPath parent = poly.parent; + while (parent != null) { + parent = parent.parent; + depth++; + } + + poly.filled = depth % 2 == 0; + } + + Map> beToFillStyle0List = new LinkedHashMap<>(); + Map> beToFillStyle1List = new LinkedHashMap<>(); + + Map beToFillStyle0 = new LinkedHashMap<>(); + Map beToFillStyle1 = new LinkedHashMap<>(); + + */ + for (int i = 0; i < allLists.size(); i++) { + List list = allLists.get(i); + fillStyleIdx = listFills.get(i); + lineStyleIdx = listLines.get(i); + //boolean clockwise = !polygon.ccw; + + Path path = new Path(); + layerObj.paths.add(path); + + int localFs = globalToLocalFillStyleMap.get(fillStyleIdx); + int localLs = globalToLocalLineStyleMap.get(lineStyleIdx); + + /*if (polygon.filled == clockwise) { + path.fillStyle1 = localFs; + } else { + path.fillStyle0 = localFs; + }*/ + path.fillStyle0 = localFs; + path.lineStyle = localLs; + + for (IEdge e : list) { + BezierEdge be = iEdgeToBezier(e); + //BezierEdge beRev = be.reverse(); + path.edges.add(be); + + //BezierEdge search = new BezierEdge(180.0, -3040.0, 480.0, -3400.0); + //boolean print = false; + + /*if (be.equals(search)) { + System.err.println("xxx"); + System.err.println("" + polygon); + print = true; + } + if (be.equals(search.reverse())) { + System.err.println("yyy"); + print = true; + }*/ + /*if (print) { + System.err.println("localFS: " + localFs); + System.err.println("filled: " + polygon.filled); + System.err.println("clockwise: " + clockwise); + } + + if (polygon.filled == clockwise) { + if (!beToFillStyle1List.containsKey(be)) { + beToFillStyle1List.put(be, new ArrayList<>()); + } + if (!beToFillStyle0List.containsKey(beRev)) { + beToFillStyle0List.put(beRev, new ArrayList<>()); + } + beToFillStyle1List.get(be).add(localFs); + beToFillStyle0List.get(beRev).add(localFs); + + if (print) { + System.err.println("setting FS1 and rev FS0"); + } + + } else { + if (!beToFillStyle0List.containsKey(be)) { + beToFillStyle0List.put(be, new ArrayList<>()); + } + if (!beToFillStyle1List.containsKey(beRev)) { + beToFillStyle1List.put(beRev, new ArrayList<>()); + } + + beToFillStyle0List.get(be).add(localFs); + beToFillStyle1List.get(beRev).add(localFs); + + if (print) { + System.err.println("setting FS0 and rev FS1"); + } + + } + + if (print) { + System.err.println(""); + }*/ + } + + } + } + + } + + private BezierEdge iEdgeToBezier(IEdge ie) { + assert (ie != null); + if (ie instanceof CurvedEdge) { + CurvedEdge ce = (CurvedEdge) ie; + return new BezierEdge(ce.getFromX(), ce.getFromY(), + ce.getControlX(), ce.getControlY(), + ce.getToX(), ce.getToY() + ); + } else { + return new BezierEdge(ie.getFromX(), ie.getFromY(), ie.getToX(), ie.getToY()); + } + } + + static class GridIndex { + + // Simple uniform grid over bbox domain + private final double cellSize; + private final Map> cells = new HashMap<>(); + 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; + double miny = Double.POSITIVE_INFINITY; + for (Path w : polys) { + Rectangle2D b = w.bbox; + if (b.getMinX() < minx) { + minx = b.getMinX(); + } + if (b.getMinY() < miny) { + miny = b.getMinY(); + } + } + this.minX = minx; + this.minY = miny; + + // Insert + for (Path w : polys) { + forEachCell(w.bbox, (gx, gy) -> { + cells.computeIfAbsent(key(gx, gy), k -> new ArrayList<>()).add(w); + }); + } + } + + private long key(int gx, int gy) { + // Pack two 32-bit ints into one long + return ((long) gx << 32) ^ (gy & 0xffffffffL); + } + + private int gx(double x) { + return (int) Math.floor((x - minX) / cellSize); + } + + private int gy(double y) { + return (int) Math.floor((y - minY) / cellSize); + } + + private void forEachCell(Rectangle2D r, CellConsumer cc) { + int x0 = gx(r.getMinX()); + int x1 = gx(r.getMaxX()); + int y0 = gy(r.getMinY()); + int y1 = gy(r.getMaxY()); + for (int x = x0; x <= x1; x++) { + for (int y = y0; y <= y1; y++) { + cc.accept(x, y); + } + } + } + + List query(Rectangle2D r) { + // Collect candidates from overlapping cells (deduplicated) + HashSet set = new HashSet<>(); + forEachCell(r, (gx, gy) -> { + List bucket = cells.get(key(gx, gy)); + if (bucket != null) { + set.addAll(bucket); + } + }); + return new ArrayList<>(set); + } + } + + interface CellConsumer { + + void accept(int gx, int gy); + } + + private static void buildContainment(List polygons) { + + Map> byStyle = polygons.stream() + .collect(java.util.stream.Collectors.groupingBy(w -> w.fillStyle0)); + + for (Map.Entry> e : byStyle.entrySet()) { + List group = e.getValue(); + if (e.getKey() == 0) { //no fill + continue; + } + + double avgW = group.stream().mapToDouble(w -> w.bbox.getWidth()).average().orElse(1.0); + double avgH = group.stream().mapToDouble(w -> w.bbox.getHeight()).average().orElse(1.0); + double cellSize = Math.max(1.0, Math.max(avgW, avgH)); + + GridIndex index = new GridIndex(group, cellSize); + + group.sort((a, b) -> Double.compare(b.areaValue, a.areaValue)); + + for (int i = group.size() - 1; i >= 0; i--) { + Path inner = group.get(i); + List candidates = index.query(inner.bbox); + + Path bestParent = null; + double bestArea = Double.POSITIVE_INFINITY; + + for (Path outer : candidates) { + if (outer == inner) { + continue; + } + if (outer.areaValue <= inner.areaValue) { + continue; // only larger can contain + } + if (!outer.bbox.contains(inner.bbox)) { + continue; // cheap reject + } + if (outer.contains(inner)) { + if (outer.areaValue < bestArea) { + bestArea = outer.areaValue; + bestParent = outer; + } + } + } + if (bestParent != null) { + bestParent.children.add(inner); + } + } + } + } +} 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 df8e53bde..b5552810b 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 @@ -43,6 +43,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; @@ -279,6 +280,80 @@ public class SwitchedFillSidesFixer { } } } + } + + private static class ExistingEdge { + int fillStyle0; + int fillStyle1; + int lineStyle; + + BezierEdge be; + + public ExistingEdge(int fillStyle0, int fillStyle1, int lineStyle, BezierEdge be) { + this.fillStyle0 = fillStyle0; + this.fillStyle1 = fillStyle1; + this.lineStyle = lineStyle; + this.be = be; + } + + @Override + public int hashCode() { + int hash = 7; + hash = lineStyle == 0 ? 0 : 1; + return hash; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final ExistingEdge other = (ExistingEdge) obj; + + if ((this.fillStyle0 == 0 && this.fillStyle1 == 0) + != (other.fillStyle0 == 0 && other.fillStyle1 == 0)) { + return false; + } + + if (this.be.equals(other.be)) { + /*if (this.fillStyle0 != other.fillStyle0) { + return false; + } + if (this.fillStyle1 != other.fillStyle1) { + return false; + } + if (this.lineStyle != other.lineStyle) { + return false; + }*/ + + return true; + } + if (this.be.equalsReverse(other.be)) { + /*if (this.fillStyle0 != other.fillStyle1) { + return false; + } + if (this.fillStyle1 != other.fillStyle0) { + return false; + } + if (this.lineStyle != other.lineStyle) { + return false; + }*/ + + + return true; + } + + return false; + } + + + } private void fixSidesInLayer( @@ -286,8 +361,10 @@ public class SwitchedFillSidesFixer { List> shapes, List fillStyles0, List fillStyles1, + List lineStyles, int layer, - int startIndex, int endIndex, + List layers, + int startIndex, Reference endIndex, Map globalToLocalFillStyleMap ) { @@ -427,6 +504,8 @@ public class SwitchedFillSidesFixer { } } + + Set existingEdges = new HashSet<>(); for (BezierEdge be : beToFillStyle0List.keySet()) { /*for (int i = beToFillStyle0List.get(be).size() - 1; i >= 0; i--) { @@ -461,20 +540,39 @@ public class SwitchedFillSidesFixer { } } - for (int i = startIndex; i < endIndex; i++) { + for (int i = startIndex; i < endIndex.getVal(); i++) { List shape = shapes.get(i); + + int lastFs0 = -1; + int lastFs1 = -1; + int lastLs = -1; + List newFillStyles0 = new ArrayList<>(); + List newFillStyles1 = new ArrayList<>(); + List newLineStyles = new ArrayList<>(); + List> newShapes = new ArrayList<>(); + List currentShape = new ArrayList<>(); + List newLayers = new ArrayList<>(); + + Integer fs0before = fillStyles0.get(i); + Integer fs1before = fillStyles1.get(i); + int ls = lineStyles.get(i); + + shapes.remove(i); + fillStyles0.remove(i); + fillStyles1.remove(i); + lineStyles.remove(i); + layers.remove(i); + for (int j = 0; j < shape.size(); j++) { BezierEdge be = shape.get(j); if (be.isEmpty()) { continue; - } - - Integer fs0before = fillStyles0.get(i); - Integer fs1before = fillStyles1.get(i); - - if (fs0before == 0 && fs1before == 0) { //only strokes + } + + + /*if (fs0before == 0 && fs1before == 0) { //only strokes break; - } + }*/ Integer fs0after = beToFillStyle0.get(be); Integer fs1after = beToFillStyle1.get(be); @@ -487,7 +585,11 @@ public class SwitchedFillSidesFixer { } if (fs0after == -1 || fs1after == -1) { - break; + //?? + fs0after = fs0before; + fs1after = fs1before; + //break; + Logger.getLogger(SwitchedFillSidesFixer.class.getName()).log(Level.FINE, "More than 2 fillstyles for {0} - old: {1}, {2} new: {3}, {4}", new Object[]{be, fs0before, fs1before, fs0after, fs1after}); } if (fs0after == 0 && Objects.equals(fs1after, fs1before)) { @@ -495,33 +597,75 @@ public class SwitchedFillSidesFixer { } else if (fs1after == 0 && Objects.equals(fs0after, fs0before)) { fs1after = fs1before; } - - fillStyles0.set(i, fs0after); - fillStyles1.set(i, fs1after); + + ExistingEdge ee = new ExistingEdge(fs0after, fs1after, ls, be); + if (existingEdges.contains(ee)) { + Logger.getLogger(SwitchedFillSidesFixer.class.getName()).log(Level.FINE, "Duplicated edge {0} - old: {1}, {2} new: {3}, {4}", new Object[]{be, fs0before, fs1before, fs0after, fs1after}); + continue; + } + existingEdges.add(ee); + + if (lastFs0 != fs0after || lastFs1 != fs1after || lastLs != ls) { + if (!currentShape.isEmpty()) { + newShapes.add(currentShape); + currentShape = new ArrayList<>(); + newFillStyles0.add(lastFs0); + newFillStyles1.add(lastFs1); + newLineStyles.add(lastLs); + newLayers.add(layer - 1); + } + lastFs0 = fs0after; + lastFs1 = fs1after; + lastLs = ls; + } + + currentShape.add(be); + + /*fillStyles0.set(i, fs0after); + fillStyles1.set(i, fs1after);*/ if (!Objects.equals(fs0before, fs0after) || !Objects.equals(fs1before, fs1after)) { Logger.getLogger(SwitchedFillSidesFixer.class.getName()).log(Level.FINE, "Changed edge {0} - old: {1}, {2} new: {3}, {4}", new Object[]{be, fs0before, fs1before, fs0after, fs1after}); } - break; + //break; } + if (!currentShape.isEmpty()) { + newShapes.add(currentShape); + newFillStyles0.add(lastFs0); + newFillStyles1.add(lastFs1); + newLineStyles.add(lastLs); + newLayers.add(layer - 1); + } + + /*if (newShapes.size() > 1) { + Logger.getLogger(SwitchedFillSidesFixer.class.getName()).log(Level.FINE, "Multi shape - size = {0}", new Object[]{newShapes.size()}); + }*/ + endIndex.setVal(endIndex.getVal() - 1 + newShapes.size()); + shapes.addAll(i, newShapes); + fillStyles0.addAll(i, newFillStyles0); + fillStyles1.addAll(i, newFillStyles1); + lineStyles.addAll(i, newLineStyles); + layers.addAll(i, newLayers); + i += newShapes.size() - 1; } } public void fixSwitchedFills( int shapeNum, List records, - FILLSTYLEARRAY fillStyles, - LINESTYLEARRAY lineStyles, + FILLSTYLEARRAY baseFillStyles, + LINESTYLEARRAY baseLineStyles, List> shapes, List fillStyles0, List fillStyles1, + List lineStyles, List layers ) { SHAPEWITHSTYLE shp = new SHAPEWITHSTYLE(); shp.shapeRecords = records; - shp.fillStyles = fillStyles; - shp.lineStyles = lineStyles; + shp.fillStyles = baseFillStyles; + shp.lineStyles = baseLineStyles; List> fillList = new ArrayList<>(); @@ -600,7 +744,7 @@ public class SwitchedFillSidesFixer { Map globalToLocalFillStyleMap = new LinkedHashMap<>(); int lastFs = 0; globalToLocalFillStyleMap.put(0, 0); - for (int i = 0; i < fillStyles.fillStyles.length; i++) { + for (int i = 0; i < baseFillStyles.fillStyles.length; i++) { lastFs++; globalToLocalFillStyleMap.put(lastFs, lastFs); } @@ -619,12 +763,15 @@ public class SwitchedFillSidesFixer { int from = 0; for (int i = 1; i < layers.size(); i++) { if (!layers.get(i).equals(layers.get(i - 1))) { - fixSidesInLayer(fillList, shapes, fillStyles0, fillStyles1, layers.get(i - 1), from, i, globalToLocalFillStyleMap); + Reference endIndexRef = new Reference<>(i); + fixSidesInLayer(fillList, shapes, fillStyles0, fillStyles1, lineStyles, layers.get(i - 1), layers, from, endIndexRef, globalToLocalFillStyleMap); + i = endIndexRef.getVal(); from = i; } } if (!layers.isEmpty()) { - fixSidesInLayer(fillList, shapes, fillStyles0, fillStyles1, layers.get(layers.size() - 1), from, layers.size(), globalToLocalFillStyleMap); + Reference endIndexRef = new Reference<>(layers.size()); + fixSidesInLayer(fillList, shapes, fillStyles0, fillStyles1, lineStyles, layers.get(layers.size() - 1), layers, from, endIndexRef, globalToLocalFillStyleMap); } } }