mirror of
https://git.huckle.dev/Huckles-Minecraft-Archive/jpexs-decompiler.git
synced 2026-06-22 11:25:38 +00:00
Fixed: #2443 SVG importer - converting cubic bezier curves to quadratic
This commit is contained in:
@@ -17,6 +17,7 @@
|
||||
package com.jpexs.decompiler.flash.importers.svg;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
@@ -28,6 +29,12 @@ import java.util.List;
|
||||
*/
|
||||
public class CubicToQuad {
|
||||
|
||||
|
||||
// Precision used to check determinant in quad and cubic solvers,
|
||||
// any number lower than this is considered to be zero.
|
||||
// `8.67e-19` is an example of real error occurring in tests.
|
||||
private static final double EPSILON = 1e-16;
|
||||
|
||||
class Point {
|
||||
|
||||
public double x;
|
||||
@@ -66,6 +73,11 @@ public class CubicToQuad {
|
||||
public double dot(Point point) {
|
||||
return this.x * point.x + this.y * point.y;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "[" + x + ", " + y + "]";
|
||||
}
|
||||
}
|
||||
|
||||
private Point[] calcPowerCoefficients(Point p1, Point c1, Point c2, Point p2) {
|
||||
@@ -103,7 +115,7 @@ public class CubicToQuad {
|
||||
return (b == 0) ? new double[0] : new double[]{-c / b};
|
||||
}
|
||||
double D = b * b - 4 * a * c;
|
||||
if (D < 0) {
|
||||
if (Math.abs(D) < EPSILON) {
|
||||
return new double[0];
|
||||
} else if (D == 0) {
|
||||
return new double[]{-b / (2 * a)};
|
||||
@@ -228,6 +240,7 @@ public class CubicToQuad {
|
||||
return new Point[]{f1, new Point(cx, cy), f2};
|
||||
}
|
||||
|
||||
/*
|
||||
private boolean isSegmentApproximationClose(Point a, Point b, Point c, Point d, double tmin, double tmax, Point p1, Point c1, Point p2, double errorBound) {
|
||||
// a,b,c,d define cubic curve
|
||||
// tmin, tmax are boundary points on cubic curve
|
||||
@@ -256,6 +269,111 @@ public class CubicToQuad {
|
||||
}
|
||||
return true;
|
||||
}
|
||||
*/
|
||||
private Point[] calcPowerCoefficientsQuad(Point p1, Point c1, Point p2) {
|
||||
// point(t) = p1*(1-t)^2 + c1*t*(1-t) + p2*t^2 = a*t^2 + b*t + c
|
||||
// for each t value, so
|
||||
// a = p1 + p2 - 2 * c1
|
||||
// b = 2 * (c1 - p1)
|
||||
// c = p1
|
||||
Point a = c1.mul(-2).add(p1).add(p2);
|
||||
Point b = c1.sub(p1).mul(2);
|
||||
Point c = p1;
|
||||
return new Point[]{a, b, c};
|
||||
}
|
||||
|
||||
/*
|
||||
* Calculate a distance between a `point` and a line segment `p1, p2`
|
||||
* (result is squared for performance reasons), see details here:
|
||||
* https://stackoverflow.com/questions/849211/shortest-distance-between-a-point-and-a-line-segment
|
||||
*/
|
||||
private double minDistanceToLineSq(Point point, Point p1, Point p2) {
|
||||
Point p1p2 = p2.sub(p1);
|
||||
double dot = point.sub(p1).dot(p1p2);
|
||||
double lenSq = p1p2.sqr();
|
||||
double param = 0;
|
||||
Point diff;
|
||||
if (lenSq != 0) {
|
||||
param = dot / lenSq;
|
||||
}
|
||||
if (param <= 0) {
|
||||
diff = point.sub(p1);
|
||||
} else if (param >= 1) {
|
||||
diff = point.sub(p2);
|
||||
} else {
|
||||
diff = point.sub(p1.add(p1p2.mul(param)));
|
||||
}
|
||||
return diff.sqr();
|
||||
}
|
||||
|
||||
/*
|
||||
* Divide cubic and quadratic curves into 10 points and 9 line segments.
|
||||
* Calculate distances between each point on cubic and nearest line segment
|
||||
* on quadratic (and vice versa), and make sure all distances are less
|
||||
* than `errorBound`.
|
||||
*
|
||||
* We need to calculate BOTH distance from all points on quadratic to any cubic,
|
||||
* and all points on cubic to any quadratic.
|
||||
*
|
||||
* If we do it only one way, it may lead to an error if the entire original curve
|
||||
* falls within errorBound (then **any** quad will erroneously treated as good):
|
||||
* https://github.com/fontello/svg2ttf/issues/105#issuecomment-842558027
|
||||
*
|
||||
* - a,b,c,d define cubic curve (power coefficients)
|
||||
* - tmin, tmax are boundary points on cubic curve (in 0-1 range)
|
||||
* - p1, c1, p2 define quadratic curve (control points)
|
||||
* - errorBound is maximum allowed distance
|
||||
*/
|
||||
private boolean isSegmentApproximationClose(Point a, Point b, Point c, Point d, double tmin, double tmax, Point p1, Point c1, Point p2, double errorBound) {
|
||||
int n = 10; // number of points
|
||||
double t;
|
||||
double dt;
|
||||
Point[] p = calcPowerCoefficientsQuad(p1, c1, p2);
|
||||
Point qa = p[0];
|
||||
Point qb = p[1];
|
||||
Point qc = p[2];
|
||||
int i;
|
||||
int j;
|
||||
double distSq;
|
||||
double errorBoundSq = errorBound * errorBound;
|
||||
List<Point> cubicPoints = new ArrayList<>();
|
||||
List<Point> quadPoints = new ArrayList<>();
|
||||
double minDistSq;
|
||||
|
||||
dt = (tmax - tmin) / ((double) n);
|
||||
for (i = 0, t = tmin; i <= n; i++, t += dt) {
|
||||
cubicPoints.add(calcPoint(a, b, c, d, t));
|
||||
}
|
||||
|
||||
dt = 1 / ((double) n);
|
||||
for (i = 0, t = 0; i <= n; i++, t += dt) {
|
||||
quadPoints.add(calcPointQuad(qa, qb, qc, t));
|
||||
}
|
||||
|
||||
for (i = 1; i < cubicPoints.size() - 1; i++) {
|
||||
minDistSq = Double.MAX_VALUE;
|
||||
for (j = 0; j < quadPoints.size() - 1; j++) {
|
||||
distSq = minDistanceToLineSq(cubicPoints.get(i), quadPoints.get(j), quadPoints.get(j + 1));
|
||||
minDistSq = Math.min(minDistSq, distSq);
|
||||
}
|
||||
if (minDistSq > errorBoundSq) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
for (i = 1; i < quadPoints.size() - 1; i++) {
|
||||
minDistSq = Double.MAX_VALUE;
|
||||
for (j = 0; j < cubicPoints.size() - 1; j++) {
|
||||
distSq = minDistanceToLineSq(quadPoints.get(i), cubicPoints.get(j), cubicPoints.get(j + 1));
|
||||
minDistSq = Math.min(minDistSq, distSq);
|
||||
}
|
||||
if (minDistSq > errorBoundSq) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean _isApproximationClose(Point a, Point b, Point c, Point d, List<Point[]> quadCurves, double errorBound) {
|
||||
double dt = 1.0 / quadCurves.size();
|
||||
@@ -307,12 +425,42 @@ public class CubicToQuad {
|
||||
return _isApproximationClose(pc[0], pc[1], pc[2], pc[3], fromFlatArray(quads), errorBound);
|
||||
}
|
||||
|
||||
/*
|
||||
* Split cubic bézier curve into two cubic curves, see details here:
|
||||
* https://math.stackexchange.com/questions/877725
|
||||
*/
|
||||
private double[][] subdivideCubic(double x1, double y1, double x2, double y2, double x3, double y3, double x4, double y4, double t) {
|
||||
double u = 1 - t;
|
||||
double v = t;
|
||||
|
||||
double bx = x1 * u + x2 * v;
|
||||
double sx = x2 * u + x3 * v;
|
||||
double fx = x3 * u + x4 * v;
|
||||
double cx = bx * u + sx * v;
|
||||
double ex = sx * u + fx * v;
|
||||
double dx = cx * u + ex * v;
|
||||
|
||||
double by = y1 * u + y2 * v;
|
||||
double sy = y2 * u + y3 * v;
|
||||
double fy = y3 * u + y4 * v;
|
||||
double cy = by * u + sy * v;
|
||||
double ey = sy * u + fy * v;
|
||||
double dy = cy * u + ey * v;
|
||||
|
||||
return new double[][]{
|
||||
{x1, y1, bx, by, cx, cy, dx, dy},
|
||||
{dx, dy, ex, ey, fx, fy, x4, y4}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Approximate cubic Bezier curve defined with base points p1, p2 and control points c1, c2 with
|
||||
* with a few quadratic Bezier curves.
|
||||
* The function uses tangent method to find quadratic approximation of cubic curve segment and
|
||||
* simplified Hausdorff distance to determine number of segments that is enough to make error small.
|
||||
* In general the method is the same as described here: https://fontforge.github.io/bezier.html.
|
||||
* Approximate cubic Bezier curve defined with base points p1, p2 and
|
||||
* control points c1, c2 with with a few quadratic Bezier curves. The
|
||||
* function uses tangent method to find quadratic approximation of cubic
|
||||
* curve segment and simplified Hausdorff distance to determine number of
|
||||
* segments that is enough to make error small. In general the method is the
|
||||
* same as described here: https://fontforge.github.io/bezier.html.
|
||||
*
|
||||
* @param p1x Base point 1 x coordinate
|
||||
* @param p1y Base point 1 y coordinate
|
||||
* @param c1x Control point 1 x coordinate
|
||||
@@ -325,6 +473,66 @@ public class CubicToQuad {
|
||||
* @return List of quadratic Bezier curve points
|
||||
*/
|
||||
public List<Double> cubicToQuad(double p1x, double p1y, double c1x, double c1y, double c2x, double c2y, double p2x, double p2y, double errorBound) {
|
||||
List<Double> inflections = solveInflections(p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y);
|
||||
if (inflections.isEmpty()) {
|
||||
return _cubicToQuad(p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y, errorBound);
|
||||
}
|
||||
|
||||
List<Double> result = new ArrayList<>();
|
||||
double[] curve = new double[]{p1x, p1y, c1x, c1y, c2x, c2y, p2x, p2y};
|
||||
double prevPoint = 0;
|
||||
List<Double> quad;
|
||||
double[][] split;
|
||||
|
||||
for (int inflectionIdx = 0; inflectionIdx < inflections.size(); inflectionIdx++) {
|
||||
split = subdivideCubic(
|
||||
curve[0], curve[1], curve[2], curve[3],
|
||||
curve[4], curve[5], curve[6], curve[7],
|
||||
// we make a new curve, so adjust inflection point accordingly
|
||||
1 - (1 - inflections.get(inflectionIdx)) / (1 - prevPoint)
|
||||
);
|
||||
|
||||
quad = _cubicToQuad(
|
||||
split[0][0], split[0][1], split[0][2], split[0][3],
|
||||
split[0][4], split[0][5], split[0][6], split[0][7],
|
||||
errorBound
|
||||
);
|
||||
|
||||
result.addAll(quad.subList(0, quad.size() - 2));
|
||||
curve = split[1];
|
||||
prevPoint = inflections.get(inflectionIdx);
|
||||
}
|
||||
|
||||
quad = _cubicToQuad(
|
||||
curve[0], curve[1], curve[2], curve[3],
|
||||
curve[4], curve[5], curve[6], curve[7],
|
||||
errorBound
|
||||
);
|
||||
|
||||
result.addAll(quad);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Approximate cubic Bezier curve defined with base points p1, p2 and
|
||||
* control points c1, c2 with with a few quadratic Bezier curves. The
|
||||
* function uses tangent method to find quadratic approximation of cubic
|
||||
* curve segment and simplified Hausdorff distance to determine number of
|
||||
* segments that is enough to make error small. In general the method is the
|
||||
* same as described here: https://fontforge.github.io/bezier.html.
|
||||
*
|
||||
* @param p1x Base point 1 x coordinate
|
||||
* @param p1y Base point 1 y coordinate
|
||||
* @param c1x Control point 1 x coordinate
|
||||
* @param c1y Control point 1 y coordinate
|
||||
* @param c2x Control point 2 x coordinate
|
||||
* @param c2y Control point 2 y coordinate
|
||||
* @param p2x Base point 2 x coordinate
|
||||
* @param p2y Base point 2 y coordinate
|
||||
* @param errorBound Error bound
|
||||
* @return List of quadratic Bezier curve points
|
||||
*/
|
||||
private List<Double> _cubicToQuad(double p1x, double p1y, double c1x, double c1y, double c2x, double c2y, double p2x, double p2y, double errorBound) {
|
||||
Point p1 = new Point(p1x, p1y);
|
||||
Point c1 = new Point(c1x, c1y);
|
||||
Point c2 = new Point(c2x, c2y);
|
||||
@@ -352,4 +560,35 @@ public class CubicToQuad {
|
||||
}
|
||||
return toFlatArray(approximation);
|
||||
}
|
||||
|
||||
/*
|
||||
* Find inflection points on a cubic curve, algorithm is similar to this one:
|
||||
* http://www.caffeineowl.com/graphics/2d/vectorial/cubic-inflexion.html
|
||||
*/
|
||||
private List<Double> solveInflections(double x1, double y1, double x2, double y2, double x3, double y3, double x4, double y4) {
|
||||
double p = -(x4 * (y1 - 2 * y2 + y3)) + x3 * (2 * y1 - 3 * y2 + y4)
|
||||
+ x1 * (y2 - 2 * y3 + y4) - x2 * (y1 - 3 * y3 + 2 * y4);
|
||||
double q = x4 * (y1 - y2) + 3 * x3 * (-y1 + y2) + x2 * (2 * y1 - 3 * y3 + y4) - x1 * (2 * y2 - 3 * y3 + y4);
|
||||
double r = x3 * (y1 - y2) + x1 * (y2 - y3) + x2 * (-y1 + y3);
|
||||
double[] arr = quadSolve(p, q, r);
|
||||
List<Double> inf = new ArrayList<>();
|
||||
for (double t : arr) {
|
||||
if (t > 1e-8 && t < 1 - 1e-8) {
|
||||
inf.add(t);
|
||||
}
|
||||
}
|
||||
Collections.sort(inf);
|
||||
|
||||
return inf;
|
||||
// return quadSolve(p, q, r).filter(function (t) { return t > 1e-8 && t < 1 - 1e-8 }).sort(byNumber)
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
List<Double> quadCoordinates = new CubicToQuad().cubicToQuad(7217.0, 4004.0, 7155.32, 4019.7800000000007, 6403.46, 3544.120000000001, 6280.699999999999, 3559.46, 0.1);
|
||||
int r = 0;
|
||||
for (Double d : quadCoordinates) {
|
||||
System.err.println("" + r + ": " + d);
|
||||
r++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user