initial commit
This commit is contained in:
commit
13f3f9e7ae
5 changed files with 479 additions and 0 deletions
18
LICENSE.md
Normal file
18
LICENSE.md
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
Copyright (c) 2015 Jeff Epler
|
||||
|
||||
This software is provided 'as-is', without any express or implied
|
||||
warranty. In no event will the authors be held liable for any damages
|
||||
arising from the use of this software.
|
||||
|
||||
Permission is granted to anyone to use this software for any purpose,
|
||||
including commercial applications, and to alter it and redistribute it
|
||||
freely, subject to the following restrictions:
|
||||
|
||||
1. The origin of this software must not be misrepresented; you must not
|
||||
claim that you wrote the original software. If you use this software
|
||||
in a product, an acknowledgement in the product documentation would be
|
||||
appreciated but is not required.
|
||||
2. Altered source versions must be plainly marked as such, and must not be
|
||||
misrepresented as being the original software.
|
||||
3. This notice may not be removed or altered from any source distribution.
|
||||
|
||||
4
Makefile
Normal file
4
Makefile
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
default: dashing
|
||||
|
||||
dashing: main.c dashing.h
|
||||
g++ -W -Wall -O2 -g -std=c++11 $(filter %.c, $^) -o $@ -lboost_random
|
||||
54
README.md
Normal file
54
README.md
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
# dashing - a library for autocad-style hatch patterns
|
||||
|
||||
License: permissive (zlib); see source files for additional details.
|
||||
|
||||
On my core i5, it runs at over 1 million dashes per second.
|
||||
|
||||
## API
|
||||
|
||||
`struct Point`:
|
||||
Has double-precision x and y coordinates.
|
||||
|
||||
`struct Segment`:
|
||||
A line segment defined by two endpoints. Segments are used both to
|
||||
define the boundary of the region to hatch, and to express the
|
||||
coordinates of the individual dots and dashes in the resulting hatch.
|
||||
|
||||
`HatchPattern::FromFile(std::istream fi, double scale)`:
|
||||
Read an autocad-style hatch pattern file from an opened stream
|
||||
|
||||
`HatchPattern::FromFile(const char *filename, double scale)`:
|
||||
Read an autocad-style hatch pattern from the named file
|
||||
|
||||
`xyhatch(const HatchPattern&, It start, It end, Cb cb)`:
|
||||
Iterators start..end define a range of segments, which must define a
|
||||
set of closed contours. For each dash or dot in the resulting hatch, Cb
|
||||
is called with the output segment.
|
||||
|
||||
`xyhatch(const HatchPattern&, const C &segments, Cb cb)`:
|
||||
The container C holds segments which must define a set of closed
|
||||
contours. For each dash or dot in the resulting hatch, Cb is called
|
||||
with the output segment.
|
||||
|
||||
Other items in the header file are implementation details.
|
||||
|
||||
## Demo program
|
||||
|
||||
The demo program, which compiles to `dashing` with `make`, reads a dash
|
||||
pattern file and a segment list file and produces a svg file on the output
|
||||
which visualizes the result of the hatch operation.
|
||||
|
||||
A segment list file consists of a closed contour on each line specified as a series of x,y coordinates. For instance, this segment list is a simple box:
|
||||
```
|
||||
-100 -100 100 -100 100 100 -100 100
|
||||
```
|
||||
The first point is `-100 -100`.
|
||||
|
||||
It accepts several commandline parameters:
|
||||
|
||||
-b: Benchmark mode: print only the number of dashes that would have been
|
||||
generated
|
||||
|
||||
-j: Apply a jitter to all coordinaes in the segment list file
|
||||
|
||||
-s: scale the dash pattern file by a given factor
|
||||
268
dashing.h
Normal file
268
dashing.h
Normal file
|
|
@ -0,0 +1,268 @@
|
|||
/*
|
||||
Copyright (c) 2015 Jeff Epler
|
||||
|
||||
This software is provided 'as-is', without any express or implied
|
||||
warranty. In no event will the authors be held liable for any damages
|
||||
arising from the use of this software.
|
||||
|
||||
Permission is granted to anyone to use this software for any purpose,
|
||||
including commercial applications, and to alter it and redistribute it
|
||||
freely, subject to the following restrictions:
|
||||
|
||||
1. The origin of this software must not be misrepresented; you must not
|
||||
claim that you wrote the original software. If you use this software
|
||||
in a product, an acknowledgement in the product documentation would be
|
||||
appreciated but is not required.
|
||||
2. Altered source versions must be plainly marked as such, and must not be
|
||||
misrepresented as being the original software.
|
||||
3. This notice may not be removed or altered from any source distribution.
|
||||
*/
|
||||
|
||||
#ifndef DASHING_H
|
||||
#define DASHING_H
|
||||
|
||||
#include <cmath>
|
||||
#include <cassert>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <sstream>
|
||||
#include <fstream>
|
||||
#include <stdexcept>
|
||||
#include <boost/algorithm/string/replace.hpp>
|
||||
#include <boost/algorithm/string/trim.hpp>
|
||||
#include <boost/random.hpp>
|
||||
#include <boost/random/random_device.hpp>
|
||||
|
||||
struct PSMatrix {
|
||||
double a, b, c, d, e, f;
|
||||
|
||||
PSMatrix inverse() const {
|
||||
auto i = 1. / (a * d - b * c);
|
||||
return PSMatrix{d*i, -b*i, -c*i, a*i, i*(c*f-e*d), i*(b*e-a*f)};
|
||||
}
|
||||
};
|
||||
|
||||
inline PSMatrix Translation(double x, double y) {
|
||||
PSMatrix r{1.,0.,0.,1.,x,y};
|
||||
return r;
|
||||
}
|
||||
|
||||
inline PSMatrix Rotation(double theta) {
|
||||
double c = cos(theta), s = sin(theta);
|
||||
PSMatrix r{c,s,-s,c,0.,0.};
|
||||
return r;
|
||||
}
|
||||
|
||||
inline PSMatrix XSkew(double xk) {
|
||||
PSMatrix r{1.,0.,xk,1.,0.,0.};
|
||||
return r;
|
||||
}
|
||||
|
||||
inline PSMatrix YScale(double ys) {
|
||||
PSMatrix r{1.,0.,0.,ys,0.,0.};
|
||||
return r;
|
||||
}
|
||||
|
||||
struct Point { double x, y; };
|
||||
Point operator*(const Point &p, const PSMatrix &m) {
|
||||
Point r{ p.x*m.a + p.y*m.c + m.e,
|
||||
p.x*m.b + p.y*m.d + m.f };
|
||||
return r;
|
||||
}
|
||||
|
||||
PSMatrix operator*(const PSMatrix &m1, const PSMatrix m2) {
|
||||
PSMatrix r{
|
||||
m2.a * m1.a + m2.b * m1.c,
|
||||
m2.a * m1.b + m2.b * m1.d,
|
||||
m2.c * m1.a + m2.d * m1.c,
|
||||
m2.c * m1.b + m2.d * m1.d,
|
||||
m2.e * m1.a + m2.f * m1.c + m1.e,
|
||||
m2.e * m1.b + m2.f * m1.d + m1.f};
|
||||
return r;
|
||||
}
|
||||
|
||||
std::vector<double> parse_numbers(std::string line) {
|
||||
boost::algorithm::replace_all(line, ",", " ");
|
||||
std::istringstream fi(line);
|
||||
std::vector<double> result;
|
||||
double d;
|
||||
while((fi >> d)) result.push_back(d);
|
||||
return result;
|
||||
}
|
||||
|
||||
double radians(double degrees) { return degrees * acos(0) / 90.; }
|
||||
|
||||
struct Dash {
|
||||
PSMatrix tr, tf;
|
||||
std::vector<double> dash, sum;
|
||||
|
||||
template<class It>
|
||||
Dash(double th, double x0, double y0, double dx, double dy,
|
||||
It dbegin, It dend) : dash(dbegin, dend) {
|
||||
auto s = 0.;
|
||||
for(auto d : dash) { sum.push_back(s); s += fabs(d); }
|
||||
sum.push_back(s);
|
||||
|
||||
tr = Translation(x0, y0) * Rotation(th) * XSkew(dx / dy) * YScale(dy);
|
||||
tf = tr.inverse();
|
||||
}
|
||||
|
||||
static Dash FromString(const std::string &line, double scale) {
|
||||
std::vector<double> words = parse_numbers(line);
|
||||
if(words.size() < 5)
|
||||
throw std::invalid_argument("not a valid dash specification");
|
||||
for(auto i = words.begin() + 1; i != words.end(); i++) *i *= scale;
|
||||
return Dash(radians(words.at(0)), words.at(1), words.at(2), words.at(3),
|
||||
words.at(4), words.begin() + 5, words.end());
|
||||
}
|
||||
};
|
||||
|
||||
// "sort" a segment so that its first component has the lower y-value
|
||||
struct Segment { Point p, q; };
|
||||
void ysort(Segment &s) {
|
||||
if(s.p.y < s.q.y) return;
|
||||
std::swap(s.p, s.q);
|
||||
}
|
||||
|
||||
double intceil(double x) { return int(ceil(x)); }
|
||||
double intfloor(double x) { return int(floor(x)); }
|
||||
|
||||
double pythonmod(double a, double b) {
|
||||
auto r = a - floor(a / b) * b;
|
||||
if(r == b) return 0;
|
||||
return r;
|
||||
}
|
||||
|
||||
double utoidx(const Dash &d, double u, double &o) {
|
||||
u = pythonmod(u, d.sum.back());
|
||||
for(size_t i = 1; i != d.sum.size(); i++) {
|
||||
if(u < d.sum[i]) { o = u - d.sum[i-1]; return i-1; }
|
||||
}
|
||||
abort(); // should be unreachable
|
||||
}
|
||||
|
||||
template<class Cb>
|
||||
void uvdraw(const Dash &pattern, double v, double u1, double u2, Cb cb) {
|
||||
if(pattern.dash.empty()) { cb(v, u1, u2); return; }
|
||||
double o;
|
||||
auto i = utoidx(pattern, u1, o);
|
||||
for(auto u = u1; u < u2;) {
|
||||
auto pi = pattern.dash[i];
|
||||
if(pi >= 0) cb(v, u, std::min(u2, u+pi-o));
|
||||
u += fabs(pi)-o;
|
||||
o = 0;
|
||||
i = pythonmod(i+1, pattern.dash.size());
|
||||
}
|
||||
}
|
||||
|
||||
template<class Cb>
|
||||
void uvspans(const Dash &pattern, std::vector<Segment> && segments, Cb cb) {
|
||||
if(segments.empty()) return; // no segments
|
||||
|
||||
for(auto &s : segments) ysort(s);
|
||||
std::sort(segments.begin(), segments.end(),
|
||||
[](const Segment &a, const Segment &b) {
|
||||
return b.p.y < a.p.y; // sort in decreasing p.y
|
||||
});
|
||||
|
||||
// we want to maintain the heap condition in such a way that we can always
|
||||
// quickly pop items that our span has moved past.
|
||||
// C++ heaps are max-heaps, so we need a decreasing sort.
|
||||
auto heapcmp = [](const Segment &a, const Segment &b) {
|
||||
return b.q.y < a.q.y; // sort in decreasing q.y;
|
||||
};
|
||||
|
||||
std::vector<Segment> active;
|
||||
|
||||
auto vstart = intfloor(segments.back().p.y);
|
||||
auto vend = intceil(std::max_element(segments.begin(), segments.end(),
|
||||
[](const Segment &a, const Segment &b) {
|
||||
return a.p.y < b.p.y; // sort in increasing q.y;
|
||||
})->q.y);
|
||||
|
||||
// sweep-line algorithm to intersects spans with segments
|
||||
// "active" holds segments that may intersect with this span;
|
||||
// when v moves below an active segment, drop it from the active heap.
|
||||
// when v moves into a remaining segment, move it from segments to active.
|
||||
std::vector<double> uu;
|
||||
for(auto v = vstart; v != vend; v++) {
|
||||
uu.clear();
|
||||
|
||||
while(!active.empty() && active.front().q.y < v)
|
||||
{
|
||||
std::pop_heap(active.begin(), active.end(), heapcmp);
|
||||
active.pop_back();
|
||||
}
|
||||
while(!segments.empty() && segments.back().p.y < v) {
|
||||
const auto &s = segments.back();
|
||||
if(s.q.y >= v) {
|
||||
active.push_back(s);
|
||||
std::push_heap(active.begin(), active.end(), heapcmp);
|
||||
}
|
||||
segments.pop_back();
|
||||
}
|
||||
|
||||
//std::cerr << "v=" << v << " heap size=" << active.size() << "\n";
|
||||
for(const auto &s : active) {
|
||||
auto du = s.q.x - s.p.x;
|
||||
auto dv = s.q.y - s.p.y;
|
||||
if(dv) uu.push_back(s.p.x + du * (v - s.p.y) / dv);
|
||||
else { uu.push_back(s.p.x); uu.push_back(s.q.x); }
|
||||
}
|
||||
assert(uu.size() % 2 == 0);
|
||||
std::sort(uu.begin(), uu.end());
|
||||
for(size_t i=0; i<uu.size(); i+= 2) {
|
||||
uvdraw(pattern, v, uu[i], uu[i+1], cb);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct HatchPattern {
|
||||
std::vector<Dash> d;
|
||||
static HatchPattern FromFile(std::istream &fi, double scale) {
|
||||
HatchPattern result;
|
||||
|
||||
std::string line;
|
||||
while(getline(fi, line)) {
|
||||
auto i = line.find(";");
|
||||
if(i != line.npos) line.erase(i, line.npos);
|
||||
boost::algorithm::trim(line);
|
||||
boost::algorithm::trim(line);
|
||||
if(line.empty()) continue;
|
||||
if(line[0] == '*') continue;
|
||||
result.d.push_back(Dash::FromString(line, scale));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static HatchPattern FromFile(const char *filename, double scale) {
|
||||
std::ifstream fi(filename);
|
||||
return FromFile(fi, scale);
|
||||
}
|
||||
};
|
||||
|
||||
template<class It, class Cb>
|
||||
void xyhatch(const Dash &pattern, It start, It end, Cb cb) {
|
||||
std::vector<Segment> uvsegments;
|
||||
std::transform(start, end, std::back_inserter(uvsegments),
|
||||
[&](const Segment &s)
|
||||
{ return Segment{s.p * pattern.tf, s.q * pattern.tf };
|
||||
});
|
||||
uvspans(pattern, std::move(uvsegments), [&](double v, double u1, double u2) {
|
||||
Point p{u1, v}, q{u2, v};
|
||||
Segment xy{ p * pattern.tr, q * pattern.tr };
|
||||
cb(xy);
|
||||
});
|
||||
}
|
||||
|
||||
template<class It, class Cb>
|
||||
void xyhatch(const HatchPattern &pattern, It start, It end, Cb cb) {
|
||||
for(const auto &i : pattern.d) xyhatch(i, start, end, cb);
|
||||
}
|
||||
|
||||
template<class C, class Cb>
|
||||
void xyhatch(const HatchPattern &pattern, const C &c, Cb cb) {
|
||||
for(const auto &i : pattern.d) xyhatch(i, c.begin(), c.end(), cb);
|
||||
}
|
||||
|
||||
#endif
|
||||
135
main.c
Normal file
135
main.c
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
/*
|
||||
Copyright (c) 2015 Jeff Epler
|
||||
|
||||
This software is provided 'as-is', without any express or implied
|
||||
warranty. In no event will the authors be held liable for any damages
|
||||
arising from the use of this software.
|
||||
|
||||
Permission is granted to anyone to use this software for any purpose,
|
||||
including commercial applications, and to alter it and redistribute it
|
||||
freely, subject to the following restrictions:
|
||||
|
||||
1. The origin of this software must not be misrepresented; you must not
|
||||
claim that you wrote the original software. If you use this software
|
||||
in a product, an acknowledgement in the product documentation would be
|
||||
appreciated but is not required.
|
||||
2. Altered source versions must be plainly marked as such, and must not be
|
||||
misrepresented as being the original software.
|
||||
3. This notice may not be removed or altered from any source distribution.
|
||||
*/
|
||||
|
||||
// demo and benchmark program for dashing
|
||||
|
||||
#include "dashing.h"
|
||||
#include <iostream>
|
||||
|
||||
std::vector<Segment> SegmentsFromFile(std::istream &fi, double jitter) {
|
||||
static boost::random_device urandom;
|
||||
static boost::taus88 gen(urandom);
|
||||
boost::uniform_real<> dist(-jitter/2, jitter/2);
|
||||
boost::variate_generator<boost::taus88&, boost::uniform_real<> >
|
||||
random(gen, dist);
|
||||
std::vector<Segment> result;
|
||||
std::string line;
|
||||
while(getline(fi, line)) {
|
||||
auto numbers = parse_numbers(line);
|
||||
if(jitter)
|
||||
for(auto & n : numbers) n += random();
|
||||
if(numbers.size() % 2 != 0)
|
||||
throw std::invalid_argument("odd number of values in segment line");
|
||||
if(numbers.size() < 6)
|
||||
throw std::invalid_argument("too few values in segment line");
|
||||
for(size_t i=0; i<numbers.size() - 3 ; i += 2) {
|
||||
Segment s{{numbers[i], numbers[i+1]}, {numbers[i+2], numbers[i+3]}};
|
||||
result.push_back(s);
|
||||
}
|
||||
int i = numbers.size();
|
||||
Segment s{{numbers[0], numbers[1]}, {numbers[i-2], numbers[i-1]}};
|
||||
result.push_back(s);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<Segment> SegmentsFromFile(const char *filename, double jitter) {
|
||||
std::fstream fi(filename);
|
||||
return SegmentsFromFile(fi, jitter);
|
||||
}
|
||||
|
||||
void usage(const char *argv0) {
|
||||
fprintf(stderr,
|
||||
"Usage: %s [-b] [-s scale] [-j jitter] patfile segfile\n",
|
||||
argv0);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
auto scale = 1., jitter = 0.;
|
||||
bool bench = false;
|
||||
int c;
|
||||
while((c = getopt(argc, argv, "bs:j:")) > 0) {
|
||||
switch(c) {
|
||||
case 'b': bench = !bench; break;
|
||||
case 's': scale = atof(optarg); break;
|
||||
case 'j': jitter = atof(optarg); break;
|
||||
default:
|
||||
usage(argv[0]);
|
||||
}
|
||||
}
|
||||
auto nargs = argc - optind;
|
||||
if(nargs != 2) usage(argv[0]);
|
||||
|
||||
auto patfile = argv[optind];
|
||||
auto segfile = argv[optind+1];
|
||||
|
||||
auto h = HatchPattern::FromFile(patfile, scale);
|
||||
auto s = SegmentsFromFile(segfile, jitter);
|
||||
|
||||
if(bench) {
|
||||
int nseg = 0;
|
||||
auto print_seg = [&nseg](const Segment &s) { (void)s; nseg ++; };
|
||||
xyhatch(h, s, print_seg);
|
||||
std::cout << nseg << "\n";
|
||||
return 0;
|
||||
}
|
||||
|
||||
auto cmp_x = [](const Segment &a, const Segment & b)
|
||||
{ return a.p.x < b.p.x; };
|
||||
auto cmp_y = [](const Segment &a, const Segment & b)
|
||||
{ return a.p.y < b.p.y; };
|
||||
auto min_x = std::min_element(s.begin(), s.end(), cmp_x)->p.x;
|
||||
auto max_x = std::max_element(s.begin(), s.end(), cmp_x)->p.x;
|
||||
auto min_y = -std::max_element(s.begin(), s.end(), cmp_y)->p.y;
|
||||
auto max_y = -std::min_element(s.begin(), s.end(), cmp_y)->p.y;
|
||||
auto d_x = max_x - min_x;
|
||||
auto d_y = max_y - min_y;
|
||||
|
||||
std::cout <<
|
||||
"<svg width=\"100%%\" height=\"100%%\" viewBox=\""
|
||||
<< (min_x - .05 * d_x) << " " << (min_y - .05 * d_x) << " "
|
||||
<< (d_x * 1.1) << " " << (d_y * 1.1) << "\" "
|
||||
"preserveAspectRatio=\"xMidyMid\" "
|
||||
"xmlns=\"http://www.w3.org/2000/svg\" version=\"1.1\" "
|
||||
"xmlns:xlink=\"http://www.w3.org/1999/xlink\">"
|
||||
"<path d=\"M-100000 0L100000 0M0 -100000L0 100000\" stroke=\"green\" "
|
||||
"stroke-dasharray=\"20 20\"/>";
|
||||
|
||||
auto print_seg = [](const Segment &s) {
|
||||
std::cout << "M" << s.p.x << " " << -s.p.y
|
||||
<< "L" << s.q.x << " " << -s.q.y << "\n";
|
||||
};
|
||||
|
||||
std::cout << "<path fill=\"none\" stroke=\"black\" "
|
||||
"stroke-linecap=\"round\" d=\"";
|
||||
for(const auto i : s) print_seg(i);
|
||||
std::cout << "\"/>";
|
||||
|
||||
std::vector<Segment> segs;
|
||||
std::cout << "<path fill=\"none\" stroke=\"blue\" "
|
||||
"stroke-linecap=\"round\" d=\"";
|
||||
xyhatch(h, s, print_seg);
|
||||
std::cout << "\"/>";
|
||||
|
||||
std::cout << "</svg>";
|
||||
return 0;
|
||||
}
|
||||
Loading…
Reference in a new issue