mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-27 02:41:17 +00:00
Add Cubic Non-crossing multiline strategy (#15887)
Co-authored-by: Rodrigo Faselli <162915171+RF47@users.noreply.github.com>
This commit is contained in:
co-authored by
Rodrigo Faselli
parent
93b58a2034
commit
8c03985818
@@ -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.
|
||||
@@ -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 ¶ms,
|
||||
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);
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 ¶
|
||||
Polylines FillCubic::fill_surface(const Surface *surface, const FillParams ¶ms)
|
||||
{
|
||||
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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 = [®ion, 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) {
|
||||
|
||||
Reference in New Issue
Block a user