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 9eea5521d..0d009e43e 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 @@ -904,7 +904,7 @@ public class BezierEdge implements Serializable { 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 + (Point2D.Double) q2.getBeginPoint(), (Point2D.Double) q2.getControlPoint(), (Point2D.Double) q2.getEndPoint(), 1 ); } @@ -919,4 +919,14 @@ public class BezierEdge implements Serializable { ); } } + + public double closestTForPoint(Point2D point) { + return new BezierUtils().closestPointToBezier(point, getBeginPoint(), getControlPoint(), getEndPoint()); + } + + public double distanceFromPoint(Point2D point) { + double t = closestTForPoint(point); + Point2D p = pointAt(t); + return p.distance(point); + } } 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 index aec6e689f..60809af36 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/math/QuadOverlap.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/math/QuadOverlap.java @@ -26,7 +26,7 @@ public final class QuadOverlap { // 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 + final double angleTol = Math.toRadians(1);//0.01; // radians; ~0.057° — treat as parallel if below ParamPoly polyA = flattenQuad(A0, A1, A2, flatTol); ParamPoly polyB = flattenQuad(B0, B1, B2, flatTol); 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 index a5fd6af92..f940e6f9c 100644 --- 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 @@ -3,12 +3,15 @@ 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 com.jpexs.decompiler.flash.types.FILLSTYLEARRAY; +import com.jpexs.decompiler.flash.types.LINESTYLEARRAY; 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; @@ -22,8 +25,10 @@ import java.util.stream.Collectors; * @author JPEXS */ public class OverlappingEdgesSplitter { + private static class BezierEdgeWrapper { + int id; BezierEdge be; int layer; int pathIndex; @@ -89,20 +94,20 @@ public class OverlappingEdgesSplitter { } static class BezierEdgePair { + BezierEdgeWrapper be1; - BezierEdgeWrapper be2; + 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<>(); + List overlapPairs = new ArrayList<>(); //Map> overlapIntervals = new LinkedHashMap<>(); final java.util.Comparator statusCmp = (e1, e2) -> { int cMinY = Double.compare(e1.minY(), e2.minY()); @@ -119,8 +124,8 @@ public class OverlappingEdgesSplitter { 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)); + pq.add(new Event(Event.Type.START, e.minX() - 10, e)); + pq.add(new Event(Event.Type.END, e.maxX() + 10, e)); } void run() { @@ -165,19 +170,18 @@ public class OverlappingEdgesSplitter { 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); - + List overlapIntervals = new ArrayList<>(); + overlapIntervals = e1.be.overlap(e2.be); + if (!hasIntersections && overlapIntervals.isEmpty()) { return; } - - if (!splitPoints.containsKey(e1)) { splitPoints.put(e1, new ArrayList<>()); @@ -185,89 +189,43 @@ public class OverlappingEdgesSplitter { 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) { + for (OverlapInterval interval : overlapIntervals) { if (interval.t0 == interval.t1) { - continue; + 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); - } - + + overlapPairs.add(new OverlapPair(e1.id, e2.id, interval.t0, interval.t1, interval.u0, interval.u1)); 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> outs) { - private void handleBewList(List bewList, List layers) { Map> bewMap = bewList.stream() .collect(Collectors.groupingBy(b -> b.layer)); for (Map.Entry> entry : bewMap.entrySet()) { + int id = 0; + for (BezierEdgeWrapper bew : entry.getValue()) { + bew.id = id++; + } + + int layer = entry.getKey(); + Map> splittedEdges = new HashMap<>(); + Set bewsToIgnore = new LinkedHashSet<>(); Map existingEdges = new HashMap<>(); @@ -290,60 +248,22 @@ public class OverlappingEdgesSplitter { 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<>(); + List overlapPairs = new ArrayList<>(); - 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); - } - } - } + Sweep sweep = new Sweep(); + for (BezierEdgeWrapper bew : entry.getValue()) { + if (bewsToIgnore.contains(bew)) { + continue; } + sweep.addEdge(bew); } + sweep.run(); + splitPointsMap = sweep.splitPoints; + overlapPairs = sweep.overlapPairs; List splittedBewList = new ArrayList<>(splitPointsMap.keySet()); + List intersectionsList = new ArrayList<>(); splittedBewList.sort((BezierEdgeWrapper o1, BezierEdgeWrapper o2) -> { int dShapeIndex = o1.pathIndex - o2.pathIndex; @@ -357,70 +277,498 @@ public class OverlappingEdgesSplitter { return System.identityHashCode(o1) - System.identityHashCode(o2); }); + Set splittedIds = new HashSet<>(); + 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))); + splittedIds.add(bew.id); + + if (bew.be.getBeginPoint().getX() == 2755.0) { + System.err.println("xxx"); } - splits.sort((a, b) -> Double.compare(a.t, b.t)); + final double epsT = 1e-9; // param eps + List splitT = splitPointsMap.get(bew); + + splitT.sort((a, b) -> Double.compare(a, b)); + BezierEdge be = bew.be; List realSplitT = new ArrayList<>(); - for (TPoint tp : splits) { - if (tp.t == 0.0 || tp.t == 1.0) { + for (Double t : splitT) { + if (t < epsT || t >= 1.0 - epsT) { continue; } - realSplitT.add(tp.t); + realSplitT.add(t); } if (realSplitT.isEmpty()) { + splittedIds.remove(bew.id); 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 (splitT.get(0) != 0.0) { + splitT.add(0, 0.0); } - if (splits.get(splits.size() - 1).t != 1.0) { - splits.add(new TPoint(1.0, bew.be.getEndPoint(), null)); + if (splitT.get(splitT.size() - 1) != 1.0) { + splitT.add(1.0); } + + List uniq = new ArrayList<>(); + double prev = Double.NaN; - 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); + for (Double t : splitT) { + if (uniq.isEmpty() || Math.abs(t - prev) > epsT) { + uniq.add(Math.max(0, Math.min(1, t))); + prev = t; + Point2D pat = bew.be.pointAt(t); + intersectionsList.add(new Intersection(bew.id, t, pat.getX(), pat.getY())); } - 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++; + + List frs = new ArrayList<>(); + for (int j = 0; j + 1 < uniq.size(); j++) { + double t0 = uniq.get(j); + double t1 = uniq.get(j + 1); + if (t1 - t0 <= epsT) { + continue; // skip zero-length + } + frs.add(new Fragment(new Edge(bew.id, bew.be, bew.pathIndex), t0, t1, false, bew.pathIndex)); + } + splittedEdges.put(bew.id, frs); + } + + Map> comps = buildOverlapComponents(splittedEdges, overlapPairs); + + List outSplitted = normalizeOverlaps(splittedEdges, comps); + + remapIntersections(intersectionsList, splittedEdges, comps, outSplitted, 1); + + List allOut = new ArrayList<>(); + System.err.println("NON Splitted edges:"); + for (BezierEdgeWrapper bew : entry.getValue()) { + if (!splittedIds.contains(bew.id)) { + allOut.add(new OutputEdge(new Edge(bew.id, bew.be, bew.pathIndex), bew.pathIndex)); + System.err.println(bew.be.toSvg()); + } + } + + System.err.println("Splitted edges:"); + for (OutputEdge ie : outSplitted) { + System.err.println("" + ie.geom.be.toSvg()); + } + + allOut.addAll(outSplitted); + + snapEndpoints(allOut, 2); + + outs.put(layer, allOut); + } + } + + public static final class Intersection { + + public final int edgeId; + public final double t; // original param on that edge + public final double x, y; + + public Intersection(int e, double t, double x, double y) { + this.edgeId = e; + this.t = t; + this.x = x; + this.y = y; + } + } + + public static final class IntersectionOut { + + public final OutputEdge edge; // where it ended up + public final double u; // local param in [0,1] on edge.geom + public final double x, y; // (optional) exact point + + public IntersectionOut(OutputEdge e, double u, double x, double y) { + this.edge = e; + this.u = u; + this.x = x; + this.y = y; + } + } + + public static List remapIntersections( + List oldInts, + Map> fragsPerEdge, + Map> comps, + List outputs, + double tol) { + + // Build fast lookup: for each original fragment tell which OutputEdge instance represents it (by fillStyle). + // Multiple participants in a component create multiple OutputEdges with same geometry; we pick by matching fillStyle. + Map rep = new HashMap<>(); + + // Mark components first + Set compMembers = new HashSet<>(); + for (Map.Entry> e : comps.entrySet()) { + List refs = e.getValue(); + // canonical geometry is from min ref: + FragRef canRef = refs.stream() + .min((a, b) -> { + int c = Integer.compare(a.edgeId, b.edgeId); + return c != 0 ? c : Integer.compare(a.idxInEdge, b.idxInEdge); + }).get(); + Fragment canFrag = fragsPerEdge.get(canRef.edgeId).get(canRef.idxInEdge); + Edge canonical = canFrag.toEdge(); + + // find all OutputEdges built from this component: + List outs = new ArrayList<>(); + for (OutputEdge oe : outputs) { + // Heuristic: same object reference? Not guaranteed. Use geometry equality within tol. + if (sameGeometry(oe.geom, canonical, tol)) { + outs.add(oe); + } + } + // Map each member (edgeId:idx) to corresponding OutputEdge by fill style + for (FragRef r : refs) { + Fragment f = fragsPerEdge.get(r.edgeId).get(r.idxInEdge); + compMembers.add(r.edgeId + ":" + r.idxInEdge); + OutputEdge match = outs.stream() + .filter(oe -> oe.pathIndex == f.pathIndex) + .findFirst().orElse(null); + if (match != null) { + rep.put(r.edgeId + ":" + r.idxInEdge, match); } } } + + // For non-overlap fragments, map to their own OutputEdge (geometry equals fragment) + for (Map.Entry> e : fragsPerEdge.entrySet()) { + int edgeId = e.getKey(); + List L = e.getValue(); + for (int i = 0; i < L.size(); i++) { + String key = edgeId + ":" + i; + if (compMembers.contains(key)) { + continue; + } + Fragment f = L.get(i); + // Find OutputEdge with same geometry & fill + OutputEdge match = outputs.stream() + .filter(oe -> oe.pathIndex == f.pathIndex && sameGeometry(oe.geom, f.toEdge(), tol)) + .findFirst().orElse(null); + if (match != null) { + rep.put(key, match); + } + } + } + + // Now convert each original intersection + List result = new ArrayList<>(); + for (Intersection in : oldInts) { + List frs = fragsPerEdge.get(in.edgeId); + if (frs == null) { + continue; + } + // locate fragment [t0,t1] containing t + Fragment f = locateFragment(frs, in.t); + if (f == null) { + continue; + } + OutputEdge dst = rep.get(in.edgeId + ":" + frs.indexOf(f)); + if (dst == null) { + continue; + } + // convert original t to local u on fragment + double uLocal = (in.t - f.t0) / (f.t1 - f.t0); + if (f.reversed) { + uLocal = 1.0 - uLocal; + } + + result.add(new IntersectionOut(dst, uLocal, in.x, in.y)); + } + + // optional: deduplicate by (dst, rounded point) + return result; } + static Fragment locateFragment(List fs, double t) { + // Binary search over sorted fragments by t0/t1 + int lo = 0, hi = fs.size() - 1; + final double eps = 1e-12; + while (lo <= hi) { + int mid = (lo + hi) / 2; + Fragment f = fs.get(mid); + if (t < f.t0 - eps) { + hi = mid - 1; + } else if (t > f.t1 + eps) { + lo = mid + 1; + } else { + return f; + } + } + return null; + } + + static boolean sameGeometry(Edge a, Edge b, double tol) { + // As with geometricallyCoincident but cheaper thresholds. + return geometricallyCoincident( + new Fragment(a, 0, 1, false, a.pathIndex), + new Fragment(b, 0, 1, false, b.pathIndex), + tol); + } + + public static List normalizeOverlaps( + Map> fragsPerEdge, + Map> comps) { + + List out = new ArrayList<>(); + // Keep a mark for fragments which belong to any overlap component + Set inComp = new HashSet<>(); + for (List refs : comps.values()) { + // pick canonical: smallest (edgeId, idx) + FragRef canRef = refs.stream() + .min((a, b) -> { + int c = Integer.compare(a.edgeId, b.edgeId); + return c != 0 ? c : Integer.compare(a.idxInEdge, b.idxInEdge); + }).get(); + Fragment canFrag = fragsPerEdge.get(canRef.edgeId).get(canRef.idxInEdge); + Edge canonicalGeom = canFrag.toEdge(); + + for (FragRef r : refs) { + Fragment f = fragsPerEdge.get(r.edgeId).get(r.idxInEdge); + inComp.add(r.edgeId + ":" + r.idxInEdge); + + // Align orientation (optional – if you require identical direction): + Edge g = canonicalGeom; + // If you must ensure f's direction equals canonical, just use canonical as-is. + // If you want "two same segments", the geometry object can literally be same. + + out.add(new OutputEdge(g, f.pathIndex)); + } + } + + // Non-overlapped fragments go through unchanged + for (Map.Entry> e : fragsPerEdge.entrySet()) { + List fs = e.getValue(); + for (int i = 0; i < fs.size(); i++) { + String key = e.getKey() + ":" + i; + if (inComp.contains(key)) { + continue; // already replaced + } + Fragment f = fs.get(i); + out.add(new OutputEdge(f.toEdge(), f.pathIndex)); + } + } + return out; + } + + public static class OutputEdge { + + public final Edge geom; // canonical geometry + public final int pathIndex; // one per participant (you will have N of these) + + public OutputEdge(Edge g, int f) { + this.geom = g; + this.pathIndex = f; + } + } + + private static Map> buildOverlapComponents( + Map> fragsPerEdge, + List overlaps) { + + // Index all fragments by a running id for DSU + List allRefs = new ArrayList<>(); + Map baseIndex = new HashMap<>(); // edgeId -> start index + int cnt = 0; + for (Map.Entry> e : fragsPerEdge.entrySet()) { + baseIndex.put(e.getKey(), cnt); + for (int i = 0; i < e.getValue().size(); i++) { + allRefs.add(new FragRef(e.getKey(), i)); + } + cnt += e.getValue().size(); + } + + DSU dsu = new DSU(cnt); + + // For each reported overlap, link fragments whose [t0,t1] lies inside. + for (OverlapPair op : overlaps) { + List A = fragsPerEdge.get(op.edgeA); + List B = fragsPerEdge.get(op.edgeB); + if (A == null || B == null) { + continue; + } + + double aMin = Math.min(op.a0, op.a1); + double aMax = Math.max(op.a0, op.a1); + double bMin = Math.min(op.b0, op.b1); + double bMax = Math.max(op.b0, op.b1); + + final double epsT = 1e-9; + + for (int i = 0; i < A.size(); i++) { + Fragment fa = A.get(i); + if (fa.t0 + epsT >= aMax || fa.t1 - epsT <= aMin) { + continue; // no overlap in param + } + for (int j = 0; j < B.size(); j++) { + Fragment fb = B.get(j); + if (fb.t0 + epsT >= bMax || fb.t1 - epsT <= bMin) { + continue; + } + + // Optional: geometric check with tolerance to avoid false positives: + if (!geometricallyCoincident(fa, fb, /*tol=*/ 10)) { + continue; + } + + int ia = baseIndex.get(op.edgeA) + i; + int ib = baseIndex.get(op.edgeB) + j; + dsu.u(ia, ib); + } + } + } + + // Gather components + Map> comps = new HashMap<>(); + for (int k = 0; k < allRefs.size(); k++) { + int root = dsu.f(k); + comps.computeIfAbsent(root, key -> new ArrayList<>()).add(allRefs.get(k)); + } + return comps; + } + + // Geometric equality test within tolerance using sampling + static boolean geometricallyCoincident(Fragment f1, Fragment f2, double tol) { + // If line-line: check collinearity + projection overlap more strictly. + // For general case: symmetric sample Hausdorff approx. + final int S = 24; + double maxd = 0; + for (int i = 0; i <= S; i++) { + double u = (double) i / S; + Point2D p = f1.toEdge().be.pointAt(u); + double d = distancePointToEdge(p, f2.toEdge()); + maxd = Math.max(maxd, d); + if (maxd > tol) { + return false; + } + } + for (int i = 0; i <= S; i++) { + double u = (double) i / S; + Point2D p = f2.toEdge().be.pointAt(u); + double d = distancePointToEdge(p, f1.toEdge()); + maxd = Math.max(maxd, d); + if (maxd > tol) { + return false; + } + } + return true; + } + +// Distance from point to edge (line or quadratic), via projection / sampling. + static double distancePointToEdge(Point2D p, Edge e) { + return e.be.distanceFromPoint(p); + } + + public static void snapEndpoints(List edges, double snapTol) { + // Collect all endpoints + List pts = new ArrayList<>(); + for (OutputEdge e : edges) { + Point2D a = e.geom.be.getBeginPoint(); + Point2D c = e.geom.be.getControlPoint(); + Point2D b = e.geom.be.getEndPoint(); + pts.add(a); + pts.add(c); + pts.add(b); + } + + // Build clusters of points within snapTol (simple O(n^2) is often OK; use grid hash if needed) + int n = pts.size(); + int[] parent = new int[n]; + for (int i = 0; i < n; i++) { + parent[i] = i; + } + java.util.function.IntUnaryOperator find = new java.util.function.IntUnaryOperator() { + @Override + public int applyAsInt(int x) { + return parent[x] == x ? x : (parent[x] = applyAsInt(parent[x])); + } + }; + java.util.function.BiConsumer unite = (ii, jj) -> { + int i = find.applyAsInt(ii), j = find.applyAsInt(jj); + if (i != j) { + parent[j] = i; + } + }; + + double tol2 = snapTol * snapTol; + for (int i = 0; i < n; i++) { + Point2D pi = pts.get(i); + for (int j = i + 1; j < n; j++) { + Point2D pj = pts.get(j); + double dx = pi.getX() - pj.getX(); + double dy = pi.getY() - pj.getY(); + if (dx * dx + dy * dy <= tol2) { + unite.accept(i, j); + } + } + } + + // Compute cluster representatives as arithmetic mean for stability + Map rep = new HashMap<>(); + Map cnt = new HashMap<>(); + for (int i = 0; i < n; i++) { + int r = find.applyAsInt(i); + Point2D p = pts.get(i); + rep.compute(r, (k, v) -> { + if (v == null) { + return new Point2D.Double(p.getX(), p.getY()); + } + v.setLocation(v.getX() + p.getX(), v.getY() + p.getY()); + return v; + }); + cnt.merge(r, 1, Integer::sum); + } + for (Map.Entry e : rep.entrySet()) { + int c = cnt.get(e.getKey()); + e.getValue().setLocation(e.getValue().getX() / c, e.getValue().getY() / c);; + } + + // Write snapped endpoints back into edges + int idx = 0; + for (OutputEdge e : edges) { + int rA = find.applyAsInt(idx++); + int rC = find.applyAsInt(idx++); + int rB = find.applyAsInt(idx++); + Point2D A = rep.get(rA); + Point2D C = rep.get(rC); + Point2D B = rep.get(rB); + // Rebuild edge with snapped endpoints (keep control point for quads via De Casteljau split at 0/1) + if (!e.geom.be.isQuad()) { + // create new LineEdge with same fill/id but snapped ends + e.geom.be.setBeginPoint(A); + e.geom.be.setEndPoint(B); + } else { + double dx0 = A.getX() - e.geom.be.getBeginPoint().getX(); + double dy0 = A.getY() - e.geom.be.getBeginPoint().getY(); + double dx1 = B.getX() - e.geom.be.getEndPoint().getX(); + double dy1 = B.getY() - e.geom.be.getEndPoint().getY(); + + e.geom.be.setBeginPoint(A); + /*e.geom.be.setControlPoint(new Point2D.Double( + e.geom.be.getControlPoint().getX() + 0.5 * (dx0 + dx1), + e.geom.be.getControlPoint().getY() + 0.5 * (dy0 + dy1) + ));*/ + e.geom.be.setControlPoint(C); + e.geom.be.setEndPoint(B); + } + } + } + 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); @@ -435,11 +783,43 @@ public class OverlappingEdgesSplitter { } } } - - handleBewList(strokesBewList, layers); - handleBewList(fillsBewList, layers); - for (int layer = 0; layer < layers.size(); layer++) { + Map> outStrokes = new HashMap<>(); + Map> outFills = new HashMap<>(); + + handleBewList(strokesBewList, layers, outStrokes); + handleBewList(fillsBewList, layers, outFills); + + Map> outAll = outFills; + for (int layer : outStrokes.keySet()) { + if (!outAll.containsKey(layer)) { + outAll.put(layer, new ArrayList<>()); + } + outAll.get(layer).addAll(outStrokes.get(layer)); + } + + for (int i = 0; i < layers.size(); i++) { + Layer layer = layers.get(i); + Map> pathIndexToEdge = outAll.get(i).stream() + .collect(Collectors.groupingBy(b -> b.pathIndex)); + for (int p = 0; p < layer.paths.size(); p++) { + Path path = layer.paths.get(p); + path.edges.clear(); + if (pathIndexToEdge.containsKey(p)) { + for (OutputEdge oe : pathIndexToEdge.get(p)) { + path.edges.add(oe.geom.be); + } + } + } + for (int p = 0; p < layer.paths.size(); p++) { + if (layer.paths.get(p).edges.isEmpty()) { + layer.paths.remove(p); + p--; + } + } + } + + /*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++) { @@ -447,6 +827,156 @@ public class OverlappingEdgesSplitter { be1.shrinkToLine(); } } + }*/ + } + + private static class Edge { + + int id; + BezierEdge be; + int pathIndex; + + public Edge(int id, BezierEdge be, int fillStyle) { + this.id = id; + this.be = be; + this.pathIndex = fillStyle; + } + + } + + private static class Fragment { + + Edge source; + double t0; + double t1; + boolean reversed; + int pathIndex; + + public Fragment(Edge source, double t0, double t1, boolean reversed, int fillStyle) { + this.source = source; + this.t0 = t0; + this.t1 = t1; + this.reversed = reversed; + this.pathIndex = fillStyle; + } + + public Edge toEdge() { + return new Edge(source.id, toBezierEdge(), pathIndex); + } + + public BezierEdge toBezierEdge() { + if (t0 == 0.0) { + if (t1 == 1.0) { + return source.be; + } + return source.be.split(Arrays.asList(t1)).get(0); + } + if (t1 == 1.0) { + return source.be.split(Arrays.asList(t0)).get(1); + } + + return source.be.split(Arrays.asList(t0, t1)).get(1); + } + + } + + private static class OverlapPair { + + int edgeA; + int edgeB; + double a0; + double a1; + double b0; + double b1; // can be a0>a1 or b0>b1 if opposite direction + + public OverlapPair(int edgeA, int edgeB, double a0, double a1, double b0, double b1) { + this.edgeA = edgeA; + this.edgeB = edgeB; + this.a0 = a0; + this.a1 = a1; + this.b0 = b0; + this.b1 = b1; + } + } + + // Tiny union-find + static final class DSU { + + int[] p; + int[] r; + + DSU(int n) { + p = new int[n]; + r = new int[n]; + for (int i = 0; i < n; i++) { + p[i] = i; + } + } + + int f(int x) { + return p[x] == x ? x : (p[x] = f(p[x])); + } + + void u(int a, int b) { + a = f(a); + b = f(b); + if (a == b) { + return; + } + if (r[a] < r[b]) { + int t = a; + a = b; + b = t; + } + p[b] = a; + if (r[a] == r[b]) { + r[a]++; + } + } + } + + static final class FragRef { + + final int edgeId; + final int idxInEdge; // index in that edge's fragment list + + FragRef(int e, int i) { + edgeId = e; + idxInEdge = i; + } + } + + public static void main(String[] args) { + OverlappingEdgesSplitter sp = new OverlappingEdgesSplitter(); + List layers = new ArrayList<>(); + Layer lay = new Layer(); + lay.fillStyleArray = new FILLSTYLEARRAY(); + lay.lineStyleArray = new LINESTYLEARRAY(); + lay.paths = new ArrayList<>(); + Path p = new Path(); + + //M 105 1238 Q -84 1185 -273 1133 + //M -456 1085L -452 1087Q -229 1144 -49 1195 + + p.edges.add(new BezierEdge(105,1238 , -84 ,1185 ,-273, 1133)); + + p.edges.add(new BezierEdge(-456, 1085, -452, 1087)); + p.edges.add(new BezierEdge(-452,1087, -229, 1144, -49, 1195)); + lay.paths.add(p); + layers.add(lay); + + System.err.println("BEFORE:"); + for (Path path : layers.get(0).paths) { + System.err.println("" + path.toString()); + } + System.err.println("-----------------"); + sp.splitOverlappingEdges(layers); + + + System.err.println("AFTER:"); + System.err.println("----------------"); + for (Path path : layers.get(0).paths) { + System.err.println("" + path.toString()); } } } 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 index 63056be6a..56515a9e4 100644 --- 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 @@ -226,16 +226,18 @@ public class ShapeFixer2 { removeEmpty(layers);*/ OverlappingEdgesSplitter splitter = new OverlappingEdgesSplitter(); - splitter.splitOverlappingEdges(layers); + splitter.splitOverlappingEdges(layers); - for (Layer layer : layers) { - detectEdgeFills(layer); - } + for (Layer layer : layers) { layer.round(wasSmall); } + for (Layer layer : layers) { + detectEdgeFills(layer); + } + removeEmpty(layers); /*for (Layer layer : layers) { @@ -278,7 +280,7 @@ public class ShapeFixer2 { } } - private void detectEdgeFills(Layer layer) { + public void detectEdgeFills(Layer layer) { double epsBase = 1e-4;