ArcsToCubics.java
/* ========================================================================
* PlantUML : a free UML diagram generator
* ========================================================================
*
* (C) Copyright 2009-2025, Arnaud Roques
*
* Project Info: https://plantuml.com
*
* If you like this project or if you find it useful, you can support us at:
*
* https://plantuml.com/patreon (only 1$ per month!)
* https://plantuml.com/paypal
*
* This file is part of PlantUML.
*
* PlantUML 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.
*
* PlantUML 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 library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
* USA.
*
*
* Original Author: Arnaud Roques
*
*/
package net.sourceforge.plantuml.klimt.drawing.raster;
/**
* A {@link RasterPath.Visitor} for canvases that only know cubic Bézier curves:
* it turns the arcs into cubics (at most a quarter of a turn each), and keeps
* track of the current point, which SVG arcs start from.
*/
public abstract class ArcsToCubics implements RasterPath.Visitor {
private double currentX;
private double currentY;
private double startX;
private double startY;
/** Segments drawn by the canvas, once the arcs are turned into cubics. */
protected abstract void doMoveTo(double x, double y);
protected abstract void doLineTo(double x, double y);
protected abstract void doCubicTo(double x1, double y1, double x2, double y2, double x3, double y3);
protected abstract void doClosePath();
@Override
public final void moveTo(double x, double y) {
doMoveTo(x, y);
currentX = startX = x;
currentY = startY = y;
}
@Override
public final void lineTo(double x, double y) {
doLineTo(x, y);
currentX = x;
currentY = y;
}
@Override
public final void cubicTo(double x1, double y1, double x2, double y2, double x3, double y3) {
doCubicTo(x1, y1, x2, y2, x3, y3);
currentX = x3;
currentY = y3;
}
@Override
public final void closePath() {
doClosePath();
currentX = startX;
currentY = startY;
}
/**
* An open arc as {@code Arc2D}: the point at angle a is (cx + rx cos a, cy - ry
* sin a), angles in degrees.
*/
@Override
public final void arc(double x, double y, double width, double height, double start, double extent) {
final double rx = width / 2;
final double ry = height / 2;
final double cx = x + rx;
final double cy = y + ry;
final double a0 = Math.toRadians(start);
final double delta = Math.toRadians(extent);
moveTo(cx + rx * Math.cos(a0), cy - ry * Math.sin(a0));
final int n = Math.max(1, (int) Math.ceil(Math.abs(delta) / (Math.PI / 2) - 1e-9));
final double step = delta / n;
final double k = 4.0 / 3.0 * Math.tan(step / 4);
for (int i = 0; i < n; i++) {
final double t0 = a0 + i * step;
final double t1 = t0 + step;
final double x0 = cx + rx * Math.cos(t0);
final double y0 = cy - ry * Math.sin(t0);
final double x1 = cx + rx * Math.cos(t1);
final double y1 = cy - ry * Math.sin(t1);
// Derivatives of the parametric arc at t0 and t1
final double dx0 = -rx * Math.sin(t0);
final double dy0 = -ry * Math.cos(t0);
final double dx1 = -rx * Math.sin(t1);
final double dy1 = -ry * Math.cos(t1);
cubicTo(x0 + k * dx0, y0 + k * dy0, x1 - k * dx1, y1 - k * dy1, x1, y1);
}
}
/**
* An SVG elliptical arc from the current point (endpoint to center
* parameterization of the SVG specification, appendix F.6.5).
*/
@Override
public final void arcTo(double rx, double ry, double rotation, boolean largeArc, boolean sweep, double x,
double y) {
final double x1 = currentX;
final double y1 = currentY;
if (x1 == x && y1 == y)
return;
rx = Math.abs(rx);
ry = Math.abs(ry);
if (rx == 0 || ry == 0) {
lineTo(x, y);
return;
}
final double phi = Math.toRadians(rotation % 360);
final double cos = Math.cos(phi);
final double sin = Math.sin(phi);
final double dx2 = (x1 - x) / 2;
final double dy2 = (y1 - y) / 2;
final double x1p = cos * dx2 + sin * dy2;
final double y1p = -sin * dx2 + cos * dy2;
// Radii too small: scale them up
final double lambda = (x1p * x1p) / (rx * rx) + (y1p * y1p) / (ry * ry);
if (lambda > 1) {
rx *= Math.sqrt(lambda);
ry *= Math.sqrt(lambda);
}
final double numerator = rx * rx * ry * ry - rx * rx * y1p * y1p - ry * ry * x1p * x1p;
final double denominator = rx * rx * y1p * y1p + ry * ry * x1p * x1p;
double coef = denominator == 0 ? 0 : Math.sqrt(Math.max(0, numerator / denominator));
if (largeArc == sweep)
coef = -coef;
final double cxp = coef * rx * y1p / ry;
final double cyp = -coef * ry * x1p / rx;
final double cx = cos * cxp - sin * cyp + (x1 + x) / 2;
final double cy = sin * cxp + cos * cyp + (y1 + y) / 2;
final double theta1 = angle(1, 0, (x1p - cxp) / rx, (y1p - cyp) / ry);
double delta = angle((x1p - cxp) / rx, (y1p - cyp) / ry, (-x1p - cxp) / rx, (-y1p - cyp) / ry);
if (sweep == false && delta > 0)
delta -= 2 * Math.PI;
else if (sweep && delta < 0)
delta += 2 * Math.PI;
final int n = Math.max(1, (int) Math.ceil(Math.abs(delta) / (Math.PI / 2) - 1e-9));
final double step = delta / n;
final double k = 4.0 / 3.0 * Math.tan(step / 4);
for (int i = 0; i < n; i++) {
final double t0 = theta1 + i * step;
final double t1 = t0 + step;
final double ex0 = rx * Math.cos(t0);
final double ey0 = ry * Math.sin(t0);
final double ex1 = rx * Math.cos(t1);
final double ey1 = ry * Math.sin(t1);
final double dx0 = -rx * Math.sin(t0);
final double dy0 = ry * Math.cos(t0);
final double dx1 = -rx * Math.sin(t1);
final double dy1 = ry * Math.cos(t1);
final double px0 = cx + cos * ex0 - sin * ey0;
final double py0 = cy + sin * ex0 + cos * ey0;
final double px1 = i == n - 1 ? x : cx + cos * ex1 - sin * ey1;
final double py1 = i == n - 1 ? y : cy + sin * ex1 + cos * ey1;
cubicTo(px0 + k * (cos * dx0 - sin * dy0), py0 + k * (sin * dx0 + cos * dy0),
px1 - k * (cos * dx1 - sin * dy1), py1 - k * (sin * dx1 + cos * dy1), px1, py1);
}
}
// Signed angle from vector u to vector v
private static double angle(double ux, double uy, double vx, double vy) {
return Math.atan2(ux * vy - uy * vx, ux * vx + uy * vy);
}
}