317 lines
10 KiB
C++
317 lines
10 KiB
C++
/*
|
|
* This file is part of gerbolyze, a vector image preprocessing toolchain
|
|
* Copyright (C) 2021 Jan Sebastian Götte <gerbolyze@jaseg.de>
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU Affero 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 Affero General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Affero General Public License
|
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#pragma once
|
|
|
|
#include <array>
|
|
#include <string>
|
|
#include <sstream>
|
|
#include <iomanip>
|
|
#include <cmath>
|
|
#include <algorithm>
|
|
|
|
#include <clipper.hpp>
|
|
|
|
#include "svg_import_defs.h"
|
|
|
|
using namespace std;
|
|
|
|
namespace gerbolyze {
|
|
|
|
typedef std::array<double, 2> d2p;
|
|
typedef std::vector<d2p> Polygon;
|
|
|
|
typedef std::array<int64_t, 2> i2p;
|
|
typedef std::vector<i2p> Polygon_i;
|
|
|
|
class xform2d {
|
|
public:
|
|
xform2d(double xx, double xy, double yx, double yy, double x0=0.0, double y0=0.0) :
|
|
xx(xx), xy(xy), x0(x0), yx(yx), yy(yy), y0(y0) {}
|
|
|
|
xform2d() : xform2d(1.0, 0.0, 0.0, 1.0) {}
|
|
|
|
xform2d(const string &svg_transform) : xform2d() {
|
|
if (svg_transform.empty())
|
|
return;
|
|
|
|
string start("matrix(");
|
|
if (svg_transform.substr(0, start.length()) != start)
|
|
return;
|
|
if (svg_transform.back() != ')')
|
|
return;
|
|
|
|
const string &foo = svg_transform.substr(start.length(), svg_transform.length());
|
|
const string &bar = foo.substr(0, foo.length() - 1);
|
|
|
|
istringstream xform(bar);
|
|
|
|
double a, c, e,
|
|
b, d, f;
|
|
xform >> a >> b >> c >> d >> e >> f;
|
|
if (xform.fail())
|
|
return;
|
|
|
|
xx=a, yx=b, xy=c, yy=d, x0=e, y0=f;
|
|
//cerr << "xform loaded " << dbg_str() << endl;
|
|
}
|
|
|
|
xform2d &translate(double x, double y) {
|
|
xform2d xf(1, 0, 0, 1, x, y);
|
|
transform(xf);
|
|
return *this;
|
|
}
|
|
|
|
xform2d &scale(double x, double y) {
|
|
xform2d xf(x, 0, 0, y);
|
|
transform(xf);
|
|
return *this;
|
|
}
|
|
|
|
xform2d &rotate(double theta) {
|
|
double s = sin(theta);
|
|
double c = cos(theta);
|
|
xform2d xf(c, -s, s, c);
|
|
transform(xf);
|
|
return *this;
|
|
}
|
|
|
|
xform2d &skew(double m) {
|
|
xform2d xf(1, m, 0, 1);
|
|
transform(xf);
|
|
return *this;
|
|
}
|
|
|
|
xform2d &transform(const xform2d &other) {
|
|
double n_xx = other.xx * xx + other.yx * xy;
|
|
double n_yx = other.xx * yx + other.yx * yy;
|
|
|
|
double n_xy = other.xy * xx + other.yy * xy;
|
|
double n_yy = other.xy * yx + other.yy * yy;
|
|
|
|
double n_x0 = other.x0 * xx + other.y0 * xy + x0;
|
|
double n_y0 = other.x0 * yx + other.y0 * yy + y0;
|
|
|
|
xx = n_xx;
|
|
yx = n_yx;
|
|
xy = n_xy;
|
|
yy = n_yy;
|
|
x0 = n_x0;
|
|
y0 = n_y0;
|
|
decomposed = false;
|
|
|
|
return *this;
|
|
};
|
|
|
|
double doc2phys_dist(double dist_doc) {
|
|
return dist_doc * sqrt(xx*xx + xy*xy);
|
|
}
|
|
|
|
double phys2doc_dist(double dist_doc) {
|
|
return dist_doc / sqrt(xx*xx + xy*xy);
|
|
}
|
|
|
|
std::tuple<double, double, double, double> decompose() {
|
|
/* FIXME unit tests, especially for degenerate cases! */
|
|
if (decomposed) {
|
|
return {s_x, s_y, m, theta};
|
|
}
|
|
|
|
/* https://math.stackexchange.com/a/3521141 */
|
|
/* https://stackoverflow.com/a/70381885 */
|
|
/* xx xy x0
|
|
* yx yy y0 */
|
|
s_x = sqrt(xx*xx + yx*yx);
|
|
|
|
if (xx == 0 && yx == 0) {
|
|
theta = 0;
|
|
} else {
|
|
theta = atan2(yx, xx);
|
|
}
|
|
|
|
double f = (xx*yy - xy*yx);
|
|
|
|
if (f == 0) {
|
|
m = 0;
|
|
} else {
|
|
m = (xx*xy + yy*yx) / f;
|
|
}
|
|
|
|
f = xx + m*yx;
|
|
if (fabs(f) < 1e-12) {
|
|
f = m*xx - yx;
|
|
if (fabs(f) < 1e-12) {
|
|
s_y = 0;
|
|
} else {
|
|
s_y = xy*s_x / f;
|
|
}
|
|
} else {
|
|
s_y = yy*s_x / f;
|
|
}
|
|
|
|
double b = sqrt(s_y*s_y + m*m);
|
|
f_min = fmin(s_x, b);
|
|
f_max = fmax(s_x, b);
|
|
|
|
decomposed = true;
|
|
return {s_x, s_y, m, theta};
|
|
}
|
|
|
|
bool doc2phys_skew_ok(double dist_doc, double rel_tol, double abs_tol) {
|
|
decompose();
|
|
|
|
if (f_min == 0) {
|
|
return false;
|
|
}
|
|
|
|
double imbalance = f_max / f_min - 1.0;
|
|
//cerr << " * skew check: " << dbg_str();
|
|
//cerr << " imbalance=" << imbalance << endl;
|
|
//cerr << " rel=" << (imbalance < rel_tol) << " abs=" << (imbalance*fabs(dist_doc) < abs_tol) << endl;
|
|
return imbalance < rel_tol && imbalance*fabs(dist_doc) < abs_tol;
|
|
}
|
|
|
|
double doc2phys_min(double dist_doc) {
|
|
decompose();
|
|
return dist_doc * f_min;
|
|
}
|
|
|
|
double doc2phys_max(double dist_doc) {
|
|
decompose();
|
|
return dist_doc * f_max;
|
|
}
|
|
|
|
double phys2doc_min(double dist_doc) {
|
|
decompose();
|
|
|
|
if (f_min == 0)
|
|
return std::nan("9");
|
|
|
|
return dist_doc / f_min;
|
|
}
|
|
|
|
double phys2doc_max(double dist_doc) {
|
|
decompose();
|
|
|
|
if (f_max == 0)
|
|
return std::nan("9");
|
|
|
|
return dist_doc / f_max;
|
|
}
|
|
|
|
d2p doc2phys(const d2p p) {
|
|
return d2p {
|
|
xx * p[0] + xy * p[1] + x0,
|
|
yx * p[0] + yy * p[1] + y0
|
|
};
|
|
}
|
|
|
|
xform2d &invert(bool *success_out=nullptr) {
|
|
/* From Cairo source */
|
|
|
|
/* inv (A) = 1/det (A) * adj (A) */
|
|
double det = xx*yy - yx*xy;
|
|
|
|
if (det == 0 || !isfinite(det)) {
|
|
if (success_out)
|
|
*success_out = false;
|
|
*this = xform2d(); /* unity matrix */
|
|
return *this;
|
|
}
|
|
|
|
*this = xform2d(yy/det, -yx/det,
|
|
-xy/det, xx/det,
|
|
(xy*y0 - yy*x0)/det, (yx*x0 - xx*y0)/det);
|
|
|
|
if (success_out)
|
|
*success_out = true;
|
|
|
|
return *this;
|
|
}
|
|
|
|
/* Transform given clipper paths */
|
|
void doc2phys_clipper(ClipperLib::Paths &paths) {
|
|
for (auto &p : paths) {
|
|
doc2phys_clipper(p);
|
|
}
|
|
}
|
|
|
|
void doc2phys_clipper(ClipperLib::Path &path) {
|
|
std::transform(path.begin(), path.end(), path.begin(),
|
|
[this](ClipperLib::IntPoint p) -> ClipperLib::IntPoint {
|
|
d2p out(this->doc2phys(d2p{p.X / clipper_scale, p.Y / clipper_scale}));
|
|
return {
|
|
(ClipperLib::cInt)round(out[0] * clipper_scale),
|
|
(ClipperLib::cInt)round(out[1] * clipper_scale)
|
|
};
|
|
});
|
|
}
|
|
|
|
/* Transform given clipper paths */
|
|
void phys2doc_clipper(ClipperLib::Paths &paths) {
|
|
for (auto &p : paths) {
|
|
phys2doc_clipper(p);
|
|
}
|
|
}
|
|
|
|
void phys2doc_clipper(ClipperLib::Path &path) {
|
|
xform2d copy(*this);
|
|
bool inverted = false;
|
|
copy.invert(&inverted);
|
|
if (!inverted) {
|
|
path.clear();
|
|
return;
|
|
}
|
|
|
|
std::transform(path.begin(), path.end(), path.begin(),
|
|
[this, ©](ClipperLib::IntPoint p) -> ClipperLib::IntPoint {
|
|
d2p out(copy.doc2phys(d2p{p.X / clipper_scale, p.Y / clipper_scale}));
|
|
return {
|
|
(ClipperLib::cInt)round(out[0] * clipper_scale),
|
|
(ClipperLib::cInt)round(out[1] * clipper_scale)
|
|
};
|
|
});
|
|
}
|
|
|
|
void transform_polygon(Polygon &poly) {
|
|
std::transform(poly.begin(), poly.end(), poly.begin(),
|
|
[this](d2p p) -> d2p {
|
|
return this->doc2phys(d2p{p[0], p[1]});
|
|
});
|
|
}
|
|
|
|
string dbg_str() {
|
|
decompose();
|
|
ostringstream os;
|
|
os << "xform2d< " << setw(5);
|
|
os << xx << ", " << xy << ", " << x0 << " / ";
|
|
os << yy << ", " << yx << ", " << y0 << " / ";
|
|
os << "θ=" << theta << ", m=" << m << " s=(" << s_x << ", " << s_y << " | ";
|
|
os << "f_min=" << f_min << ", f_max=" << f_max;
|
|
os << " >";
|
|
return os.str();
|
|
}
|
|
|
|
private:
|
|
double xx, xy, x0,
|
|
yx, yy, y0;
|
|
double theta, m, s_x, s_y;
|
|
double f_min, f_max;
|
|
bool decomposed = false;
|
|
};
|
|
}
|