initial commit

This commit is contained in:
Jeff Epler 2015-08-19 19:45:06 -05:00
commit 13f3f9e7ae
5 changed files with 479 additions and 0 deletions

18
LICENSE.md Normal file
View 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
View 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
View 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
View 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
View 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;
}