mirror of
https://git.huckle.dev/Huckles-Minecraft-Archive/jpexs-decompiler.git
synced 2026-06-25 15:25:39 +00:00
approximate cubic curves with quadratic
This commit is contained in:
@@ -20,6 +20,7 @@ import com.jpexs.decompiler.flash.SWF;
|
||||
import com.jpexs.decompiler.flash.exporters.commonshape.Matrix;
|
||||
import com.jpexs.decompiler.flash.exporters.commonshape.Point;
|
||||
import com.jpexs.decompiler.flash.helpers.ImageHelper;
|
||||
import com.jpexs.decompiler.flash.importers.svg.CubicToQuad;
|
||||
import com.jpexs.decompiler.flash.tags.DefineBitsJPEG2Tag;
|
||||
import com.jpexs.decompiler.flash.tags.DefineBitsJPEG3Tag;
|
||||
import com.jpexs.decompiler.flash.tags.DefineBitsJPEG4Tag;
|
||||
@@ -53,6 +54,7 @@ import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.StringReader;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
import javax.xml.parsers.DocumentBuilder;
|
||||
@@ -193,8 +195,8 @@ public class ShapeImporter {
|
||||
if (!fill) {
|
||||
// todo: how to calulate the real SVG size?
|
||||
RECT bounds = shapes.getBounds();
|
||||
rect.Xmax = rect.Xmin + bounds.getWidth();
|
||||
rect.Ymax = rect.Ymin + bounds.getHeight();
|
||||
rect.Xmax = rect.Xmin + bounds.Xmax - Math.min(0, bounds.Xmin);
|
||||
rect.Ymax = rect.Ymin + bounds.Ymax - Math.min(0, bounds.Ymin);
|
||||
}
|
||||
|
||||
st.shapes = shapes;
|
||||
@@ -226,6 +228,8 @@ public class ShapeImporter {
|
||||
|
||||
char command = 0;
|
||||
Point prevPoint = new Point(0, 0);
|
||||
Point startPoint = prevPoint;
|
||||
Point prevCControlPoint = null;
|
||||
double x0 = 0;
|
||||
double y0 = 0;
|
||||
|
||||
@@ -235,7 +239,6 @@ public class ShapeImporter {
|
||||
char newCommand;
|
||||
if ((newCommand = pathReader.readCommand()) != 0) {
|
||||
command = newCommand;
|
||||
continue;
|
||||
}
|
||||
|
||||
boolean isRelative = Character.isLowerCase(command);
|
||||
@@ -244,7 +247,8 @@ public class ShapeImporter {
|
||||
double y = y0;
|
||||
|
||||
Point p = null;
|
||||
switch (Character.toUpperCase(command)) {
|
||||
char cmd = Character.toUpperCase(command);
|
||||
switch (cmd) {
|
||||
case 'M':
|
||||
StyleChangeRecord scr;
|
||||
if (newShape) {
|
||||
@@ -269,9 +273,20 @@ public class ShapeImporter {
|
||||
scr.stateMoveTo = true;
|
||||
|
||||
shapes.shapeRecords.add(scr);
|
||||
startPoint = p;
|
||||
break;
|
||||
case 'Z':
|
||||
StraightEdgeRecord serz = new StraightEdgeRecord();
|
||||
p = startPoint;
|
||||
serz.deltaX = (int) Math.round(p.x - prevPoint.x);
|
||||
serz.deltaY = (int) Math.round(p.y - prevPoint.y);
|
||||
prevPoint = p;
|
||||
System.out.println("Z" + serz.deltaX + "," + serz.deltaY);
|
||||
serz.generalLineFlag = true;
|
||||
shapes.shapeRecords.add(serz);
|
||||
break;
|
||||
case 'L':
|
||||
StraightEdgeRecord ser = new StraightEdgeRecord();
|
||||
StraightEdgeRecord serl = new StraightEdgeRecord();
|
||||
x = pathReader.readDouble();
|
||||
y = pathReader.readDouble();
|
||||
if (isRelative) {
|
||||
@@ -280,12 +295,12 @@ public class ShapeImporter {
|
||||
}
|
||||
|
||||
p = transform.transform(x, y);
|
||||
ser.deltaX = (int) Math.round(p.x - prevPoint.x);
|
||||
ser.deltaY = (int) Math.round(p.y - prevPoint.y);
|
||||
serl.deltaX = (int) Math.round(p.x - prevPoint.x);
|
||||
serl.deltaY = (int) Math.round(p.y - prevPoint.y);
|
||||
prevPoint = p;
|
||||
System.out.println("L" + ser.deltaX + "," + ser.deltaY);
|
||||
ser.generalLineFlag = true;
|
||||
shapes.shapeRecords.add(ser);
|
||||
System.out.println("L" + serl.deltaX + "," + serl.deltaY);
|
||||
serl.generalLineFlag = true;
|
||||
shapes.shapeRecords.add(serl);
|
||||
break;
|
||||
case 'H':
|
||||
StraightEdgeRecord serh = new StraightEdgeRecord();
|
||||
@@ -343,13 +358,33 @@ public class ShapeImporter {
|
||||
shapes.shapeRecords.add(cer);
|
||||
break;
|
||||
case 'C':
|
||||
case 'S':
|
||||
if (!cubicWarning) {
|
||||
cubicWarning = true;
|
||||
Logger.getLogger(ShapeImporter.class.getName()).log(Level.WARNING, "Cubic curves are not supported by Flash.");
|
||||
}
|
||||
|
||||
// create at least something...
|
||||
CurvedEdgeRecord cer2 = new CurvedEdgeRecord();
|
||||
Point pStart = prevPoint;
|
||||
Point pControl1;
|
||||
|
||||
if (cmd == 'C') {
|
||||
x = pathReader.readDouble();
|
||||
y = pathReader.readDouble();
|
||||
if (isRelative) {
|
||||
x += x0;
|
||||
y += y0;
|
||||
}
|
||||
|
||||
pControl1 = transform.transform(x, y);
|
||||
} else {
|
||||
if (prevCControlPoint != null) {
|
||||
pControl1 = new Point(2 * pStart.x - prevCControlPoint.x, 2 * pStart.y - prevCControlPoint.y);
|
||||
} else {
|
||||
pControl1 = pStart;
|
||||
}
|
||||
}
|
||||
|
||||
x = pathReader.readDouble();
|
||||
y = pathReader.readDouble();
|
||||
if (isRelative) {
|
||||
@@ -357,10 +392,8 @@ public class ShapeImporter {
|
||||
y += y0;
|
||||
}
|
||||
|
||||
p = transform.transform(x, y);
|
||||
int controlDeltaX1 = (int) Math.round(p.x - prevPoint.x);
|
||||
int controlDeltaY1 = (int) Math.round(p.y - prevPoint.y);
|
||||
prevPoint = p;
|
||||
Point pControl2 = transform.transform(x, y);
|
||||
prevCControlPoint = pControl2;
|
||||
|
||||
x = pathReader.readDouble();
|
||||
y = pathReader.readDouble();
|
||||
@@ -370,30 +403,35 @@ public class ShapeImporter {
|
||||
}
|
||||
|
||||
p = transform.transform(x, y);
|
||||
int controlDeltaX2 = (int) Math.round(p.x - prevPoint.x);
|
||||
int controlDeltaY2 = (int) Math.round(p.y - prevPoint.y);
|
||||
prevPoint = p;
|
||||
|
||||
x = pathReader.readDouble();
|
||||
y = pathReader.readDouble();
|
||||
if (isRelative) {
|
||||
x += x0;
|
||||
y += y0;
|
||||
//StraightEdgeRecord serc = new StraightEdgeRecord();
|
||||
//serc.generalLineFlag = true;
|
||||
//serc.deltaX = (int) Math.round(p.x - prevPoint.x);
|
||||
//serc.deltaY = (int) Math.round(p.y - prevPoint.y);
|
||||
//shapes.shapeRecords.add(serc);
|
||||
List<Double> quadCoordinates = new CubicToQuad().cubicToQuad(pStart.x, pStart.y, pControl1.x, pControl1.y, pControl2.x, pControl2.y, p.x, p.y, 0.0006);
|
||||
for (int i = 2; i < quadCoordinates.size();) {
|
||||
CurvedEdgeRecord cerc = new CurvedEdgeRecord();
|
||||
p = new Point(quadCoordinates.get(i++), quadCoordinates.get(i++));
|
||||
cerc.controlDeltaX = (int) Math.round(p.x - prevPoint.x);
|
||||
cerc.controlDeltaY = (int) Math.round(p.y - prevPoint.y);
|
||||
prevPoint = p;
|
||||
|
||||
p = new Point(quadCoordinates.get(i++), quadCoordinates.get(i++));
|
||||
cerc.anchorDeltaX = (int) Math.round(p.x - prevPoint.x);
|
||||
cerc.anchorDeltaY = (int) Math.round(p.y - prevPoint.y);
|
||||
prevPoint = p;
|
||||
shapes.shapeRecords.add(cerc);
|
||||
}
|
||||
|
||||
cer2.controlDeltaX = (controlDeltaX1 + controlDeltaX2) / 2;
|
||||
cer2.controlDeltaY = (controlDeltaY1 + controlDeltaY2) / 2;
|
||||
|
||||
p = transform.transform(x, y);
|
||||
cer2.anchorDeltaX = (int) Math.round(p.x - prevPoint.x);
|
||||
cer2.anchorDeltaY = (int) Math.round(p.y - prevPoint.y);
|
||||
prevPoint = p;
|
||||
System.out.println("C" + cer2.controlDeltaX + "," + cer2.controlDeltaY + "," + cer2.anchorDeltaX + "," + cer2.controlDeltaY);
|
||||
shapes.shapeRecords.add(cer2);
|
||||
break;
|
||||
default:
|
||||
Logger.getLogger(ShapeImporter.class.getName()).log(Level.WARNING, "Unknown command: {0}", command);
|
||||
break;
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd != 'C') {
|
||||
prevCControlPoint = null;
|
||||
}
|
||||
|
||||
x0 = x;
|
||||
@@ -421,7 +459,7 @@ public class ShapeImporter {
|
||||
}
|
||||
|
||||
scr.lineStyles = new LINESTYLEARRAY();
|
||||
Color lineColor = style.strokeColor;
|
||||
Color lineColor = style.getStrokeColorWithOpacity();
|
||||
if (lineColor != null) {
|
||||
scr.lineStyles.lineStyles = new LINESTYLE[1];
|
||||
scr.lineStyles.lineStyles[0] = shapeNum <= 3 ? new LINESTYLE() : new LINESTYLE2();
|
||||
@@ -464,6 +502,8 @@ public class ShapeImporter {
|
||||
|
||||
public Color fillColor;
|
||||
|
||||
public double opacity;
|
||||
|
||||
public double fillOpacity;
|
||||
|
||||
public Color strokeColor;
|
||||
@@ -475,6 +515,7 @@ public class ShapeImporter {
|
||||
fillOpacity = 1;
|
||||
strokeColor = null;
|
||||
strokeWidth = 1;
|
||||
opacity = 1;
|
||||
}
|
||||
|
||||
public Color getFillColorWithOpacity() {
|
||||
@@ -482,7 +523,7 @@ public class ShapeImporter {
|
||||
return null;
|
||||
}
|
||||
|
||||
int opacity = (int) Math.round(fillOpacity * 255);
|
||||
int opacity = (int) Math.round(this.opacity * fillOpacity * 255);
|
||||
if (opacity == 255) {
|
||||
return fillColor;
|
||||
}
|
||||
@@ -490,6 +531,19 @@ public class ShapeImporter {
|
||||
return new Color(fillColor.getRed(), fillColor.getGreen(), fillColor.getBlue(), opacity);
|
||||
}
|
||||
|
||||
public Color getStrokeColorWithOpacity() {
|
||||
if (strokeColor == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
int opacity = (int) Math.round(this.opacity * 255);
|
||||
if (opacity == 255) {
|
||||
return strokeColor;
|
||||
}
|
||||
|
||||
return new Color(strokeColor.getRed(), strokeColor.getGreen(), strokeColor.getBlue(), opacity);
|
||||
}
|
||||
|
||||
private SvgStyle apply(Element element) {
|
||||
SvgStyle result = new SvgStyle();
|
||||
result.fillColor = fillColor;
|
||||
@@ -521,6 +575,12 @@ public class ShapeImporter {
|
||||
result.strokeWidth = strokeWidth;
|
||||
}
|
||||
|
||||
attr = element.getAttribute("opacity");
|
||||
if (attr.length() > 0) {
|
||||
double opacity = Double.parseDouble(attr);
|
||||
result.opacity = opacity;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,340 @@
|
||||
/*
|
||||
* Copyright (C) 2010-2015 JPEXS, All rights reserved.
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU Lesser General Public
|
||||
* License as published by the Free Software Foundation; either
|
||||
* version 3.0 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This library 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
|
||||
* Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public
|
||||
* License along with this library.
|
||||
*/
|
||||
package com.jpexs.decompiler.flash.importers.svg;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Ported from https://github.com/fontello/cubic2quad
|
||||
*
|
||||
* @author JPEXS, Vitaly Puzrin
|
||||
*/
|
||||
public class CubicToQuad {
|
||||
|
||||
class Point {
|
||||
|
||||
public double x;
|
||||
|
||||
public double y;
|
||||
|
||||
public Point(double x, double y) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
|
||||
public Point add(Point point) {
|
||||
return new Point(this.x + point.x, this.y + point.y);
|
||||
}
|
||||
|
||||
public Point sub(Point point) {
|
||||
return new Point(this.x - point.x, this.y - point.y);
|
||||
}
|
||||
|
||||
public Point mul(double value) {
|
||||
return new Point(this.x * value, this.y * value);
|
||||
}
|
||||
|
||||
public Point div(double value) {
|
||||
return new Point(this.x / value, this.y / value);
|
||||
}
|
||||
|
||||
public double dist() {
|
||||
return Math.sqrt(this.x * this.x + this.y * this.y);
|
||||
}
|
||||
|
||||
public double sqr() {
|
||||
return this.x * this.x + this.y * this.y;
|
||||
}
|
||||
|
||||
public double dot(Point point) {
|
||||
return this.x * point.x + this.y * point.y;
|
||||
}
|
||||
}
|
||||
|
||||
private Point[] calcPowerCoefficients(Point p1, Point c1, Point c2, Point p2) {
|
||||
// point(t) = p1*(1-t)^3 + c1*t*(1-t)^2 + c2*t^2*(1-t) + p2*t^3 = a*t^3 + b*t^2 + c*t + d
|
||||
// for each t value, so
|
||||
// a = (p2 - p1) + 3 * (c1 - c2)
|
||||
// b = 3 * (p1 + c2) - 6 * c1
|
||||
// c = 3 * (c1 - p1)
|
||||
// d = p1
|
||||
Point a = p2.sub(p1).add(c1.sub(c2).mul(3));
|
||||
Point b = p1.add(c2).mul(3).sub(c1.mul(6));
|
||||
Point c = c1.sub(p1).mul(3);
|
||||
Point d = p1;
|
||||
return new Point[]{a, b, c, d};
|
||||
}
|
||||
|
||||
private Point calcPoint(Point a, Point b, Point c, Point d, double t) {
|
||||
// a*t^3 + b*t^2 + c*t + d = ((a*t + b)*t + c)*t + d
|
||||
return a.mul(t).add(b).mul(t).add(c).mul(t).add(d);
|
||||
}
|
||||
|
||||
private Point calcPointQuad(Point a, Point b, Point c, double t) {
|
||||
// a*t^2 + b*t + c = (a*t + b)*t + c
|
||||
return a.mul(t).add(b).mul(t).add(c);
|
||||
}
|
||||
|
||||
private Point calcPointDerivative(Point a, Point b, Point c, Point d, double t) {
|
||||
// d/dt[a*t^3 + b*t^2 + c*t + d] = 3*a*t^2 + 2*b*t + c = (3*a*t + 2*b)*t + c
|
||||
return a.mul(3 * t).add(b.mul(2)).mul(t).add(c);
|
||||
}
|
||||
|
||||
private double[] quadSolve(double a, double b, double c) {
|
||||
// a*x^2 + b*x + c = 0
|
||||
if (a == 0) {
|
||||
return (b == 0) ? new double[0] : new double[]{-c / b};
|
||||
}
|
||||
double D = b * b - 4 * a * c;
|
||||
if (D < 0) {
|
||||
return new double[0];
|
||||
} else if (D == 0) {
|
||||
return new double[]{-b / (2 * a)};
|
||||
}
|
||||
double DSqrt = Math.sqrt(D);
|
||||
return new double[]{(-b - DSqrt) / (2 * a), (-b + DSqrt) / (2 * a)};
|
||||
}
|
||||
|
||||
private double cubicRoot(double x) {
|
||||
return (x < 0) ? -Math.pow(-x, 1 / 3) : Math.pow(x, 1 / 3);
|
||||
}
|
||||
|
||||
private double[] cubicSolve(double a, double b, double c, double d) {
|
||||
// a*x^3 + b*x^2 + c*x + d = 0
|
||||
if (a == 0) {
|
||||
return quadSolve(b, c, d);
|
||||
}
|
||||
|
||||
// solve using Cardan's method, which is described in paper of R.W.D. Nickals
|
||||
// http://www.nickalls.org/dick/papers/maths/cubic1993.pdf (doi:10.2307/3619777)
|
||||
double xn = -b / (3 * a); // point of symmetry x coordinate
|
||||
double yn = ((a * xn + b) * xn + c) * xn + d; // point of symmetry y coordinate
|
||||
double deltaSq = (b * b - 3 * a * c) / (9 * a * a); // delta^2
|
||||
double hSq = 4 * a * a * Math.pow(deltaSq, 3); // h^2
|
||||
double D3 = yn * yn - hSq;
|
||||
if (D3 > 0) { // 1 real root
|
||||
double D3Sqrt = Math.sqrt(D3);
|
||||
return new double[]{xn + cubicRoot((-yn + D3Sqrt) / (2 * a)) + cubicRoot((-yn - D3Sqrt) / (2 * a))};
|
||||
} else if (D3 == 0) { // 2 real roots
|
||||
double delta1 = cubicRoot(yn / (2 * a));
|
||||
return new double[]{xn - 2 * delta1, xn + delta1};
|
||||
}
|
||||
|
||||
// 3 real roots
|
||||
double theta = Math.acos(-yn / Math.sqrt(hSq)) / 3;
|
||||
double delta = Math.sqrt(deltaSq);
|
||||
return new double[]{
|
||||
xn + 2 * delta * Math.cos(theta),
|
||||
xn + 2 * delta * Math.cos(theta + Math.PI * 2 / 3),
|
||||
xn + 2 * delta * Math.cos(theta + Math.PI * 4 / 3)
|
||||
};
|
||||
}
|
||||
|
||||
private double minDistanceToQuad(Point point, Point p1, Point c1, Point p2) {
|
||||
// f(t) = (1-t)^2 * p1 + 2*t*(1 - t) * c1 + t^2 * p2 = a*t^2 + b*t + c, t in [0, 1],
|
||||
// a = p1 + p2 - 2 * c1
|
||||
// b = 2 * (c1 - p1)
|
||||
// c = p1; a, b, c are vectors because p1, c1, p2 are vectors too
|
||||
// The distance between given point and quadratic curve is equal to
|
||||
// sqrt((f(t) - point)^2), so these expression has zero derivative by t at points where
|
||||
// (f'(t), (f(t) - point)) = 0.
|
||||
// Substituting quadratic curve as f(t) one could obtain a cubic equation
|
||||
// e3*t^3 + e2*t^2 + e1*t + e0 = 0 with following coefficients:
|
||||
// e3 = 2 * a^2
|
||||
// e2 = 3 * a*b
|
||||
// e1 = (b^2 + 2 * a*(c - point))
|
||||
// e0 = (c - point)*b
|
||||
// One of the roots of the equation from [0, 1], or t = 0 or t = 1 is a value of t
|
||||
// at which the distance between given point and quadratic Bezier curve has minimum.
|
||||
// So to find the minimal distance one have to just pick the minimum value of
|
||||
// the distance on set {t = 0 | t = 1 | t is root of the equation from [0, 1] }.
|
||||
|
||||
Point a = p1.add(p2).sub(c1.mul(2));
|
||||
Point b = c1.sub(p1).mul(2);
|
||||
Point c = p1;
|
||||
double e3 = 2 * a.sqr();
|
||||
double e2 = 3 * a.dot(b);
|
||||
double e1 = (b.sqr() + 2 * a.dot(c.sub(point)));
|
||||
double e0 = c.sub(point).dot(b);
|
||||
double[] solveResult = cubicSolve(e3, e2, e1, e0);
|
||||
List<Double> candidates = new ArrayList<>();
|
||||
for (double t : solveResult) {
|
||||
if (t > 0 && t < 1) {
|
||||
candidates.add(t);
|
||||
}
|
||||
}
|
||||
|
||||
candidates.add(0d);
|
||||
candidates.add(1d);
|
||||
|
||||
double minDistance = 1e9;
|
||||
for (int i = 0; i < candidates.size(); i++) {
|
||||
double distance = calcPointQuad(a, b, c, candidates.get(i)).sub(point).dist();
|
||||
if (distance < minDistance) {
|
||||
minDistance = distance;
|
||||
}
|
||||
}
|
||||
return minDistance;
|
||||
}
|
||||
|
||||
private Point[] processSegment(Point a, Point b, Point c, Point d, double t1, double t2) {
|
||||
// Find a single control point for given segment of cubic Bezier curve
|
||||
// These control point is an interception of tangent lines to the boundary points
|
||||
// Let's denote that f(t) is a vector function of parameter t that defines the cubic Bezier curve,
|
||||
// f(t1) + f'(t1)*z1 is a parametric equation of tangent line to f(t1) with parameter z1
|
||||
// f(t2) + f'(t2)*z2 is the same for point f(t2) and the vector equation
|
||||
// f(t1) + f'(t1)*z1 = f(t2) + f'(t2)*z2 defines the values of parameters z1 and z2.
|
||||
// Defining fx(t) and fy(t) as the x and y components of vector function f(t) respectively
|
||||
// and solving the given system for z1 one could obtain that
|
||||
//
|
||||
// -(fx(t2) - fx(t1))*fy'(t2) + (fy(t2) - fy(t1))*fx'(t2)
|
||||
// z1 = ------------------------------------------------------.
|
||||
// -fx'(t1)*fy'(t2) + fx'(t2)*fy'(t1)
|
||||
//
|
||||
// Let's assign letter D to the denominator and note that if D = 0 it means that the curve actually
|
||||
// is a line. Substituting z1 to the equation of tangent line to the point f(t1), one could obtain that
|
||||
// cx = [fx'(t1)*(fy(t2)*fx'(t2) - fx(t2)*fy'(t2)) + fx'(t2)*(fx(t1)*fy'(t1) - fy(t1)*fx'(t1))]/D
|
||||
// cy = [fy'(t1)*(fy(t2)*fx'(t2) - fx(t2)*fy'(t2)) + fy'(t2)*(fx(t1)*fy'(t1) - fy(t1)*fx'(t1))]/D
|
||||
// where c = (cx, cy) is the control point of quadratic Bezier curve.
|
||||
|
||||
Point f1 = calcPoint(a, b, c, d, t1);
|
||||
Point f2 = calcPoint(a, b, c, d, t2);
|
||||
Point f1_ = calcPointDerivative(a, b, c, d, t1);
|
||||
Point f2_ = calcPointDerivative(a, b, c, d, t2);
|
||||
|
||||
double D = -f1_.x * f2_.y + f2_.x * f1_.y;
|
||||
if (Math.abs(D) < 1e-8) {
|
||||
return new Point[]{f1, f1.add(f2).div(2), f2}; // straight line segment
|
||||
}
|
||||
double cx = (f1_.x * (f2.y * f2_.x - f2.x * f2_.y) + f2_.x * (f1.x * f1_.y - f1.y * f1_.x)) / D;
|
||||
double cy = (f1_.y * (f2.y * f2_.x - f2.x * f2_.y) + f2_.y * (f1.x * f1_.y - f1.y * f1_.x)) / D;
|
||||
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
|
||||
// p1, c1, p2 define quadratic curve
|
||||
// errorBound is maximum allowed distance
|
||||
// Try to find maximum distance between one of N points segment of given cubic
|
||||
// and corresponding quadratic curve that estimates the cubic one, assuming
|
||||
// that the boundary points of cubic and quadratic points are equal.
|
||||
//
|
||||
// The distance calculation method comes from Hausdorff distance defenition
|
||||
// (https://en.wikipedia.org/wiki/Hausdorff_distance), but with following simplifications
|
||||
// * it looks for maximum distance only for finite number of points of cubic curve
|
||||
// * it doesn't perform reverse check that means selecting set of fixed points on
|
||||
// the quadratic curve and looking for the closest points on the cubic curve
|
||||
// But this method allows easy estimation of approximation error, so it is enough
|
||||
// for practical purposes.
|
||||
|
||||
int n = 10; // number of points + 1
|
||||
double dt = (tmax - tmin) / n;
|
||||
for (double t = tmin + dt; t < tmax - dt; t += dt) { // don't check distance on boundary points
|
||||
// because they should be the same
|
||||
Point point = calcPoint(a, b, c, d, t);
|
||||
if (minDistanceToQuad(point, p1, c1, p2) > errorBound) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean _isApproximationClose(Point a, Point b, Point c, Point d, List<Point[]> quadCurves, double errorBound) {
|
||||
double dt = 1 / quadCurves.size();
|
||||
for (int i = 0; i < quadCurves.size(); i++) {
|
||||
Point p1 = quadCurves.get(i)[0];
|
||||
Point c1 = quadCurves.get(i)[1];
|
||||
Point p2 = quadCurves.get(i)[2];
|
||||
if (!isSegmentApproximationClose(a, b, c, d, i * dt, (i + 1) * dt, p1, c1, p2, errorBound)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private List<Point[]> fromFlatArray(double[] points) {
|
||||
List<Point[]> result = new ArrayList<>();
|
||||
int segmentsNumber = (points.length - 2) / 4;
|
||||
for (int i = 0; i < segmentsNumber; i++) {
|
||||
result.add(new Point[]{
|
||||
new Point(points[4 * i], points[4 * i + 1]),
|
||||
new Point(points[4 * i + 2], points[4 * i + 3]),
|
||||
new Point(points[4 * i + 4], points[4 * i + 5])
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private List<Double> toFlatArray(List<Point[]> quadsList) {
|
||||
List<Double> result = new ArrayList<>();
|
||||
result.add(quadsList.get(0)[0].x);
|
||||
result.add(quadsList.get(0)[0].y);
|
||||
for (int i = 0; i < quadsList.size(); i++) {
|
||||
result.add(quadsList.get(i)[1].x);
|
||||
result.add(quadsList.get(i)[1].y);
|
||||
result.add(quadsList.get(i)[2].x);
|
||||
result.add(quadsList.get(i)[2].y);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private boolean isApproximationClose(double p1x, double p1y, double c1x, double c1y, double c2x, double c2y, double p2x, double p2y, double[] quads, double errorBound) {
|
||||
// TODO: rewrite it in C-style and remove _isApproximationClose
|
||||
Point[] pc = calcPowerCoefficients(
|
||||
new Point(p1x, p1y),
|
||||
new Point(c1x, c1y),
|
||||
new Point(c2x, c2y),
|
||||
new Point(p2x, p2y)
|
||||
);
|
||||
return _isApproximationClose(pc[0], pc[1], pc[2], pc[3], fromFlatArray(quads), errorBound);
|
||||
}
|
||||
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
public 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);
|
||||
Point p2 = new Point(p2x, p2y);
|
||||
Point[] pc = calcPowerCoefficients(p1, c1, c2, p2);
|
||||
Point a = pc[0], b = pc[1], c = pc[2], d = pc[3];
|
||||
|
||||
List<Point[]> approximation = new ArrayList<>();
|
||||
for (int segmentsCount = 1; segmentsCount <= 8; segmentsCount++) {
|
||||
approximation.clear();
|
||||
for (double t = 0; t < 1; t += 1.0 / segmentsCount) {
|
||||
approximation.add(processSegment(a, b, c, d, t, t + 1.0 / segmentsCount));
|
||||
}
|
||||
if (segmentsCount == 1 && (approximation.get(0)[1].sub(p1).dot(c1.sub(p1)) < 0
|
||||
|| approximation.get(0)[1].sub(p2).dot(c2.sub(p2)) < 0)) {
|
||||
// approximation concave, while the curve is convex (or vice versa)
|
||||
continue;
|
||||
}
|
||||
if (_isApproximationClose(a, b, c, d, approximation, errorBound)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return toFlatArray(approximation);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user