diff --git a/src/com/jpexs/decompiler/flash/gui/BezierUtils.java b/src/com/jpexs/decompiler/flash/gui/BezierUtils.java new file mode 100644 index 000000000..8cd6a0a0a --- /dev/null +++ b/src/com/jpexs/decompiler/flash/gui/BezierUtils.java @@ -0,0 +1,346 @@ +/* + * Copyright (C) 2022 JPEXS + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package com.jpexs.decompiler.flash.gui; + +import java.awt.geom.Point2D; +import java.util.ArrayList; +import java.util.List; + +/** + * + * Bezier utils. + * Based on + * https://code.google.com/archive/p/degrafa/source/default/source + * which is derived from an algorithm in the book "Graphics Gems" + * + * This modification does not have cubic curves support. + */ +public class BezierUtils { + + private static final int MAX_DEPTH = 64; // maximum recursion depth + private static final double[] Z_QUAD = new double[]{1.0, 2.0 / 3.0, 1.0 / 3.0, 1 / 3.0, 2.0 / 3.0, 1.0}; + private static final double EPSILON = 1.0 * Math.pow(2, -MAX_DEPTH - 1); // flatness tolerance + + public Point2D pointAt(double t, Point2D p0, Point2D p1, Point2D p2) { + double xt = (1 - t) * (1 - t) * p0.getX() + 2 * (1 - t) * t * p1.getX() + t * t * p2.getX(); + double yt = (1 - t) * (1 - t) * p0.getY() + 2 * (1 - t) * t * p1.getY() + t * t * p2.getY(); + return new Point2D.Double(xt, yt); + } + + public double closestPointToBezier(Point2D _p, Point2D p0, Point2D p1, Point2D p2) { + Point2D p = p0; + double deltaX = p.getX() - _p.getX(); + double deltaY = p.getY() - _p.getY(); + double d0 = Math.sqrt(deltaX * deltaX + deltaY * deltaY); + + p = p2; + deltaX = p.getX() - _p.getX(); + deltaY = p.getY() - _p.getY(); + double d1 = Math.sqrt(deltaX * deltaX + deltaY * deltaY); + + int n = 2; // degree of input Bezier curve + + // array of control points + List v = new ArrayList<>(); + v.add(p0); + v.add(p1); + v.add(p2); + + // instead of power form, convert the function whose zeros are required to Bezier form + List w = toBezierForm(_p, v); + + // Find roots of the Bezier curve with control points stored in 'w' (algorithm is recursive, this is root depth of 0) + List roots = findRoots(w, 2 * n - 1, 0); + + // compare the candidate distances to the endpoints and declare a winner :) + double tMinimum; + double dMinimum; + if (d0 < d1) { + tMinimum = 0; + dMinimum = d0; + } else { + tMinimum = 1; + dMinimum = d1; + } + + // tbd - compare 2-norm squared + for (int i = 0; i < roots.size(); i++) { + double t = roots.get(i); + if (t >= 0 && t <= 1) { + p = pointAt(t, p0, p1, p2); + deltaX = p.getX() - _p.getX(); + deltaY = p.getY() - _p.getY(); + double d = Math.sqrt(deltaX * deltaX + deltaY * deltaY); + + if (d < dMinimum) { + tMinimum = t; + dMinimum = d; + } + } + } + + // tbd - alternate optima. + return tMinimum; + + } + + private List toBezierForm(Point2D _p, List _v) { + List c = new ArrayList<>(); // V(i) - P + List d = new ArrayList<>(); // V(i+1) - V(i) + List w = new ArrayList<>(); // control-points for Bezier curve whose zeros represent candidates for closest point to the input parametric curve + int n = _v.size() - 1; //degree of B(t) + int degree = 2 * n - 1; // degree of B(t) . P + double pX = _p.getX(); + double pY = _p.getY(); + + Point2D v; + for (int i = 0; i <= n; i++) { + v = _v.get(i); + c.add(new Point2D.Double(v.getX() - pX, v.getY() - pY)); + } + + double s = n; + for (int i = 0; i <= n - 1; i++) { + v = _v.get(i); + Point2D v1 = _v.get(i + 1); + d.add(new Point2D.Double(s * (v1.getX() - v.getX()), s * (v1.getY() - v.getY()))); + } + + List cd = new ArrayList<>(); + for (int row = 0; row <= n - 1; row++) { + Point2D di = d.get(row); + double dX = di.getX(); + double dY = di.getY(); + for (int col = 0; col <= n; col++) { + int k = getLinearIndex(n + 1, row, col); + cd.add(dX * c.get(col).getX() + dY * c.get(col).getY()); + } + } + + // Bezier is uniform parameterized + double dInv = 1.0 / degree; + for (int i = 0; i <= degree; i++) { + w.add(new Point2D.Double(i * dInv, 0)); + } + + // reference to appropriate pre-computed coefficients + double[] z = Z_QUAD; + + // accumulate y-coords of the control points along the skew diagonal of the (n-1) x n matrix of c.d and z values + int m = n - 1; + for (int k = 0; k <= n + m; k++) { + int lb = Math.max(0, k - m); + int ub = Math.min(k, n); + for (int i = lb; i <= ub; i++) { + int j = k - i; + Point2D p = w.get(i + j); + int index = getLinearIndex(n + 1, j, i); + p.setLocation(p.getX(), p.getY() + cd.get(index) * z[index]); + w.set(i + j, p); + } + } + return w; + } + + // how many times does the Bezier curve cross the horizontal axis - the number of roots is less than or equal to this count + private int crossingCount(List _v, int _degree) { + int nCrossings = 0; + int sign = _v.get(0).getY() < 0 ? -1 : 1; + int oldSign = sign; + for (int i = 1; i <= _degree; i++) { + sign = _v.get(i).getY() < 0 ? -1 : 1; + if (sign != oldSign) { + nCrossings++; + } + + oldSign = sign; + } + + return nCrossings; + } + + // convert 2D array indices in a k x n matrix to a linear index (this is an interim step ahead of a future implementation optimized for 1D array indexing) + private int getLinearIndex(int _n, int _row, int _col) { + // no range-checking; you break it ... you buy it! + return _row * _n + _col; + } + + /** + * subdivide( _c:Array, _t:Number, _left:Array, _right:Array ) - deCasteljau + * subdivision of an arbitrary-order Bezier curve + * + * @param _c:Array array of control points for the Bezier curve + * @param _t:Number t-parameter at which the curve is subdivided (must be in + * (0,1) = no check at this point + * @param _left:Array reference to an array in which the control points, + * Array of Point references, of the left control + * cage after subdivision are stored + * @param _right:Array reference to an array in which the control points, + * Array of Point references, of the right control + * cage after subdivision are stored + * + * @since 1.0 + * + */ + public void subdivide(List _c, double _t, List _left, List _right) { + int degree = _c.size() - 1; + int n = degree + 1; + List p = new ArrayList<>(_c); + double t1 = 1.0 - _t; + + for (int i = 1; i <= degree; ++i) { + for (int j = 0; j <= degree - i; ++j) { + Point2D vertex = new Point2D.Double(); + int ij = getLinearIndex(n, i, j); + int im1j = getLinearIndex(n, i - 1, j); + int im1jp1 = getLinearIndex(n, i - 1, j + 1); + + vertex.setLocation(t1 * p.get(im1j).getX() + _t * p.get(im1jp1).getX(), t1 * p.get(im1j).getY() + _t * p.get(im1jp1).getY()); + while (ij >= p.size()) { + p.add(new Point2D.Double(0, 0)); + } + p.set(ij, vertex); + } + } + + for (int j = 0; j <= degree; j++) { + int index = getLinearIndex(n, j, 0); + _left.add(p.get(index)); + } + + for (int j = 0; j <= degree; j++) { + int index = getLinearIndex(n, degree - j, j); + _right.add(p.get(index)); + } + } + + // is the control polygon for a Bezier curve suitably linear for subdivision to terminate? + private boolean isControlPolygonLinear(List _v, int _degree) { + // Given array of control points, _v, find the distance from each interior control point to line connecting v[0] and v[degree] + + // implicit equation for line connecting first and last control points + double a = _v.get(0).getY() - _v.get(_degree).getY(); + double b = _v.get(_degree).getX() - _v.get(0).getX(); + double c = _v.get(0).getX() * _v.get(_degree).getY() - _v.get(_degree).getX() * _v.get(0).getY(); + + double abSquared = a * a + b * b; + List distance = new ArrayList<>(); // Distances from control points to line + + distance.add(0.0); + for (int i = 1; i < _degree; i++) { + // Compute distance from each of the points to that line + distance.add(a * _v.get(i).getX() + b * _v.get(i).getY() + c); + if (distance.get(i) > 0.0) { + distance.set(i, (distance.get(i) * distance.get(i)) / abSquared); + } + if (distance.get(i) < 0.0) { + distance.set(i, -((distance.get(i) * distance.get(i)) / abSquared)); + } + } + + // Find the largest distance + double maxDistanceAbove = 0.0; + double maxDistanceBelow = 0.0; + for (int i = 1; i < _degree; i++) { + if (distance.get(i) < 0.0) { + maxDistanceBelow = Math.min(maxDistanceBelow, distance.get(i)); + } + if (distance.get(i) > 0.0) { + maxDistanceAbove = Math.max(maxDistanceAbove, distance.get(i)); + } + } + + // Implicit equation for zero line + double a1 = 0.0; + double b1 = 1.0; + double c1 = 0.0; + + // Implicit equation for "above" line + double a2 = a; + double b2 = b; + double c2 = c + maxDistanceAbove; + + double det = a1 * b2 - a2 * b1; + double dInv = 1.0 / det; + + double intercept1 = (b1 * c2 - b2 * c1) * dInv; + + // Implicit equation for "below" line + a2 = a; + b2 = b; + c2 = c + maxDistanceBelow; + + double intercept2 = (b1 * c2 - b2 * c1) * dInv; + + // Compute intercepts of bounding box + double leftIntercept = Math.min(intercept1, intercept2); + double rightIntercept = Math.max(intercept1, intercept2); + + double error = 0.5 * (rightIntercept - leftIntercept); + + return error < EPSILON; + } + + // return roots in [0,1] of a polynomial in Bernstein-Bezier form + private List findRoots(List _w, int _degree, int _depth) { + List t = new ArrayList<>(); // t-values of roots + int m = 2 * _degree - 1; + + switch (crossingCount(_w, _degree)) { + case 0: + return new ArrayList<>(); + case 1: + // Unique solution - stop recursion when the tree is deep enough (return 1 solution at midpoint) + if (_depth >= MAX_DEPTH) { + t.add(0.5 * (_w.get(0).getX() + _w.get(m).getX())); + return t; + } + + if (isControlPolygonLinear(_w, _degree)) { + t.add(computeXIntercept(_w, _degree)); + return t; + } + break; + } + + // Otherwise, solve recursively after subdividing control polygon + List left = new ArrayList<>(); + List right = new ArrayList<>(); + + // child solutions + subdivide(_w, 0.5, left, right); + List leftT = findRoots(left, _degree, _depth + 1); + List rightT = findRoots(right, _degree, _depth + 1); + + t.addAll(leftT); + t.addAll(rightT); + return t; + } + + // compute intersection of line segnet from first to last control point with horizontal axis + private double computeXIntercept(List _v, int _degree) { + double XNM = _v.get(_degree).getX() - _v.get(0).getX(); + double YNM = _v.get(_degree).getY() - _v.get(0).getY(); + double XMK = _v.get(0).getX(); + double YMK = _v.get(0).getY(); + + double detInv = -1.0 / YNM; + + return (XNM * YMK - YNM * XMK) * detInv; + } + +} diff --git a/src/com/jpexs/decompiler/flash/gui/DisplayPoint.java b/src/com/jpexs/decompiler/flash/gui/DisplayPoint.java index fb928ff08..562b6d291 100644 --- a/src/com/jpexs/decompiler/flash/gui/DisplayPoint.java +++ b/src/com/jpexs/decompiler/flash/gui/DisplayPoint.java @@ -16,6 +16,8 @@ */ package com.jpexs.decompiler.flash.gui; +import java.awt.geom.Point2D; + /** * * @author JPEXS @@ -25,6 +27,14 @@ public class DisplayPoint { public int y; public boolean onPath; + public DisplayPoint(Point2D point) { + this(point, true); + } + public DisplayPoint(Point2D point, boolean onPath) { + x = (int)Math.round(point.getX()); + y = (int)Math.round(point.getY()); + this.onPath = onPath; + } public DisplayPoint(DisplayPoint src) { this(src.x, src.y, src.onPath); } @@ -43,4 +53,7 @@ public class DisplayPoint { return "["+x+","+y+"]"; } + public Point2D toPoint2D(){ + return new Point2D.Double(x, y); + } } diff --git a/src/com/jpexs/decompiler/flash/gui/ImagePanel.java b/src/com/jpexs/decompiler/flash/gui/ImagePanel.java index 3eb5b4589..5984d10fb 100644 --- a/src/com/jpexs/decompiler/flash/gui/ImagePanel.java +++ b/src/com/jpexs/decompiler/flash/gui/ImagePanel.java @@ -246,6 +246,7 @@ public final class ImagePanel extends JPanel implements MediaDisplay { private static Cursor shearYCursor; private static Cursor movePointCursor; private static Cursor defaultCursor; + private static Cursor addPointCursor; private Point2D offsetPoint = new Point2D.Double(0, 0); @@ -273,9 +274,15 @@ public final class ImagePanel extends JPanel implements MediaDisplay { private List hilightedPoints = null; + //private DisplayPoint closestPoint = null; private List pointsUnderCursor = new ArrayList<>(); private List selectedPoints = new ArrayList<>(); + private Integer pathPointUnderCursor = null; + private Double pathPointPosition = null; + + private DisplayPoint closestPoint = null; + private List selectedPointsOriginalValues = new ArrayList<>(); private int hilightEdgeColorStep = 10; @@ -321,10 +328,10 @@ public final class ImagePanel extends JPanel implements MediaDisplay { } } - private boolean firePointAdded(List points, int position, DisplayPoint point) { + private boolean fireEdgeSplit(List points, int position, double splitPoint) { boolean result = true; for (PointUpdateListener listener : pointUpdateListeners) { - result = result && listener.pointAdded(points, position, point); + result = result && listener.edgeSplit(points, position, splitPoint); } return result; } @@ -339,9 +346,9 @@ public final class ImagePanel extends JPanel implements MediaDisplay { private void applyPointsXY() { try { - int x = (int)Math.round(parseDouble(pointXTextField.getText()) * SWF.unitDivisor); - int y = (int)Math.round(parseDouble(pointYTextField.getText()) * SWF.unitDivisor); - + int x = (int) Math.round(parseDouble(pointXTextField.getText()) * SWF.unitDivisor); + int y = (int) Math.round(parseDouble(pointYTextField.getText()) * SWF.unitDivisor); + java.awt.Point minSelectedPoint = getMinSelectedPoint(); if (minSelectedPoint == null) { return; @@ -351,15 +358,15 @@ public final class ImagePanel extends JPanel implements MediaDisplay { point.x = point.x - minSelectedPoint.x + x; point.y = point.y - minSelectedPoint.y + y; } - - firePointsUpdated(hilightedPoints); + + firePointsUpdated(hilightedPoints); } catch (NumberFormatException nfe) { //ignore - } + } } - + private java.awt.Point getMinSelectedPoint() { - if (selectedPoints.isEmpty()) { + if (selectedPoints.isEmpty()) { return null; } int minX = Integer.MAX_VALUE; @@ -375,7 +382,7 @@ public final class ImagePanel extends JPanel implements MediaDisplay { } return new java.awt.Point(minX, minY); } - + private void calculatePointsXY() { Point2D minPoint = getMinSelectedPoint(); if (minPoint == null) { @@ -383,7 +390,7 @@ public final class ImagePanel extends JPanel implements MediaDisplay { pointYTextField.setText(""); return; } - + pointXTextField.setText(formatDouble(minPoint.getX() / SWF.unitDivisor)); pointYTextField.setText(formatDouble(minPoint.getY() / SWF.unitDivisor)); } @@ -494,7 +501,7 @@ public final class ImagePanel extends JPanel implements MediaDisplay { shearYCursor = loadCursor("shear_y", 5, 9); movePointCursor = loadCursor("move_point", 0, 0); defaultCursor = loadCursor("default", 0, 0); - + addPointCursor = loadCursor("add_point", 0, 0); } catch (IOException ex) { Logger.getLogger(MainPanel.class.getName()).log(Level.SEVERE, null, ex); } @@ -788,13 +795,13 @@ public final class ImagePanel extends JPanel implements MediaDisplay { } RECT timRect = timelined.getRect(); double zoomDouble = zoom.fit ? getZoomToFit() : zoom.value; - AffineTransform t = new AffineTransform(); - t.translate(offsetPoint.getX() - (int) (timRect.Xmin * zoomDouble / SWF.unitDivisor), + AffineTransform trans = new AffineTransform(); + trans.translate(offsetPoint.getX() - (int) (timRect.Xmin * zoomDouble / SWF.unitDivisor), offsetPoint.getY() - (int) (timRect.Ymin * zoomDouble / SWF.unitDivisor)); - t.scale(1 / SWF.unitDivisor, 1 / SWF.unitDivisor); - t.scale(zoomDouble, zoomDouble); + trans.scale(1 / SWF.unitDivisor, 1 / SWF.unitDivisor); + trans.scale(zoomDouble, zoomDouble); AffineTransform oldTransform = g2.getTransform(); - g2.setTransform(t); + g2.setTransform(trans); if (hilightedEdge != null) { g2.setStroke(new BasicStroke((float) (SWF.unitDivisor * 6 / zoomDouble))); @@ -831,7 +838,7 @@ public final class ImagePanel extends JPanel implements MediaDisplay { g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC); g2.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); - + g2.setStroke(new BasicStroke((float) (SWF.unitDivisor * 1 / zoomDouble))); double pointSize = SWF.unitDivisor * 4 / zoomDouble; //selectedPoints @@ -856,6 +863,51 @@ public final class ImagePanel extends JPanel implements MediaDisplay { } g2.draw(pointShape); } + + /*DisplayPoint p = closestPoint; + if (p != null) { + Shape pointShape; + pointShape = new Ellipse2D.Double(p.x - pointSize, p.y - pointSize, pointSize * 2, pointSize * 2); + g2.setPaint(Color.blue); + g2.fill(pointShape); + } + + Integer puc = pathPointUnderCursor; + if (puc != null) { + p = hilightedPoints.get((int) puc); + Shape pointShape; + pointShape = new Ellipse2D.Double(p.x - pointSize, p.y - pointSize, pointSize * 2, pointSize * 2); + g2.setPaint(Color.pink); + g2.fill(pointShape); + + if (puc - 1 >= 0) { + p = hilightedPoints.get(puc - 1); + pointShape = new Ellipse2D.Double(p.x - pointSize, p.y - pointSize, pointSize * 2, pointSize * 2); + g2.setPaint(Color.cyan); + g2.fill(pointShape); + } + + }*/ + /*for (int i = 0; i < points.size(); i++) { + DisplayPoint p = points.get(i); + if (!p.onPath) { + DisplayPoint p0 = points.get(i - 1); + DisplayPoint p1 = points.get(i); + DisplayPoint p2 = points.get(i + 1); + + for (int j = 0; j <= 10; j++) { + double t = j / 10.0; + double xt = (1 - t) * (1 - t) * p0.x + 2 * t * (1 - t) * p1.x + t * t * p2.x; + double yt = (1 - t) * (1 - t) * p0.y + 2 * t * (1 - t) * p1.y + t * t * p2.y; + //System.out.println("P("+t+") = "+xt+","+yt); + + Shape pointShape; + pointShape = new Ellipse2D.Double(xt - pointSize, yt - pointSize, pointSize * 2, pointSize * 2); + g2.setPaint(Color.green); + g2.fill(pointShape); + } + } + }*/ } g2.setTransform(oldTransform); } @@ -918,6 +970,10 @@ public final class ImagePanel extends JPanel implements MediaDisplay { return altDown; } + public boolean isCtrlDown() { + return ctrlDown; + } + public IconPanel() { KeyboardFocusManager manager = KeyboardFocusManager.getCurrentKeyboardFocusManager(); @@ -961,7 +1017,7 @@ public final class ImagePanel extends JPanel implements MediaDisplay { return o2 - o1; } }); - for (int i:selectedPointsDesc) { + for (int i : selectedPointsDesc) { firePointRemoved(hilightedPoints, i); } selectedPoints.clear(); @@ -1079,7 +1135,7 @@ public final class ImagePanel extends JPanel implements MediaDisplay { } } if (shiftDown) { - for (int p: selectedPoints) { + for (int p : selectedPoints) { if (!newSelectedPoints.contains(p)) { newSelectedPoints.add(p); } @@ -1088,6 +1144,15 @@ public final class ImagePanel extends JPanel implements MediaDisplay { selectedPoints = newSelectedPoints; calculatePointsXY(); } + + if (ctrlDown && pathPointUnderCursor != null) { + fireEdgeSplit(hilightedPoints, pathPointUnderCursor, pathPointPosition); + selectedPoints.clear(); + pointsUnderCursor.clear(); + pathPointUnderCursor = null; + pathPointPosition = 0.0; + repaint(); + } } dragStart = null; @@ -1656,12 +1721,28 @@ public final class ImagePanel extends JPanel implements MediaDisplay { } } + class DistanceItem { + + public double distance; + public int pathPoint; + public double pathPosition; + public DisplayPoint closestPoint; + + public DistanceItem(double distance, int pathPoint, double pathPosition, DisplayPoint closestPoint) { + this.distance = distance; + this.pathPoint = pathPoint; + this.pathPosition = pathPosition; + this.closestPoint = closestPoint; + } + + } + @Override public void mouseMoved(MouseEvent e) { List points = hilightedPoints; if (points != null) { - Cursor cursor = selectCursor; int maxDistance = 5; + double zoomDouble = zoom.fit ? getZoomToFit() : zoom.value; List newPointsUnderCursor = new ArrayList<>(); for (int i = 0; i < points.size(); i++) { DisplayPoint p = points.get(i); @@ -1670,17 +1751,91 @@ public final class ImagePanel extends JPanel implements MediaDisplay { int ey = e.getY(); if (ex > ip.getX() - maxDistance && ex < ip.getX() + maxDistance) { if (ey > ip.getY() - maxDistance && ey < ip.getY() + maxDistance) { - cursor = movePointCursor; newPointsUnderCursor.add(i); } } } - if (dragStart == null) { - pointsUnderCursor = newPointsUnderCursor; + + Point2D p = toTransformPoint(e.getPoint()); + + List distanceList = new ArrayList<>(); + for (int i = 0; i < points.size() - 1; i++) { + if (points.get(i).onPath && points.get(i + 1).onPath) { + DisplayPoint p0 = points.get(i); + DisplayPoint p1 = points.get(i + 1); + + //y = mx + b + double lineDistance; + Point2D closestPoint; + if (p1.x == p0.x) { + lineDistance = Math.abs(p1.x - p.getX()); + closestPoint = new Point2D.Double(p1.x, p.getY()); + } else if (p1.y == p0.y) { + lineDistance = Math.abs(p1.y - p.getY()); + closestPoint = new Point2D.Double(p.getX(), p1.y); + } else { + double m = (p1.y - p0.y) / (double) (p1.x - p0.x); + double b = p0.y - m * p0.x; + + double m_perp = - 1 / m; + + double b_perp = p.getY() - m_perp * p.getX(); + + double x = (b_perp - b) / (m - m_perp); + double y = m * x + b; + closestPoint = new Point2D.Double(x, y); + lineDistance = p.distance(closestPoint); + } + + double minX = Math.min(p0.x, p1.x) - maxDistance * SWF.unitDivisor / zoomDouble; + double minY = Math.min(p0.y, p1.y) - maxDistance * SWF.unitDivisor / zoomDouble; + + double maxX = Math.max(p0.x, p1.x) + maxDistance * SWF.unitDivisor / zoomDouble; + double maxY = Math.max(p0.y, p1.y) + maxDistance * SWF.unitDivisor / zoomDouble; + + if (p.getX() >= minX && p.getX() <= maxX && p.getY() >= minY && p.getY() <= maxY) { + double t = p0.toPoint2D().distance(closestPoint) / p0.toPoint2D().distance(p1.toPoint2D()); + if (lineDistance <= maxDistance * SWF.unitDivisor / zoomDouble) { + distanceList.add(new DistanceItem(lineDistance, i + 1, t, new DisplayPoint(closestPoint))); + } + } + + + } + if (i < points.size() - 2 && !points.get(i + 1).onPath) { + DisplayPoint p0 = points.get(i); + DisplayPoint p1 = points.get(i + 1); + DisplayPoint p2 = points.get(i + 2); + + BezierUtils bezierUtils = new BezierUtils(); + double t = bezierUtils.closestPointToBezier(p, p0.toPoint2D(), p1.toPoint2D(), p2.toPoint2D()); + DisplayPoint closestPoint = new DisplayPoint(bezierUtils.pointAt(t, p0.toPoint2D(), p1.toPoint2D(), p2.toPoint2D())); + double curveDistance = Math.sqrt((p.getX() - closestPoint.x) * (p.getX() - closestPoint.x) + + (p.getY() - closestPoint.y) * (p.getY() - closestPoint.y)); + if (curveDistance <= maxDistance * SWF.unitDivisor / zoomDouble) { + distanceList.add(new DistanceItem(curveDistance, i + 1, t, closestPoint)); + } + } } - if (getCursor() != cursor) { - setCursor(cursor); + distanceList.sort(new Comparator() { + @Override + public int compare(DistanceItem o1, DistanceItem o2) { + return Double.compare(o1.distance, o2.distance); + } + }); + if (dragStart == null) { + if (!distanceList.isEmpty()) { + DistanceItem di = distanceList.get(0); + pathPointUnderCursor = di.pathPoint; + pathPointPosition = di.pathPosition; + closestPoint = di.closestPoint; + } else { + pathPointUnderCursor = null; + pathPointPosition = null; + closestPoint = null; + } + pointsUnderCursor = newPointsUnderCursor; } return; } @@ -2030,7 +2185,7 @@ public final class ImagePanel extends JPanel implements MediaDisplay { debugLabel.setAlignmentX(JLabel.LEFT_ALIGNMENT); topPanel.add(debugLabel); - DocumentListener documentListener = new DocumentListener(){ + DocumentListener documentListener = new DocumentListener() { @Override public void insertUpdate(DocumentEvent e) { applyPointsXY(); @@ -2044,21 +2199,21 @@ public final class ImagePanel extends JPanel implements MediaDisplay { @Override public void changedUpdate(DocumentEvent e) { applyPointsXY(); - } + } }; - + pointEditPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); pointEditPanel.add(new JLabel(AppStrings.translate("edit.points.x"))); - pointXTextField = new JTextField(6); + pointXTextField = new JTextField(6); pointXTextField.getDocument().addDocumentListener(documentListener); pointEditPanel.add(pointXTextField); pointEditPanel.add(new JLabel(AppStrings.translate("edit.points.y"))); pointYTextField = new JTextField(6); pointYTextField.getDocument().addDocumentListener(documentListener); pointEditPanel.add(pointYTextField); - + pointEditPanel.setAlignmentX(JPanel.LEFT_ALIGNMENT); - + topPanel.add(pointEditPanel); pointEditPanel.setVisible(false); add(topPanel, BorderLayout.NORTH); @@ -3068,6 +3223,23 @@ public final class ImagePanel extends JPanel implements MediaDisplay { } else { debugLabel.setText(ret.toString()); } + if (hilightedPoints != null) { + Cursor newCursor; + if (!pointsUnderCursor.isEmpty()) { + newCursor = movePointCursor; + } else if (pathPointUnderCursor != null) { + if (iconPanel.isCtrlDown()) { + newCursor = addPointCursor; + } else { + newCursor = defaultCursor; + } + } else { + newCursor = selectCursor; + } + if (iconPanel.getCursor() != newCursor) { //call setcursor only when needed to avoid cursor flickering when dragging in the tree + iconPanel.setCursor(newCursor); + } + } if (freeTransformDepth == -1 && hilightedPoints == null) { Cursor newCursor; if (iconPanel.isAltDown()) { diff --git a/src/com/jpexs/decompiler/flash/gui/PointUpdateListener.java b/src/com/jpexs/decompiler/flash/gui/PointUpdateListener.java index c6b01f75d..2c5e0ce49 100644 --- a/src/com/jpexs/decompiler/flash/gui/PointUpdateListener.java +++ b/src/com/jpexs/decompiler/flash/gui/PointUpdateListener.java @@ -25,7 +25,7 @@ import java.util.List; public interface PointUpdateListener { public void pointsUpdated(List points); - public boolean pointAdded(List points, int position, DisplayPoint point); + public boolean edgeSplit(List points, int position, double splitPoint); public boolean pointRemoved(List points, int position); } diff --git a/src/com/jpexs/decompiler/flash/gui/PreviewPanel.java b/src/com/jpexs/decompiler/flash/gui/PreviewPanel.java index 948a47d9a..1297f17d2 100644 --- a/src/com/jpexs/decompiler/flash/gui/PreviewPanel.java +++ b/src/com/jpexs/decompiler/flash/gui/PreviewPanel.java @@ -780,9 +780,80 @@ public class PreviewPanel extends JPersistentSplitPane implements TagEditorPanel } @Override - public boolean pointAdded(List points, int position, DisplayPoint point) { - return true; - } + public boolean edgeSplit(List points, int position, double splitPoint) { + ShapeTag shape = (ShapeTag) displayEditTag; + int pointsPos = 0; + int x = 0; + int y = 0; + for (int i = 0; i < shape.shapes.shapeRecords.size(); i++) { + SHAPERECORD rec = shape.shapes.shapeRecords.get(i); + if (rec instanceof StyleChangeRecord) { + StyleChangeRecord scr = (StyleChangeRecord) rec; + if (scr.stateMoveTo) { + pointsPos++; + } + } + if (rec instanceof StraightEdgeRecord) { + StraightEdgeRecord ser = (StraightEdgeRecord) rec; + if (pointsPos == position) { + StraightEdgeRecord newSer = new StraightEdgeRecord(); + newSer.generalLineFlag = true; + newSer.deltaX = (int)Math.round(ser.deltaX * (1-splitPoint)); + newSer.deltaY = (int)Math.round(ser.deltaY * (1-splitPoint)); + newSer.simplify(); + ser.generalLineFlag = true; + ser.deltaX -= newSer.deltaX; + ser.deltaY -= newSer.deltaY; + ser.simplify(); + shape.shapes.shapeRecords.add(i + 1, newSer); + points.add(position, new DisplayPoint(new Point2D.Double(x + ser.deltaX, y + ser.deltaY))); + shape.getSwf().clearShapeCache(); + displayEditImagePanel.repaint(); + return true; + } + pointsPos += 1; + ser.simplify(); + } + if (rec instanceof CurvedEdgeRecord) { + CurvedEdgeRecord cer = (CurvedEdgeRecord) rec; + if (pointsPos == position) { + Point2D p0 = new Point2D.Double(x, y); + Point2D p1 = new Point2D.Double(x + cer.controlDeltaX, y + cer.controlDeltaY); + Point2D p2 = new Point2D.Double(x + cer.controlDeltaX + cer.anchorDeltaX, y + cer.controlDeltaY + cer.anchorDeltaY); + List v = new ArrayList<>(); + v.add(p0); + v.add(p1); + v.add(p2); + BezierUtils bu = new BezierUtils(); + List left = new ArrayList<>(); + List right = new ArrayList<>(); + bu.subdivide(v, splitPoint, left, right); + cer.controlDeltaX = (int)Math.round(left.get(1).getX() - left.get(0).getX()); + cer.controlDeltaY = (int)Math.round(left.get(1).getY() - left.get(0).getY()); + cer.anchorDeltaX = (int)Math.round(left.get(2).getX() - left.get(1).getX()); + cer.anchorDeltaY = (int)Math.round(left.get(2).getY() - left.get(1).getY()); + + CurvedEdgeRecord newCer = new CurvedEdgeRecord(); + newCer.controlDeltaX = (int)Math.round(right.get(1).getX() - right.get(0).getX()); + newCer.controlDeltaY = (int)Math.round(right.get(1).getY() - right.get(0).getY()); + newCer.anchorDeltaX = (int)Math.round(right.get(2).getX() - right.get(1).getX()); + newCer.anchorDeltaY = (int)Math.round(right.get(2).getY() - right.get(1).getY()); + shape.shapes.shapeRecords.add(i + 1, newCer); + points.remove(position); + points.add(position, new DisplayPoint(new Point2D.Double(left.get(1).getX(), left.get(1).getY()), false)); + points.add(position + 1, new DisplayPoint(new Point2D.Double(left.get(2).getX(), left.get(2).getY()))); + points.add(position + 2, new DisplayPoint(new Point2D.Double(right.get(1).getX(), right.get(1).getY()), false)); + shape.getSwf().clearShapeCache(); + displayEditImagePanel.repaint(); + return true; + } + pointsPos += 2; + } + x = rec.changeX(x); + y = rec.changeY(y); + } + return false; + } @Override public boolean pointRemoved(List points, int position) { diff --git a/src/com/jpexs/decompiler/flash/gui/graphics/cursors/add_point.png b/src/com/jpexs/decompiler/flash/gui/graphics/cursors/add_point.png new file mode 100644 index 000000000..999940fc2 Binary files /dev/null and b/src/com/jpexs/decompiler/flash/gui/graphics/cursors/add_point.png differ