Add Cubic Non-crossing multiline strategy (#15887)

Co-authored-by: Rodrigo Faselli <162915171+RF47@users.noreply.github.com>
This commit is contained in:
Ian Bassi
2026-09-26 11:40:49 -03:00
committed by GitHub
co-authored by Rodrigo Faselli
parent 93b58a2034
commit 8c03985818
6 changed files with 1026 additions and 1 deletions
+123
View File
@@ -0,0 +1,123 @@
# Multiline infill — High Level Design
## Purpose and scope
`fill_multiline` prints every sparse infill wall as N adjacent lines instead of
one, so a wall is `d1 = N * spacing` thick. Only internal sparse infill uses it.
Each pattern first builds its single-line centerlines at N times the usual line
spacing (so the density holds), and `multiline_fill()` then replaces each
centerline by the lines of that wall: the centerline itself when N is odd, and
closed outlines around it at every `spacing` out to `d1 / 2`. The outlines are
clipped to the fill region contracted by half a line width, then connected like
any other infill.
Outlines of centerlines that cross each other overlap at every crossing, which
over-extrudes the wall intersections. The line-crossing patterns Grid,
Triangles, Tri-hexagon and Cubic therefore build centerlines that never cross
(`FillRectilinear::fill_surface_trapezoidal()`), and so do Adaptive Cubic and
Support Cubic (`FillAdaptive`); the other patterns outline their usual
centerlines.
## Non-crossing centerlines
The crossing lines are resolved into x-monotone paths, the levels of the line
arrangement: walking along x, the k-th path is always the k-th line from the
bottom. At every crossing, the two paths bounce off each other instead of
passing through. Adjacent paths meet only at crossings, so their outlines touch
there and nowhere overlap.
Where two paths meet, each is cut short by a line perpendicular to the bisector
of its bend, `d1 / 2` from the crossing. The two cut segments are parallel and
`d1` apart, so the outermost lines of the two walls sit exactly `spacing` apart,
like the lines inside a wall. Where three lines meet at one point, the middle
path runs straight through and the outer two are cut `d1` from it.
Each pattern builds its rows along x in a rotated frame. Grid lines run at ±45°
there, and its rows are trapezoid waves that transpose on alternate layers. The three families of Triangles, Tri-hexagon and
Cubic run at 0°, 60° and 120°. Those rows rotate by 120° every layer about a
3-fold center of the arrangement, so each family takes every role in turn.
The pattern is phased on fixed positions, so it lines up across layers and
across the regions of one layer. Rounding the corners with
`sparse_infill_smooth_factor` happens before `multiline_fill()`.
## Cubic
Single-line Cubic draws the three families at the same spacing `h` and shifts
them with z: by `+dx`, `-dx` and `+dx`, `dx = z / sqrt(2)`. The multiline paths
follow the same lines. In the frame where one family is horizontal, the other two
cross in rows `h` apart, alternating by half a period, at height
`tau = -3 * dx (mod h)` above the horizontal line below them. The crossings split
every band between horizontal lines into up-pointing triangles of height `tau`,
down-pointing triangles of height `h - tau`, and hexagons. At `tau = 0` (and `h`)
all three families meet at common points, as in Triangles. At `tau = h / 2` the
triangles are equal, as in Tri-hexagon. The origin of that frame is always a
3-fold center, whatever z is, so the per-layer rotation keeps the lines in place.
Each band holds two paths that touch at its crossings: the upper one takes the
V below the crossing and runs along the top horizontal line, and the lower one
takes the inverted V above it and runs along the bottom line. Both are the same function
of `tau`, the lower one mirrored with `h - tau`. `cubic_upper_level()` builds one
period of the upper path as the lower envelope of five lines, clipped from below:
- the two slanted lines through the crossings,
- the horizontal line, lowered when the triangle above it is less than `1.5 * d1` high,
- the two chamfers where the path turns onto and off the horizontal line, `d1 / 2`
from those crossings,
- the flat cut into the V at the crossing.
The cut height `clamp(tau - d1 / 2, 0, h - d1) + d1` is what makes the pattern
continuous in z. While both triangles are at least `1.5 * d1` high, every
crossing is a pair of bends `d1 / 2` from it, as in Tri-hexagon. When a triangle
is thinner, its three paths stack like a triple crossing. The path through it
flattens toward its base line and lies on it once the triangle is under `d1 / 2`
high, and the paths beside it are pushed `d1` away. The layout thus reaches the
Triangles one where the families meet. Adjacent paths stay at least `d1` apart
at every `tau` and at every density up to 100%.
## Adaptive Cubic
Adaptive Cubic and Support Cubic take their lines from an octree of cubes
standing on a corner. On each layer every cube cuts its three mid-planes into
segments of the same three 60° families as Cubic, but the pattern is not
periodic. Smaller cubes near the surface add finer lines, and a finer line ends
where it meets the wall of its coarser cube, so the lines form crossings and
T-junctions. `FillAdaptive::multiline_paths()` builds the paths from these
segments directly, for each fill region and within `4 * d1` of it.
At a crossing the two paths bounce as in Cubic. At a T-junction the through line
runs straight on and the path of the ending line stops there. Every path still
runs left to right in the frame where one family is horizontal, and that family
rotates with the layer.
Every line of every cube size lies on one fine lattice, so crossings closer than
a few `d1` are the corners of one small triangle of that lattice, as in Cubic.
The cuts follow the Cubic rules without a closed formula:
- The two bends of a crossing are cut `d1` apart, `d1 / 2` each, perpendicular
to their bisector, so their walls touch. A cut goes no further than the path
end, and the other bend takes the rest of `d1`.
- At the tip of a small triangle, between the two slanted families, a cut also
goes no further than the neighbouring bend turning the other way, and the
path beyond that bend is kept a wall away from it. The bends onto the
horizontal family are not limited this way: pushing their paths apart would
open gaps between walls that should touch.
- A cut moves the path only where the cut line lies beyond it, near its bend.
The sharp bends between the two slanted families are cut after the bends onto
the horizontal family, so the tip of a small triangle wins, as in Cubic.
- A path stopping at a T-junction is trimmed until it is `d1` less half a line
spacing from every other path, so that its end overlaps the wall it stops on
by half a line and bonds to it. The paths are trimmed one at a time against
the others as already trimmed, so two ends facing each other meet instead of
both backing off. A path stopping on the line of another is trimmed before
that one, so it gives way and the other still reaches the line it stops on. A
second round trims every path again from its full length, so an end grows
back where the ends it gave way to were trimmed later, and a last round only
shortens them, keeping them that far apart. Paths shorter than `d1` are left
out.
- A line that ends on another less than `2 * d1` past a crossing stops at that
crossing instead, the shorter one where both do. The path along such a stub
would be trimmed away, leaving a hole between the walls that were cut to
touch it.
Short paths enclosed by coarser lines still print as closed outlines, but most
paths run on across several cells.
+572
View File
@@ -1,3 +1,4 @@
#include "../AABBTreeLines.hpp"
#include "../ClipperUtils.hpp"
#include "../ExPolygon.hpp"
#include "../Surface.hpp"
@@ -14,7 +15,9 @@
#include <cstdlib>
#include <cmath>
#include <algorithm>
#include <functional>
#include <numeric>
#include <tuple>
// Boost pool: Don't use mutexes to synchronize memory allocation.
#define BOOST_POOL_NO_MT
@@ -1318,6 +1321,564 @@ bool has_no_collinear_lines(const Polylines &polylines)
}
#endif
// Non-crossing centerlines for multiline adaptive cubic, see docs/HLSD/multiline-infill.md.
namespace noncrossing {
// y = x() * x + y() in the frame where the sweep family is horizontal.
using Lin = Vec2d;
static const Vec2d family_dir[3] { Vec2d(1., 0.), Vec2d(0.5, 0.5 * sqrt(3.)), Vec2d(0.5, -0.5 * sqrt(3.)) };
struct SweepLine
{
Vec2d a, b; // a.x() < b.x()
int family;
Lin lin;
std::vector<std::pair<double, int>> junctions; // (x, junction)
};
struct Junction
{
Vec2d p;
std::vector<int> lines;
std::vector<std::pair<int, int>> pairs; // (left, right) line of each path through, bottom-up
std::vector<std::pair<int, int>> bends; // (path, bend) of each pair, -1 where it runs straight
};
struct LevelPath
{
std::vector<Vec2d> verts; // start, bends, end
std::vector<int> lines; // line of each piece
std::vector<int> junctions; // junction of each bend
std::vector<int> turn; // 1 turning up, -1 turning down
std::vector<std::vector<Lin>> cuts;
std::vector<std::pair<Lin, int>> pushes; // (line, bend) keeping a wall away from a neighbour's cut
int start_term { -1 }; // junction where the path stops on another line, or -1
int end_term { -1 };
};
// Moves f onto line c (side 1: from below) wherever c lies beyond it, over the stretches overlapping [w0, w1].
static void clip_profile(std::vector<Vec2d> &f, const Lin &c, int side, double w0, double w1, double lim0, double lim1, bool cut_at_window, bool drop_past_limits)
{
const double r0 = std::max(lim0, f.front().x()), r1 = std::min(lim1, f.back().x());
if (r1 <= r0)
return;
const size_t ia = std::upper_bound(f.begin(), f.end(), r0, [](double x, const Vec2d &p) { return x < p.x(); }) - f.begin();
const size_t ib = std::lower_bound(f.begin() + ia, f.end(), r1, [](const Vec2d &p, double x) { return p.x() < x; }) - f.begin();
auto interpolate = [](const Vec2d &a, const Vec2d &b, double x) { return b.x() > a.x() ? a.y() + (x - a.x()) / (b.x() - a.x()) * (b.y() - a.y()) : b.y(); };
std::vector<Vec2d> local{ Vec2d(r0, interpolate(f[ia - 1], f[ia], r0)) };
local.insert(local.end(), f.begin() + ia, f.begin() + ib);
local.emplace_back(r1, interpolate(f[ib - 1], f[ib], r1));
const double tol = 1.;
auto beyond = [&c, side, tol](const Vec2d &p) { return side * (c.x() * p.x() + c.y() - p.y()) - tol; };
std::vector<std::pair<double, double>> stretches;
auto add = [&stretches](double x0, double x1) {
if (!stretches.empty() && stretches.back().second >= x0)
stretches.back().second = x1;
else
stretches.emplace_back(x0, x1);
};
for (size_t i = 1; i < local.size(); ++i) {
const Vec2d &p = local[i - 1], &q = local[i];
if (q.x() <= p.x())
continue;
const double bp = beyond(p), bq = beyond(q);
if (bp > 0. && bq > 0.)
add(p.x(), q.x());
else if (bp > 0. || bq > 0.) {
const double x = p.x() + bp / (bp - bq) * (q.x() - p.x());
if (bp > 0.)
add(p.x(), x);
else
add(x, q.x());
}
}
std::vector<std::pair<double, double>> keep;
for (auto [x0, x1] : stretches) {
if (x1 < w0 || x0 > w1)
continue;
if (drop_past_limits && ((x0 <= r0 && r0 == lim0) || (x1 >= r1 && r1 == lim1)))
continue;
keep.emplace_back(cut_at_window ? std::max(x0, w0) : x0, cut_at_window ? std::min(x1, w1) : x1);
}
if (keep.empty())
return;
auto y_local = [&](double x) {
size_t i = 1;
while (i + 1 < local.size() && local[i].x() < x)
++i;
return interpolate(local[i - 1], local[i], x);
};
std::vector<Vec2d> out(f.begin(), f.begin() + ia);
auto push = [&out, tol](double x, double y) {
if (out.empty() || x > out.back().x() || std::abs(y - out.back().y()) > 2. * tol)
out.emplace_back(x, y);
};
size_t k = 0;
for (const Vec2d &p : local) {
for (; k < keep.size() && keep[k].second < p.x(); ++k) {
const auto [x0, x1] = keep[k];
push(x0, y_local(x0));
push(x0, c.x() * x0 + c.y());
push(x1, c.x() * x1 + c.y());
push(x1, y_local(x1));
}
if (k < keep.size() && keep[k].first <= p.x() && p.x() <= keep[k].second)
continue;
push(p.x(), p.y());
}
for (; k < keep.size(); ++k) {
const auto [x0, x1] = keep[k];
push(x0, y_local(x0));
push(x0, c.x() * x0 + c.y());
push(x1, c.x() * x1 + c.y());
push(x1, y_local(x1));
}
for (size_t i = ib; i < f.size(); ++i)
push(f[i].x(), f[i].y());
f = std::move(out);
}
static std::vector<Vec2d> path_points(const LevelPath &path, const std::vector<SweepLine> &lines, double reach)
{
std::vector<Vec2d> f = path.verts;
const int nb = int(path.junctions.size());
auto x_of = [&path](int b) { return path.verts[b + 1].x(); };
auto sharp = [&](int b) { return lines[path.lines[b]].family != 0 && lines[path.lines[b + 1]].family != 0; };
// The run of bends turning the same way as bend b, up to the neighbouring bends turning the other way.
auto window = [&](int b) {
int l = b - 1, r = b + 1;
while (l >= 0 && path.turn[l] == path.turn[b])
--l;
while (r < nb && path.turn[r] == path.turn[b])
++r;
return std::make_pair(l >= 0 ? x_of(l) : f.front().x(), r < nb ? x_of(r) : f.back().x());
};
// Sharp bends between the slanted lines go last, so they win at the tip of a small triangle.
for (int b = 0; b < nb; ++b)
if (!sharp(b)) {
const auto [w0, w1] = window(b);
for (const Lin &c : path.cuts[b])
clip_profile(f, c, path.turn[b], w0, w1, x_of(b) - reach, x_of(b) + reach, true, false);
}
for (int b = 0; b < nb; ++b)
if (sharp(b))
for (const Lin &c : path.cuts[b])
clip_profile(f, c, path.turn[b], x_of(b), x_of(b), x_of(b) - reach, x_of(b) + reach, false, true);
for (const auto &[c, b] : path.pushes) {
const auto [w0, w1] = window(b);
clip_profile(f, c, path.turn[b], w0, w1, x_of(b) - reach, x_of(b) + reach, true, true);
}
std::vector<Vec2d> pts;
for (const Vec2d &p : f) {
while (pts.size() >= 2 && std::abs(cross2(Vec2d(pts.back() - pts[pts.size() - 2]), Vec2d(p - pts.back()))) <=
1e-9 * (pts.back() - pts[pts.size() - 2]).norm() * (p - pts.back()).norm())
pts.pop_back();
pts.push_back(p);
}
return pts;
}
static double polyline_length(const std::vector<Vec2d> &pts)
{
double len = 0.;
for (size_t i = 1; i < pts.size(); ++i)
len += (pts[i] - pts[i - 1]).norm();
return len;
}
// Point at the given distance along pts, and the index of the segment it lies on.
static std::pair<Vec2d, size_t> point_along(const std::vector<Vec2d> &pts, double t)
{
for (size_t i = 1; i < pts.size(); ++i) {
const double len = (pts[i] - pts[i - 1]).norm();
if (t <= len)
return { pts[i - 1] + (len > 0. ? t / len : 0.) * (pts[i] - pts[i - 1]), i };
t -= len;
}
return { pts.back(), pts.size() - 1 };
}
} // namespace noncrossing
Polylines multiline_paths(const Lines &lines_in, double d1, double end_overlap, int sweep, const BoundingBox &cover)
{
using namespace noncrossing;
const double eps = scale_(0.002);
const Eigen::Rotation2Dd to_sweep(-sweep * M_PI / 3.);
const BoundingBoxf box(cover.min.cast<double>(), cover.max.cast<double>());
// Lines in the sweep frame, collinear pieces merged.
struct Piece { double c, s0, s1; };
std::array<std::vector<Piece>, 3> pieces;
for (const Line &line : lines_in) {
Vec2d a = line.a.cast<double>(), b = line.b.cast<double>();
if (!Geometry::liang_barsky_line_clipping(a, b, box) || (b - a).norm() < 10. * eps)
continue;
a = to_sweep * a;
b = to_sweep * b;
const double angle = std::atan2(b.y() - a.y(), b.x() - a.x()) / (M_PI / 3.);
if (std::abs(angle - std::round(angle)) > 0.01)
// Not one of the three families.
return {};
const int f = (int(std::round(angle)) % 3 + 3) % 3;
const Vec2d &d = family_dir[f];
const Vec2d n(-d.y(), d.x());
pieces[f].push_back({ n.dot(a), std::min(d.dot(a), d.dot(b)), std::max(d.dot(a), d.dot(b)) });
}
std::vector<SweepLine> lines;
for (int f = 0; f < 3; ++f) {
std::vector<Piece> &ps = pieces[f];
const Vec2d &d = family_dir[f];
const Vec2d n(-d.y(), d.x());
const double slope = d.y() / d.x();
std::sort(ps.begin(), ps.end(), [](const Piece &l, const Piece &r) { return l.c < r.c; });
for (size_t i = 0; i < ps.size();) {
size_t j = i + 1;
while (j < ps.size() && ps[j].c - ps[i].c < eps)
++j;
std::sort(ps.begin() + i, ps.begin() + j, [](const Piece &l, const Piece &r) { return l.s0 < r.s0; });
double c = 0.;
for (size_t k = i; k < j; ++k)
c += ps[k].c / double(j - i);
double s0 = ps[i].s0, s1 = ps[i].s1;
for (size_t k = i + 1; k <= j; ++k) {
if (k < j && ps[k].s0 <= s1 + eps) {
s1 = std::max(s1, ps[k].s1);
continue;
}
const Vec2d a = s0 * d + c * n;
lines.push_back({ a, s1 * d + c * n, f, Lin(slope, a.y() - slope * a.x()), {} });
if (k < j) {
s0 = ps[k].s0;
s1 = ps[k].s1;
}
}
i = j;
}
}
auto along = [](const SweepLine &l, const Vec2d &p) { return family_dir[l.family].dot(p - l.a); };
auto length = [](const SweepLine &l) { return (l.b - l.a).norm(); };
// Crossings, including the ends of lines stopping on another line.
std::vector<Junction> junctions;
auto detect_junctions = [&]() {
struct Hit { Vec2d p; int i, j; };
std::vector<Hit> hits;
std::vector<int> order(lines.size());
std::iota(order.begin(), order.end(), 0);
std::sort(order.begin(), order.end(), [&lines](int l, int r) { return lines[l].a.x() < lines[r].a.x(); });
for (size_t oi = 0; oi < order.size(); ++oi) {
const SweepLine &li = lines[order[oi]];
for (size_t oj = oi + 1; oj < order.size() && lines[order[oj]].a.x() <= li.b.x() + eps; ++oj) {
const SweepLine &lj = lines[order[oj]];
if (li.family == lj.family)
continue;
const double x = (lj.lin.y() - li.lin.y()) / (li.lin.x() - lj.lin.x());
const Vec2d p(x, li.lin.x() * x + li.lin.y());
const double ti = along(li, p), tj = along(lj, p);
if (ti > -eps && ti < length(li) + eps && tj > -eps && tj < length(lj) + eps)
hits.push_back({ p, order[oi], order[oj] });
}
}
std::sort(hits.begin(), hits.end(), [](const Hit &l, const Hit &r) { return l.p.x() < r.p.x(); });
junctions.clear();
for (const Hit &hit : hits) {
int found = -1;
for (int k = int(junctions.size()) - 1; k >= 0 && junctions[k].p.x() > hit.p.x() - eps; --k)
if (std::abs(junctions[k].p.y() - hit.p.y()) < eps) {
found = k;
break;
}
if (found < 0) {
found = int(junctions.size());
junctions.push_back({ hit.p, {}, {}, {} });
}
std::vector<int> &jl = junctions[found].lines;
for (int li : { hit.i, hit.j })
if (std::find(jl.begin(), jl.end(), li) == jl.end())
jl.push_back(li);
}
for (SweepLine &l : lines)
l.junctions.clear();
for (int ji = 0; ji < int(junctions.size()); ++ji)
for (int li : junctions[ji].lines)
lines[li].junctions.emplace_back(junctions[ji].p.x(), ji);
for (SweepLine &l : lines)
std::sort(l.junctions.begin(), l.junctions.end());
};
detect_junctions();
auto has_arm = [&](int ji, int li, bool right) {
const double t = along(lines[li], junctions[ji].p);
return right ? t < length(lines[li]) - eps : t > eps;
};
// A line ending on another just past a crossing stops at the crossing, where its stub would leave a hole.
auto crosses = [&](int ji, int li) { return has_arm(ji, li, false) && has_arm(ji, li, true); };
for (int pass = 0; pass < 3; ++pass) {
std::vector<bool> touched(lines.size(), false);
bool changed = false;
for (int ji = 0; ji < int(junctions.size()); ++ji) {
const Junction &J = junctions[ji];
if (J.lines.size() != 2 || touched[J.lines[0]] || touched[J.lines[1]] || !crosses(ji, J.lines[0]) || !crosses(ji, J.lines[1]))
continue;
// Shortest arm of each line from J to the junction where it ends on another line.
struct DeadArm { double length; bool at_b; int end; };
std::array<DeadArm, 2> dead;
dead.fill({ std::numeric_limits<double>::max(), false, -1 });
for (int k = 0; k < 2; ++k) {
const SweepLine &l = lines[J.lines[k]];
const size_t at = std::find_if(l.junctions.begin(), l.junctions.end(), [ji](const std::pair<double, int> &j) { return j.second == ji; }) - l.junctions.begin();
const double t = along(l, J.p);
if (at + 1 < l.junctions.size() && length(l) - along(l, junctions[l.junctions[at + 1].second].p) < eps)
dead[k] = { length(l) - t, true, l.junctions[at + 1].second };
if (at > 0 && along(l, junctions[l.junctions[at - 1].second].p) < eps && t < dead[k].length)
dead[k] = { t, false, l.junctions[at - 1].second };
}
const int k = dead[0].length <= dead[1].length ? 0 : 1;
if (dead[k].length >= 2. * d1)
continue;
SweepLine &l = lines[J.lines[k]];
(dead[k].at_b ? l.b : l.a) = J.p;
// Only the shortened line and those it ended on have stale junctions until the next pass.
for (int li : junctions[dead[k].end].lines)
touched[li] = true;
changed = true;
}
if (!changed)
break;
detect_junctions();
}
// At every crossing the lines bounce off each other, so that every path keeps running left to right.
for (int ji = 0; ji < int(junctions.size()); ++ji) {
Junction &J = junctions[ji];
std::vector<int> left, right;
for (int li : J.lines)
if (has_arm(ji, li, false) && has_arm(ji, li, true))
left.push_back(li);
right = left;
std::sort(left.begin(), left.end(), [&lines](int l, int r) { return lines[l].lin.x() > lines[r].lin.x(); });
std::sort(right.begin(), right.end(), [&lines](int l, int r) { return lines[l].lin.x() < lines[r].lin.x(); });
for (size_t k = 0; k < left.size(); ++k)
J.pairs.emplace_back(left[k], right[k]);
J.bends.assign(J.pairs.size(), { -1, -1 });
}
std::vector<LevelPath> paths;
for (int li = 0; li < int(lines.size()); ++li) {
const SweepLine &l = lines[li];
const int start = !l.junctions.empty() && !has_arm(l.junctions.front().second, li, false) ? l.junctions.front().second : -1;
LevelPath path;
path.start_term = start;
path.verts.push_back(start >= 0 ? junctions[start].p : l.a);
path.lines.push_back(li);
int cur = li;
double x = path.verts.front().x();
for (;;) {
const auto &js = lines[cur].junctions;
const auto it = std::find_if(js.begin(), js.end(), [x, eps](const std::pair<double, int> &j) { return j.first > x + 0.25 * eps; });
if (it == js.end()) {
path.verts.push_back(lines[cur].b);
break;
}
Junction &J = junctions[it->second];
const size_t k = std::find_if(J.pairs.begin(), J.pairs.end(), [cur](const std::pair<int, int> &p) { return p.first == cur; }) - J.pairs.begin();
if (k == J.pairs.size()) {
path.verts.push_back(J.p);
path.end_term = it->second;
break;
}
if (const int next = J.pairs[k].second; next != cur) {
J.bends[k] = { int(paths.size()), int(path.junctions.size()) };
path.verts.push_back(J.p);
path.lines.push_back(next);
path.junctions.push_back(it->second);
path.turn.push_back(lines[next].lin.x() > lines[cur].lin.x() ? 1 : -1);
cur = next;
}
x = it->first;
}
path.cuts.resize(path.junctions.size());
paths.push_back(std::move(path));
}
// Cut the two bends of a crossing d1 apart, no further than the neighbouring bends turning the other way.
for (const Junction &J : junctions) {
if (J.pairs.size() < 2 || J.bends.front().first < 0 || J.bends.back().first < 0)
continue;
const Vec2d n = (family_dir[lines[J.pairs.back().second].family] - family_dir[lines[J.pairs.back().first].family]).normalized();
auto cut = [&J, &n](double offset) {
const Vec2d q = J.p + offset * n;
const double s = -n.x() / n.y();
return Lin(s, q.y() - s * q.x());
};
LevelPath &lo = paths[J.bends.front().first], &hi = paths[J.bends.back().first];
const int lb = J.bends.front().second, hb = J.bends.back().second;
if (J.pairs.size() == 3) {
lo.cuts[lb].push_back(cut(-d1));
hi.cuts[hb].push_back(cut(d1));
continue;
}
// Only the tip of a small triangle, between the two slanted families, stops at its neighbouring bends.
const bool sharp = lines[J.pairs.front().first].family != 0 && lines[J.pairs.front().second].family != 0;
auto room = [&](const LevelPath &P, int b, int away, double sign) {
double c = std::numeric_limits<double>::max();
for (int nb : { b - 1, b + 1 })
if (sharp && nb >= 0 && nb < int(P.junctions.size()) && P.turn[nb] == away)
c = std::min(c, sign * n.dot(junctions[P.junctions[nb]].p - J.p));
if (b == 0 && P.start_term >= 0)
c = std::min(c, sign * n.dot(P.verts.front() - J.p));
if (b + 1 == int(P.junctions.size()) && P.end_term >= 0)
c = std::min(c, sign * n.dot(P.verts.back() - J.p));
return std::max(c, 0.);
};
const double c_lo = room(lo, lb, 1, -1.), c_hi = room(hi, hb, -1, 1.);
double d_lo = 0.5 * d1;
if (d1 - c_hi <= c_lo)
d_lo = std::clamp(d_lo, d1 - c_hi, c_lo);
else if (c_lo + c_hi > 0.)
d_lo = d1 * c_lo / (c_lo + c_hi);
const double d_hi = d1 - d_lo;
lo.cuts[lb].push_back(cut(-d_lo));
hi.cuts[hb].push_back(cut(d_hi));
auto propagate = [&](const LevelPath &P, int b, int away, int step, double offset) {
for (int nb : { b - 1, b + 1 })
if (nb >= 0 && nb < int(P.junctions.size()) && P.turn[nb] == away) {
const Junction &W = junctions[P.junctions[nb]];
const int k = int(std::find_if(W.pairs.begin(), W.pairs.end(), [&](const std::pair<int, int> &p) { return p.first == P.lines[nb]; }) - W.pairs.begin()) + step;
if (k >= 0 && k < int(W.bends.size()) && W.bends[k].first >= 0)
paths[W.bends[k].first].pushes.emplace_back(cut(offset), W.bends[k].second);
}
};
if (sharp) {
propagate(lo, lb, 1, -1, -d_lo - d1);
propagate(hi, hb, -1, 1, d_hi + d1);
}
}
std::vector<std::vector<Vec2d>> geometry;
Linesf segments;
std::vector<std::pair<int, double>> segment_start; // path, distance along it
std::vector<std::pair<double, double>> kept; // stretch of each path left by the trimming
for (const LevelPath &path : paths) {
geometry.push_back(path.junctions.empty() ? std::vector<Vec2d>{ path.verts.front(), path.verts.back() } : path_points(path, lines, 4. * d1));
double along_path = 0.;
for (size_t i = 1; i < geometry.back().size(); ++i) {
segments.emplace_back(geometry.back()[i - 1], geometry.back()[i]);
segment_start.emplace_back(int(geometry.size()) - 1, along_path);
along_path += (geometry.back()[i] - geometry.back()[i - 1]).norm();
}
kept.emplace_back(0., along_path);
}
// A path ending on another line stops end_overlap inside the walls of the others, as trimmed so far.
const double end_clearance = d1 - end_overlap;
AABBTreeLines::LinesDistancer<Linef> tree(segments);
auto clearance = [&](int pi, const Vec2d &q) {
double dist = std::numeric_limits<double>::max();
for (size_t s : tree.all_lines_in_radius(q, d1)) {
const auto [pj, start] = segment_start[s];
const Vec2d a = segments[s].a, d = segments[s].b - a;
const double len = d.norm(), t0 = std::max(0., kept[pj].first - start), t1 = std::min(len, kept[pj].second - start);
if (pj != pi && len > 0. && t0 <= t1)
dist = std::min(dist, line_alg::distance_to(Linef(a + t0 / len * d, a + t1 / len * d), q));
}
return dist;
};
// Returns the length trimmed off.
auto trim_front = [&](int pi, std::vector<Vec2d> &pts) {
const double total = polyline_length(pts), step = d1 / 32.;
double t = 0.;
while (t <= total && clearance(pi, point_along(pts, t).first) < end_clearance)
t += step;
if (t > total) {
pts.clear();
return total;
}
if (t == 0.)
return 0.;
for (double lo = std::max(0., t - step); t - lo > step / 256.;)
if (const double mid = 0.5 * (lo + t); clearance(pi, point_along(pts, mid).first) < end_clearance)
lo = mid;
else
t = mid;
const auto [q, seg] = point_along(pts, t);
pts.erase(pts.begin(), pts.begin() + (seg - 1));
pts.front() = q;
return t;
};
// A path stopping on the line of another path is trimmed first, so that it gives way to that path.
std::vector<std::vector<std::tuple<double, double, int>>> carried(lines.size()); // x range and path of each piece
for (int pi = 0; pi < int(paths.size()); ++pi)
for (size_t i = 0; i < paths[pi].lines.size(); ++i)
carried[paths[pi].lines[i]].emplace_back(paths[pi].verts[i].x(), paths[pi].verts[i + 1].x(), pi);
std::vector<std::vector<int>> stopping_on(paths.size());
for (int pi = 0; pi < int(paths.size()); ++pi)
for (const auto &[ji, own] : { std::make_pair(paths[pi].start_term, paths[pi].lines.front()), std::make_pair(paths[pi].end_term, paths[pi].lines.back()) })
if (ji >= 0)
for (int li : junctions[ji].lines)
if (li != own)
for (const auto &[x0, x1, pj] : carried[li])
if (pj != pi && x0 - eps <= junctions[ji].p.x() && junctions[ji].p.x() <= x1 + eps)
stopping_on[pj].push_back(pi);
std::vector<int> order;
std::vector<bool> visited(paths.size(), false);
std::function<void(int)> visit = [&](int pi) {
if (visited[pi])
return;
visited[pi] = true;
for (int child : stopping_on[pi])
visit(child);
order.push_back(pi);
};
for (int pi = 0; pi < int(paths.size()); ++pi)
visit(pi);
std::vector<std::vector<Vec2d>> trimmed(paths.size());
auto trim = [&](int pi) {
std::vector<Vec2d> &pts = trimmed[pi];
if (paths[pi].start_term >= 0)
kept[pi].first += trim_front(pi, pts);
if (paths[pi].end_term >= 0 && !pts.empty()) {
std::reverse(pts.begin(), pts.end());
kept[pi].second -= trim_front(pi, pts);
std::reverse(pts.begin(), pts.end());
}
if (pts.size() < 2 || polyline_length(pts) < d1) {
pts.clear();
kept[pi] = { 0., -1. };
}
};
// Ends grow back where the ends they gave way to were trimmed later; the last pass only shortens them.
for (int pass = 0; pass < 3; ++pass)
for (int pi : order) {
if (pass < 2) {
trimmed[pi] = geometry[pi];
kept[pi] = { 0., polyline_length(geometry[pi]) };
} else if (trimmed[pi].empty())
continue;
trim(pi);
}
Polylines out;
const Eigen::Rotation2Dd to_world = to_sweep.inverse();
for (const std::vector<Vec2d> &pts : trimmed) {
if (pts.empty())
continue;
Polyline pl;
for (const Vec2d &p : pts) {
const Vec2d w = to_world * p;
pl.points.emplace_back(coord_t(std::round(w.x())), coord_t(std::round(w.y())));
}
out.emplace_back(std::move(pl));
}
return out;
}
void Filler::_fill_surface_single(
const FillParams &params,
unsigned int thickness_layers,
@@ -1371,6 +1932,17 @@ void Filler::_fill_surface_single(
all_polylines.reserve(lines.size());
std::transform(lines.begin(), lines.end(), std::back_inserter(all_polylines), [](const Line& l) { return Polyline{ l.a, l.b }; });
if (params.multiline > 1) {
const double d1 = scale_(this->spacing) * params.multiline;
BoundingBox cover = get_extents(expolygon);
cover.offset(coord_t(4. * d1));
// Rotate the family the paths run along with the layer, like the other multiline patterns.
const int sweep = int((this->layer_id / std::max(thickness_layers, 1u)) % 3);
// Line ends overlap the walls they stop on by half a line, so that they bond.
if (Polylines paths = multiline_paths(lines, d1, 0.5 * scale_(this->spacing), sweep, cover); !paths.empty())
all_polylines = std::move(paths);
}
// Apply multiline offset if needed
multiline_fill(all_polylines, params, spacing);
+3
View File
@@ -48,6 +48,9 @@ FillAdaptive::OctreePtr build_octree(
// If true, octree is densified below internal overhangs only.
bool support_overhangs_only);
// Multiline infill: lines of the three families to non-crossing paths d1 apart, ends reaching end_overlap into walls.
Polylines multiline_paths(const Lines &lines, double d1, double end_overlap, int sweep, const BoundingBox &cover);
//
// Some of the algorithms used by class FillAdaptive were inspired by
// Cura Engine's class SubDivCube
+99 -1
View File
@@ -3047,12 +3047,56 @@ bool FillRectilinear::fill_surface_by_multilines(const Surface *surface, FillPar
return true;
}
// Upper level of a cubic band [0, h] over one period, from the crossing at (0, tau) to the one at (period, tau).
// See docs/HLSD/multiline-infill.md.
static std::vector<Vec2d> cubic_upper_level(double tau, double h, double period, double d1)
{
const double s3 = std::sqrt(3.);
const double y_cut = std::clamp(tau - 0.5 * d1, 0., h - d1) + d1;
const double y_flat = std::min(h, h + y_cut - 2. * d1);
const double x2 = (h - tau) / s3 + d1;
const double x3 = (h + tau) / s3 - d1;
// (slope, intercept) of the rising line, its chamfer, the horizontal line, the falling chamfer and line.
const std::array<Vec2d, 5> lines{ Vec2d(s3, tau), Vec2d(1. / s3, h - x2 / s3), Vec2d(0., y_flat),
Vec2d(-1. / s3, h + x3 / s3), Vec2d(-s3, tau + s3 * period) };
auto y_at = [&lines, y_cut](double x) {
double y = std::numeric_limits<double>::max();
for (const Vec2d &l : lines)
y = std::min(y, l.x() * x + l.y());
return std::max(y, y_cut);
};
std::vector<double> xs;
for (size_t i = 0; i < lines.size(); ++i) {
if (lines[i].x() != 0.)
xs.emplace_back((y_cut - lines[i].y()) / lines[i].x());
for (size_t j = i + 1; j < lines.size(); ++j)
xs.emplace_back((lines[j].y() - lines[i].y()) / (lines[i].x() - lines[j].x()));
}
xs.erase(std::remove_if(xs.begin(), xs.end(), [period](double x) { return x <= 1. || x >= period - 1.; }), xs.end());
xs.insert(xs.end(), { 0., period });
std::sort(xs.begin(), xs.end());
xs.erase(std::unique(xs.begin(), xs.end(), [](double a, double b) { return b - a < 1.; }), xs.end());
std::vector<Vec2d> pts;
for (double x : xs) {
const Vec2d p(x, y_at(x));
if (pts.size() >= 2) {
const Vec2d &a = pts[pts.size() - 2], &b = pts.back();
if (std::abs((b.y() - a.y()) / (b.x() - a.x()) - (p.y() - b.y()) / (p.x() - b.x())) < EPSILON)
pts.pop_back();
}
pts.emplace_back(p);
}
return pts;
}
bool FillRectilinear::fill_surface_trapezoidal(
const Surface* surface,
FillParams params,
const std::initializer_list<SweepParams>& sweep_params,
Polylines& polylines_out,
int Pattern_type) // 0=grid, 1=triangular, 2=stars
int Pattern_type) // 0=grid, 1=triangular, 2=stars, 3=cubic
{
assert(params.multiline > 1);
@@ -3350,6 +3394,55 @@ bool FillRectilinear::fill_surface_trapezoidal(
break;
}
case 3: // Cubic
{
// Same z shifted lines as the single-line cubic; the slanted ones cross tau above the horizontal ones.
auto pos_mod = [](double a, double m) { const double r = std::fmod(a, m); return r < 0. ? r + m : r; };
const double h = 0.5 * std::sqrt(3.0) * period;
const double shift = scale_(std::sqrt(0.5) * this->z);
const double tau = pos_mod(-3. * shift, h);
const double y0 = pos_mod(-2. * shift, 2. * h);
std::array<std::vector<Vec2d>, 2> levels{ cubic_upper_level(h - tau, h, period, d1), cubic_upper_level(tau, h, period, d1) };
for (Vec2d &p : levels.front())
p.y() = h - p.y();
const size_t layer_mod = infill_layer_id % 3;
const double angle = layer_mod * 2.0 * M_PI / 3.0;
// Only cover the surface, seen in the frame the pattern is built in.
ExPolygon local = expolygon;
local.translate(-rotate_vector.second.x(), -rotate_vector.second.y());
if (layer_mod)
local.rotate(-angle);
BoundingBox cover = get_extents(local);
cover.offset(period);
const int64_t n_min = int64_t(std::floor((cover.min.y() - y0) / h)) - 1;
const int64_t n_max = int64_t(std::ceil((cover.max.y() - y0) / h)) + 1;
for (int64_t n = n_min; n <= n_max; ++n) {
const double x_off = (n & 1) ? 0.5 * period : 0.;
const double base = y0 + double(n) * h - tau;
const int64_t j_min = int64_t(std::floor((cover.min.x() - x_off) / period)) - 1;
const int64_t j_max = int64_t(std::ceil((cover.max.x() - x_off) / period));
for (const std::vector<Vec2d> &level : levels) {
Polyline row;
row.points.reserve(size_t(j_max - j_min + 1) * level.size());
for (int64_t j = j_min; j <= j_max; ++j)
for (size_t i = (j == j_min) ? 0 : 1; i < level.size(); ++i)
row.points.emplace_back(coord_t(std::round(x_off + double(j * period) + level[i].x())),
coord_t(std::round(base + level[i].y())));
polylines.emplace_back(std::move(row));
}
}
if (layer_mod)
for (Polyline &pl : polylines)
pl.rotate(angle, Point(0, 0));
break;
}
default:
// Handle unknown pattern type
break;
@@ -3532,6 +3625,11 @@ Polylines FillStars::fill_surface(const Surface *surface, const FillParams &para
Polylines FillCubic::fill_surface(const Surface *surface, const FillParams &params)
{
Polylines polylines_out;
if (params.multiline > 1) {
if (!this->fill_surface_trapezoidal(surface, params, {}, polylines_out, 3))
BOOST_LOG_TRIVIAL(error) << "FillCubic::fill_surface_trapezoidal() failed.";
return polylines_out;
}
coordf_t dx = sqrt(0.5) * z;
if (! this->fill_surface_by_multilines(
surface, params,
+1
View File
@@ -163,6 +163,7 @@ inline bool is_smoothable_infill_pattern(InfillPattern pattern, int multiline =
case ipGrid:
case ipTriangles:
case ipStars:
case ipCubic:
return multiline > 1;
default:
return false;
+228
View File
@@ -2,6 +2,7 @@
#include <algorithm>
#include <cmath>
#include <functional>
#include <map>
#include <numeric>
#include <sstream>
@@ -11,8 +12,10 @@
#include "libslic3r/ClipperUtils.hpp"
#include "libslic3r/AABBTreeLines.hpp"
#include "libslic3r/Fill/Fill.hpp"
#include "libslic3r/Fill/FillAdaptive.hpp"
#include "libslic3r/Flow.hpp"
#include "libslic3r/Geometry.hpp"
#include "libslic3r/IntersectionPoints.hpp"
#include "libslic3r/Layer.hpp"
#include "libslic3r/Print.hpp"
#include "libslic3r/PrintConfig.hpp"
@@ -1196,6 +1199,231 @@ TEST_CASE("Trapezoidal grid infill rounds its corners only with more than one li
REQUIRE(single_smooth.length == single_sharp.length);
}
TEST_CASE("Multiline cubic infill follows the cubic lines without crossing itself", "[Fill]")
{
const int multiline = GENERATE(2, 3);
const double spacing = 0.45;
const double density = 0.3;
const double wall = multiline * spacing;
CAPTURE(multiline);
const ExPolygon region{ Slic3r::Points{ Point::new_scale(0., 0.), Point::new_scale(40., 0.),
Point::new_scale(40., 40.), Point::new_scale(0., 40.) } };
auto fill = [&region, spacing](int lines, double density, size_t layer_id, double z) {
std::unique_ptr<Slic3r::Fill> filler(Slic3r::Fill::new_from_type("cubic"));
filler->spacing = spacing;
filler->angle = float(M_PI / 7.);
filler->layer_id = layer_id;
filler->z = z;
FillParams params;
params.density = float(density);
params.multiline = lines;
params.dont_adjust = true;
params.anchor_length_max = 0.f; // The bare pattern, without connections along the boundary.
Slic3r::Surface surface(stInternal, region);
return filler->fill_surface(&surface, params);
};
// Away from the boundary, where a line is clipped earlier than the side of its wall.
const Polygons inner = shrink(to_polygons(region), scale_(3.));
auto farthest = [&inner](const Polylines &from, const Polylines &to) {
const AABBTreeLines::LinesDistancer<Line> tree(to_lines(to));
double distance = 0.;
for (const Polyline &path : intersection_pl(from, inner))
for (const Point &point : path.equally_spaced_points(scale_(0.2)))
distance = std::max(distance, tree.distance_from_lines<false>(point));
return unscale<double>(distance);
};
// One z period of the pattern: sqrt(2) / 3 of the 3 * wall / density line spacing.
const double z_period = std::sqrt(2.) * wall / density;
const size_t layers = 30;
for (size_t layer_id = 0; layer_id < layers; ++layer_id) {
const double z = z_period * (layer_id + 0.5) / layers;
CAPTURE(layer_id, z);
const Polylines walls = fill(multiline, density, layer_id, z);
REQUIRE_FALSE(walls.empty());
CHECK(get_intersections(to_lines(walls)).empty());
// Long paths running out to the boundary, not loops around the cells.
CHECK(std::none_of(walls.begin(), walls.end(), [](const Polyline &path) { return path.first_point() == path.last_point(); }));
// Single lines at the same spacing: the walls are drawn along them.
const Polylines lines = fill(1, density / multiline, layer_id, z);
REQUIRE_FALSE(lines.empty());
CHECK(farthest(lines, walls) < 0.5 * wall);
CHECK(farthest(walls, lines) < 1.5 * wall);
}
}
TEST_CASE("Multiline adaptive cubic infill keeps its lines apart without closing them around the cells", "[Fill]")
{
const std::string pattern = GENERATE("adaptivecubic", "supportcubic");
const int multiline = GENERATE(2, 3);
CAPTURE(pattern, multiline);
// A sphere refines the octree all around, so the finer lines end on the coarser ones at every layer.
TriangleMesh sphere = Slic3r::Test::mesh(Slic3r::Test::TestMesh::sphere_50mm);
sphere.scale(0.3f);
Print print;
Slic3r::Test::init_and_process_print({sphere}, print,
{{"sparse_infill_pattern", pattern},
{"sparse_infill_density", "40%"},
{"fill_multiline", multiline},
{"infill_anchor", 0},
{"infill_anchor_max", 0},
{"layer_height", 0.3}});
size_t paths = 0, loops = 0;
for (const Layer *layer : print.objects().front()->layers()) {
Polylines printed;
Polygons sparse;
double spacing = 0.;
for (const LayerRegion *region : layer->regions()) {
for (const ExtrusionEntity *entity : region->fills.flatten().entities)
if (entity->role() == erInternalInfill)
entity->collect_polylines(printed);
for (const Surface &surface : region->fill_surfaces.surfaces)
if (surface.surface_type == stInternal)
append(sparse, shrink(to_polygons(surface.expolygon), scale_(1.)));
spacing = region->flow(frInfill).spacing();
}
if (printed.empty())
continue;
CAPTURE(layer->print_z);
paths += printed.size();
loops += std::count_if(printed.begin(), printed.end(), [](const Polyline &pl) { return pl.first_point() == pl.last_point(); });
CHECK(get_intersections(to_lines(printed)).empty());
// Neighbouring lines stay a line spacing apart, less the overlap of a line end with the wall it stops on.
// Pieces of one line that meet end to end are one line.
std::vector<size_t> line_of(printed.size());
std::iota(line_of.begin(), line_of.end(), 0);
std::function<size_t(size_t)> find = [&](size_t i) { return line_of[i] == i ? i : line_of[i] = find(line_of[i]); };
for (size_t i = 0; i < printed.size(); ++i)
for (size_t j = i + 1; j < printed.size(); ++j)
for (const Point &a : { printed[i].first_point(), printed[i].last_point() })
for (const Point &b : { printed[j].first_point(), printed[j].last_point() })
if ((a - b).cast<double>().norm() < SCALED_EPSILON)
line_of[find(i)] = find(j);
Lines lines;
std::vector<size_t> owner;
for (size_t i = 0; i < printed.size(); ++i)
for (const Line &line : printed[i].lines()) {
lines.push_back(line);
owner.push_back(find(i));
}
AABBTreeLines::LinesDistancer<Line> tree(lines);
double closest = spacing;
for (size_t i = 0; i < printed.size(); ++i)
for (const Point &p : printed[i].equally_spaced_points(scale_(0.1)))
if (contains(sparse, p))
for (size_t k : tree.all_lines_in_radius(p, scale_(spacing)))
if (owner[k] != find(i))
closest = std::min(closest, unscale<double>(lines[k].distance_to(p)));
CHECK(closest > 0.45 * spacing);
}
REQUIRE(paths > 0);
// The lines run on through the cells instead of each cell getting its own loops.
CHECK(loops < paths / 4);
}
TEST_CASE("Multiline adaptive cubic paths touch where they bounce off each other", "[Fill]")
{
const int sweep = GENERATE(0, 1, 2);
// Offset of the third family in walls, so the three meet in points or in small triangles.
const double shift = GENERATE(0., 0.1, 0.5, 1., 2.5, -0.5, -1.);
// Like finer octree lines ending on coarser ones, the 60 degree lines may start on the horizontal line through 0.
const bool starting = GENERATE(false, true);
CAPTURE(sweep, shift, starting);
const double d1 = scale_(0.8), pitch = scale_(8.), inner = scale_(12.);
Lines lines;
for (int k = 0; k < 3; ++k) {
const Vec2d dir(std::cos(k * M_PI / 3.), std::sin(k * M_PI / 3.)), normal(-dir.y(), dir.x());
for (int i = -6; i <= 6; ++i) {
const Vec2d mid = (i * pitch + (k == 2 ? shift * d1 : 0.)) * normal;
const double start = k == 1 && starting ? -mid.y() / dir.y() : -10. * pitch;
lines.emplace_back((mid + start * dir).cast<coord_t>(), (mid + 10. * pitch * dir).cast<coord_t>());
}
}
const Polylines paths = FillAdaptive::multiline_paths(lines, d1, 0., sweep, BoundingBox(Point::new_scale(-20., -20.), Point::new_scale(20., 20.)));
REQUIRE_FALSE(paths.empty());
CHECK(get_intersections(to_lines(paths)).empty());
Lines pieces;
std::vector<size_t> owner;
for (size_t i = 0; i < paths.size(); ++i)
for (const Line &line : paths[i].lines()) {
pieces.push_back(line);
owner.push_back(i);
}
AABBTreeLines::LinesDistancer<Line> tree(pieces);
auto clearance = [&](size_t i) {
const Line &a = pieces[i];
double distance = std::numeric_limits<double>::max();
for (size_t j : tree.all_lines_in_radius(a.midpoint(), 0.5 * a.length() + 2. * d1))
if (owner[j] != owner[i]) {
const Line &b = pieces[j];
distance = std::min({ distance, a.distance_to(b.a), a.distance_to(b.b), b.distance_to(a.a), b.distance_to(a.b) });
}
return distance;
};
auto inside = [inner](const Point &p) { return std::abs(p.x()) < inner && std::abs(p.y()) < inner; };
double closest = std::numeric_limits<double>::max();
for (size_t i = 0; i < pieces.size(); ++i)
if (inside(pieces[i].midpoint()))
closest = std::min(closest, clearance(i));
CHECK(closest > 0.99 * d1);
// Each path at a crossing touches another one there, none stops short of it.
double widest = 0.;
for (size_t i = 0; i < lines.size(); ++i)
for (size_t j = i + 1; j < lines.size(); ++j)
if (Point crossing; line_alg::intersection(lines[i], lines[j], &crossing) && inside(crossing)) {
std::map<size_t, double> at;
for (size_t k : tree.all_lines_in_radius(crossing, 1.2 * d1))
at.emplace(owner[k], std::numeric_limits<double>::max());
for (size_t k : tree.all_lines_in_radius(crossing, 2. * d1))
if (auto it = at.find(owner[k]); it != at.end())
it->second = std::min(it->second, clearance(k));
for (const auto &path : at)
widest = std::max(widest, path.second);
}
CHECK(widest < 1.02 * d1);
}
TEST_CASE("Multiline adaptive cubic paths reach the line they end on when another path ends on them", "[Fill]")
{
const int sweep = GENERATE(0, 1, 2);
// Where the 120 degree line starts on the horizontal one, in walls from the 60 degree line.
const double start = GENERATE(0.3, 0.6, 1., 2.);
CAPTURE(sweep, start);
const double d1 = scale_(0.8), overlap = 0.1 * d1, length = scale_(30.);
const Vec2d diagonal(0.5, 0.5 * std::sqrt(3.)), horizontal(1., 0.), steep(-0.5, 0.5 * std::sqrt(3.));
const Vec2d on_horizontal = start * d1 * horizontal;
const Lines lines{ Line((-length * diagonal).cast<coord_t>(), (length * diagonal).cast<coord_t>()),
Line(Point(0, 0), (length * horizontal).cast<coord_t>()),
Line(on_horizontal.cast<coord_t>(), (on_horizontal - length * steep).cast<coord_t>()) };
const Polylines paths = FillAdaptive::multiline_paths(lines, d1, overlap, sweep, BoundingBox(Point::new_scale(-40., -40.), Point::new_scale(40., 40.)));
// The end of the path along each line nearest to where that line starts.
auto end_along = [&paths](const Line &line) {
for (const Polyline &path : paths)
if (line.distance_to(path.first_point()) < SCALED_EPSILON && line.distance_to(path.last_point()) < SCALED_EPSILON)
return (path.first_point() - line.a).cast<double>().norm() < (path.last_point() - line.a).cast<double>().norm() ? path.first_point() : path.last_point();
return Point(std::numeric_limits<coord_t>::max(), 0);
};
const Point horizontal_end = end_along(lines[1]), steep_end = end_along(lines[2]);
REQUIRE(horizontal_end.x() != std::numeric_limits<coord_t>::max());
REQUIRE(steep_end.x() != std::numeric_limits<coord_t>::max());
// Both reach the overlap into the wall of the path they stop at, none stops short of it.
CHECK_THAT(line_alg::distance_to_infinite(lines[0], horizontal_end) / d1, Catch::Matchers::WithinAbs(0.9, 0.01));
CHECK(lines[1].distance_to(steep_end) / d1 < 0.91);
CHECK(get_intersections(to_lines(paths)).empty());
}
TEST_CASE("3D honeycomb infill rounds its octahedral waves with the smooth factor", "[Fill]")
{
auto shape_for = [](const std::string &smooth_factor) {