Files
OrcaSlicer/src/libslic3r/BeltBrim.cpp
T
harrierpigeonandClaude Fable 5.1 a564ec23fe Belt: refresh comments that described earlier code
BeltBrim.cpp still described the brim as running inside the parallel
support step; it runs sequentially after it (generate_belt_brim).  The
GCodeWriter, calib.cpp and calib.hpp comments referred to an inheritance
layout and a dynamic_cast that no longer exist.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-07 19:08:59 -05:00

552 lines
25 KiB
C++

#include <limits>
#include "BeltBrim.hpp"
#include "ClipperUtils.hpp"
#include "Flow.hpp"
#include "Layer.hpp"
#include "Polygon.hpp"
#include "Print.hpp"
#include "ShortestPath.hpp"
#include "Support/BeltFloorContext.hpp"
#include "BoundingBox.hpp"
#include "ExPolygon.hpp"
#include "ExtrusionEntity.hpp"
#include "ExtrusionEntityCollection.hpp"
#include "Point.hpp"
#include "Polyline.hpp"
#include "PrintConfig.hpp"
#include "libslic3r.h"
#include <algorithm>
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <cstdlib>
#include <utility>
#include <vector>
namespace Slic3r {
// ---------------------------------------------------------------- scaling
static inline Point scale_u_point(const Point &p, int from_axis, double factor)
{
// llround, not a cast: casting truncates toward zero, so a round trip would
// walk every vertex toward the origin by up to one unit per pass.
return from_axis == 0 ?
Point(coord_t(std::llround(double(p.x()) * factor)), p.y()) :
Point(p.x(), coord_t(std::llround(double(p.y()) * factor)));
}
static inline void scale_u_polygon(Polygon &poly, int from_axis, double factor)
{
for (Point &p : poly.points)
p = scale_u_point(p, from_axis, factor);
}
ExPolygons belt_scale_u(const ExPolygons &src, const BeltBrimFrame &frame, double factor)
{
ExPolygons out = src;
for (ExPolygon &ex : out) {
scale_u_polygon(ex.contour, frame.from_axis, factor);
for (Polygon &hole : ex.holes)
scale_u_polygon(hole, frame.from_axis, factor);
}
return out;
}
Polylines belt_scale_u(const Polylines &src, const BeltBrimFrame &frame, double factor)
{
Polylines out = src;
for (Polyline &pl : out)
for (Point &p : pl.points)
p = scale_u_point(p, frame.from_axis, factor);
return out;
}
// ---------------------------------------------------------------- sweep
ExPolygons sweep_ex(const ExPolygons &src, const Point &t)
{
if (src.empty())
return {};
if (t == Point(0, 0))
return src;
// One parallelogram per boundary edge. Together with P and P + t these
// cover the Minkowski sum exactly: for any q = p + s*t with p in P and
// s in [0, 1], let s* be the smallest lambda >= 0 with q - lambda*t in P.
// Either s* == 0 (so q is in P) or q - s* * t lies on some boundary edge e,
// putting q in that edge's parallelogram. Hole edges must be included, or
// holes narrower than t along t would wrongly survive the sweep.
Polygons quads;
for (const ExPolygon &ex : src)
for (size_t c = 0; c < ex.num_contours(); ++ c)
for (const Line &e : ex.contour_or_hole(c).lines()) {
if (e.a == e.b)
continue;
Polygon q;
q.points = { e.a, e.b, e.b + t, e.a + t };
// The non-zero fill rule counts a clockwise ring as -1, which
// would punch a hole instead of adding material. Edges parallel
// to t give a zero-area quad; Clipper discards those harmlessly.
if (q.is_clockwise())
q.reverse();
quads.emplace_back(std::move(q));
}
ExPolygons shifted = src;
for (ExPolygon &ex : shifted)
ex.translate(t);
// union_ex(ExPolygons, Polygons) uses pftNonZero, which is the fill rule the
// argument above relies on.
return union_ex(union_ex(src, shifted), quads);
}
// ---------------------------------------------------------------- brim region
ExPolygons belt_brim_region(const ExPolygons &footprint_flat,
bool has_outer,
bool has_inner,
coord_t brim_width,
coord_t object_gap,
coord_t leading,
coord_t lateral,
const BeltBrimFrame &frame)
{
if (footprint_flat.empty() || (! has_outer && ! has_inner))
return {};
ExPolygons out;
if (has_outer) {
// Offset the outer ring from the contours only, so a hole cannot punch
// through it. Same reasoning as the plate brim in Brim.cpp.
Polygons contours;
contours.reserve(footprint_flat.size());
for (const ExPolygon &ex : footprint_flat)
contours.emplace_back(ex.contour);
// Inner and outer boundary offset from the same polygon, to avoid
// round-off mismatch between them.
ExPolygons inner = offset_ex(contours, float(object_gap), jtRound, SCALED_RESOLUTION);
// Close the interior before offsetting outwards. A belt contact patch is often a
// narrow, broken-up strip, and the offset rings of two islands less than
// 2 x brim_width apart merge and fill the space between them - space that lies
// UNDER the part, which is not what "outer brim" means. Closing also swallows
// holes in the patch for the same reason. Concavity-filling only, so an apron or
// any other outward protrusion is untouched.
ExPolygons envelope = brim_width > 0 ? closing_ex(inner, float(brim_width)) : inner;
ExPolygons base = envelope;
if (leading > 0) {
// Sweep downhill from the gapped keep-out, so the apron is contiguous with
// the ring instead of starting inside the gap.
const Point t = frame.from_axis == 0 ?
Point(frame.downhill_sign() * leading, 0) :
Point(0, frame.downhill_sign() * leading);
base = union_ex(base, sweep_ex(envelope, t));
}
if (lateral > 0) {
// Across the belt, both ways. Swept from `base` so the apron is widened
// too, and in the flattened frame the cross-belt axis is unscaled, so this
// distance is already a true on-belt distance.
const Point t = frame.from_axis == 0 ? Point(0, lateral) : Point(lateral, 0);
ExPolygons widened = union_ex(sweep_ex(base, t), sweep_ex(base, Point(-t.x(), -t.y())));
base = union_ex(base, to_polygons(widened));
}
ExPolygons outer = offset_ex(base, float(brim_width), jtRound, SCALED_RESOLUTION);
expolygons_append(out, diff_ex(outer, envelope));
}
if (has_inner) {
// Holes reversed so a negative offset grows inward, mirroring Brim.cpp.
// No apron here: an apron growing into a hole interior is never useful.
Polygons holes;
for (const ExPolygon &ex : footprint_flat)
polygons_append(holes, ex.holes);
polygons_reverse(holes);
if (! holes.empty()) {
ExPolygons hole_inner = offset_ex(holes, - float(brim_width + object_gap));
ExPolygons hole_outer = offset_ex(holes, - float(object_gap));
expolygons_append(out, intersection_ex(diff_ex(hole_outer, hole_inner), holes));
}
}
return union_ex(out);
}
// ---------------------------------------------------------------- line lattice
std::vector<coord_t> belt_brim_line_positions(coord_t u_lo,
coord_t u_hi,
coord_t pitch_u,
coord_t u_anchor)
{
std::vector<coord_t> out;
if (pitch_u <= 0 || u_hi <= u_lo)
return out;
// Walk the lattice from just below u_lo. Integer arithmetic throughout, so
// the half-open interval needs no epsilon: a point landing exactly on u_hi
// belongs to the next band.
int64_t k = int64_t(std::floor(double(u_lo - u_anchor) / double(pitch_u))) - 1;
while (u_anchor + coord_t(k) * pitch_u < u_lo)
++ k;
for (;; ++ k) {
const coord_t u = u_anchor + coord_t(k) * pitch_u;
if (u >= u_hi)
break;
out.emplace_back(u);
}
return out;
}
// ---------------------------------------------------------------- pipeline
// A band of the belt surface as an explicit box, clamped to `bounds` along the
// shear axis. Deliberately not BeltFloorContext::surface_polygon(): those
// half-planes span +-1000 mm, which is wasteful to clip against and dangerous to
// feed through the flattening scale.
static Polygon band_box(const BoundingBox &bounds, int from_axis, coordf_t u_lo, coordf_t u_hi)
{
coord_t lo = scale_(u_lo);
coord_t hi = scale_(u_hi);
const coord_t bmin = from_axis == 0 ? bounds.min.x() : bounds.min.y();
const coord_t bmax = from_axis == 0 ? bounds.max.x() : bounds.max.y();
lo = std::max(lo, bmin);
hi = std::min(hi, bmax);
Polygon poly;
if (hi <= lo)
return poly;
if (from_axis == 0)
poly.points = { Point(lo, bounds.min.y()), Point(hi, bounds.min.y()),
Point(hi, bounds.max.y()), Point(lo, bounds.max.y()) };
else
poly.points = { Point(bounds.min.x(), lo), Point(bounds.max.x(), lo),
Point(bounds.max.x(), hi), Point(bounds.min.x(), hi) };
return poly;
}
ExPolygons belt_brim_clip_leading_edge(const ExPolygons &region, const BeltBrimFrame &frame, coordf_t u_cut)
{
if (region.empty())
return region;
BoundingBox keep_bb = get_extents(region);
keep_bb.offset(scale_(1.));
const bool low_side = frame.downhill_sign() < 0; // downhill is -u
const Polygon keep = band_box(keep_bb, frame.from_axis,
low_side ? unscale<double>(frame.from_axis == 0 ? keep_bb.min.x() : keep_bb.min.y()) : u_cut,
low_side ? u_cut : unscale<double>(frame.from_axis == 0 ? keep_bb.max.x() : keep_bb.max.y()));
return keep.empty() ? ExPolygons{} : intersection_ex(region, Polygons{ keep });
}
// Everything the per-band line generator needs, gathered once per object.
struct BeltBrimContext
{
BeltFloorContext ctx;
BeltBrimFrame frame;
ExPolygons region; // brim region, object-local slicing XY
BoundingBox region_bbox;
Flow brim_flow;
coord_t pitch_u = 0;
coord_t u_anchor = 0;
double in_plane_pitch = 0.; // mm
};
// Emit the cross-belt brim lines that belong to the band [print_z - height, print_z].
static void belt_brim_band_paths(const BeltBrimContext &bc,
coordf_t print_z,
coordf_t height,
const Polygons &obstacles,
ExtrusionEntityCollection &out,
ExPolygons &areas_out)
{
coordf_t u_lo = bc.ctx.cutoff_u(print_z - height);
coordf_t u_hi = bc.ctx.cutoff_u(print_z);
if (u_lo > u_hi)
std::swap(u_lo, u_hi);
// How wide this band is measured ON the belt, versus one nominal bead.
const double band_in_plane = (u_hi - u_lo) * bc.frame.u_stretch();
// Fraction of the layer height at which a line sits above the belt. Toward the
// downhill edge, so the sheet is reasonably thick while the nozzle stays clear of
// the belt itself.
static constexpr double BAND_CLEARANCE_FRACTION = 0.75;
std::vector<coord_t> us;
double uniform_clearance = 0.; // 0 => derive per line from its own position
double line_pitch = bc.in_plane_pitch;
// One line also serves a band up to half a bead wider than the nominal pitch (a 0.3 mm
// first layer at 45 degrees): its flow is matched to the band, so the bead is that much
// wider. Two lattice lines in such a band would land almost on top of each other.
if (band_in_plane <= 1.5 * bc.in_plane_pitch + EPSILON) {
// Steep belt, which is the normal case: the band is narrower than one bead, so
// exactly one line fits. Place it at a FIXED fraction of the band rather than
// on a nominal-spacing lattice. On a lattice each line lands at an arbitrary
// point in its band, the clearance sweeps [0, height] from band to band, and the
// bead width therefore varies by 2x - visible as ragged, uneven brim lines.
// Anchoring to the band makes the clearance identical everywhere, so every bead
// is the same width.
//
// The spacing is then whatever the bands give (height / sin(tilt) on the belt)
// rather than the nominal bead spacing, so the flow below is matched to THAT
// pitch. Matched flow at the real pitch is what keeps the sheet uniform and
// gap-free; using nominal flow at band spacing would over-feed it.
us.push_back(scale_(bc.ctx.cutoff_u(print_z - BAND_CLEARANCE_FRACTION * height)));
uniform_clearance = BAND_CLEARANCE_FRACTION * height;
line_pitch = band_in_plane;
} else {
// Shallow belt: the band is wider than a bead, so it takes several lines and they
// have to sit on the nominal lattice. Their clearances then differ, and so do
// their widths - unavoidable here, but shallow belts are the rare case.
us = belt_brim_line_positions(scale_(u_lo), scale_(u_hi), bc.pitch_u, bc.u_anchor);
}
if (us.empty())
return;
const Polygons region_polys = to_polygons(bc.region);
// One lattice line at a time: the clearance - and therefore the extrusion
// volume - is a property of the line's u, so the pieces of different lines
// must not be pooled before the flow is resolved.
// Overshoot the region so the clip, not the line's ends, decides the extent.
const coord_t margin = coord_t(SCALED_EPSILON) + 1;
coord_t u_prev = std::numeric_limits<coord_t>::min();
for (coord_t u : us) {
// Nozzle-to-belt clearance for this line. Constant along the line, because the
// belt height depends only on the shear-axis coordinate. Band-anchored lines
// share one clearance by construction; lattice lines (shallow belts, or a first
// layer thick enough that the band is wider than a bead) each get their own.
//
// A lattice line can fall where the belt is only a hair below the band's print_z.
// The bead there would be laid scraping the belt while its flow is sized for a
// taller cell, so it is moved uphill to the same fraction of the band the
// single-line case uses. (The clearance is along slice Z; the real gap under the
// nozzle is clearance x cos(tilt), 0.53 h at 45 degrees for the 0.75 fraction.)
double clearance = uniform_clearance;
if (clearance <= 0.) {
const Point probe = bc.frame.from_axis == 0 ? Point(u, 0) : Point(0, u);
clearance = print_z - bc.ctx.floor_print_z(probe);
if (clearance < BAND_CLEARANCE_FRACTION * height) {
clearance = BAND_CLEARANCE_FRACTION * height;
u = scale_(bc.ctx.cutoff_u(print_z - clearance));
}
clearance = std::min(clearance, height);
}
// A line moved uphill can land on, or almost on, its neighbour; two beads closer
// than half a pitch would be laid into the same cell.
if (u_prev != std::numeric_limits<coord_t>::min() && std::abs(u - u_prev) < bc.pitch_u / 2)
continue;
u_prev = u;
Polyline line;
if (bc.frame.from_axis == 0)
line.points = { Point(u, coord_t(bc.region_bbox.min.y() - margin)),
Point(u, coord_t(bc.region_bbox.max.y() + margin)) };
else
line.points = { Point(coord_t(bc.region_bbox.min.x() - margin), u),
Point(coord_t(bc.region_bbox.max.x() + margin), u) };
Polylines pieces = intersection_pl(Polylines{ line }, region_polys);
if (! obstacles.empty())
pieces = diff_pl(pieces, obstacles);
if (pieces.empty())
continue;
// with_cross_section, not with_height: it reaches the prescribed volume while
// KEEPING the extrusion spacing, so the bead is sized to fill exactly one
// pitch x clearance cell of the sheet.
const Flow f = bc.brim_flow.with_cross_section(float(line_pitch * clearance));
// Footprint of these beads, for the first-layer convex hull and bbox.
for (const Polygon &p : offset(pieces, 0.5f * float(f.scaled_width())))
areas_out.emplace_back(ExPolygon(p));
extrusion_entities_append_paths(out.entities, chain_polylines(std::move(pieces)),
erBrim, f.mm3_per_mm(), f.width(), float(clearance));
}
}
// Union of everything extruded at `print_z` that the brim must keep clear of, expressed
// in `self`'s local slicing frame. Includes `self` itself: its slice at this Z can
// overhang outside the belt footprint and land in the brim ring, which the flattened
// brim_object_gap - a belt-plane separation - does not cover.
//
// SEQUENCING: this reads the layers and support layers of every object on the plate, so
// it must not overlap with another object's support step, which rebuilds them.
// Print::process() therefore generates the belt brims one object after the other once the
// parallel support step is over (PrintObject::generate_belt_brim()), and an object that
// arrives on or leaves the plate invalidates every other object's support step
// (PrintApply.cpp) so the brims are clipped against what is there now.
// `region_bbox` bounds the brim; anything outside it cannot clip a brim line, so whole
// objects are skipped without materialising their polygons. On a typical plate the
// objects do not overlap and every foreign object drops out here, which matters because
// this runs once per band - hundreds of times per object.
static Polygons belt_brim_obstacles(const Print &print, const PrintObject &self,
const BoundingBox &region_bbox, coordf_t print_z, coordf_t tol)
{
const Point shift_self = self.instances().empty() ? Point(0, 0)
: self.instances().front().shift_without_plate_offset();
Polygons out;
for (const PrintObject *o : print.objects()) {
const bool is_self = (o == &self);
for (const PrintInstance &inst : o->instances()) {
const Point delta = inst.shift_without_plate_offset() - shift_self;
if (const Layer *l = o->get_layer_at_printz(print_z, tol)) {
BoundingBox lb = get_extents(l->lslices);
lb.translate(delta.x(), delta.y());
if (lb.overlap(region_bbox)) {
Polygons ps = to_polygons(l->lslices);
for (Polygon &p : ps)
p.translate(delta);
polygons_append(out, std::move(ps));
}
}
if (! is_self)
continue;
if (const SupportLayer *sl = o->get_support_layer_at_printz(print_z, tol)) {
Polygons ps = sl->support_fills.polygons_covered_by_spacing();
for (Polygon &p : ps)
p.translate(delta);
polygons_append(out, std::move(ps));
}
}
}
if (out.size() < 2)
return out; // union_() of 0 or 1 polygons is pure overhead
return union_(out);
}
void make_belt_brim(PrintObject &object)
{
object.clear_belt_brim();
if (! object.has_belt_brim())
return;
const Print &print = *object.print();
BeltBrimContext bc;
if (! bc.ctx.init(object.slicing_parameters(), print.config()))
return;
bc.frame = BeltBrimFrame{ bc.ctx.shear_factor(), bc.ctx.from_axis() };
const size_t nlayers = object.layers().size();
if (nlayers == 0)
return;
// 1. Belt footprint: the union of each layer's slice clipped to that layer's
// own contact band. This is the object's bottom face, which on a belt is
// spread over every layer instead of sitting in layer 0.
ExPolygons footprint_acc;
for (size_t i = 0; i < nlayers; ++ i) {
const Layer &layer = *object.layers()[i];
if (layer.lslices.empty())
continue;
// print_z - height, not the previous layer's print_z: variable layer
// heights make the latter wrong.
coordf_t u_lo = bc.ctx.cutoff_u(layer.print_z - layer.height);
coordf_t u_hi = bc.ctx.cutoff_u(layer.print_z);
if (u_lo > u_hi)
std::swap(u_lo, u_hi);
BoundingBox bb = get_extents(layer.lslices);
bb.offset(scale_(1.));
const Polygon band = band_box(bb, bc.frame.from_axis, u_lo, u_hi);
if (band.empty())
continue;
expolygons_append(footprint_acc, intersection_ex(layer.lslices, Polygons{ band }));
}
const ExPolygons footprint = union_ex(footprint_acc);
if (footprint.empty())
return;
// 2. Brim region, offset in the flattened (true on-belt) metric.
const PrintObjectConfig &cfg = object.config();
bc.brim_flow = print.brim_flow();
const double flow_w = bc.brim_flow.scaled_spacing() * SCALING_FACTOR;
// Quantize to an even number of lines, as the plate brim does.
const coord_t width = scale_(std::floor(cfg.brim_width.value / flow_w / 2) * flow_w * 2);
const coord_t leading = scale_(cfg.leading_brim_length.value);
const coord_t lateral = scale_(cfg.extra_brim_width.value);
const coord_t gap = scale_(cfg.brim_object_gap.value);
// Belt printers collapse Auto / Mouse ear / Painted to outer-only: the auto width
// heuristic and flat ear discs have no meaning on a tilted plane. Leading-edge-only
// is an outer brim too; it is narrowed down to the first contact below.
const BrimType bt = cfg.brim_type.value;
const bool has_outer = bt == btOuterOnly || bt == btOuterAndInner
|| bt == btAutoBrim || bt == btEar || bt == btPainted
|| bt == btLeadingEdgeOnly;
const bool has_inner = bt == btInnerOnly || bt == btOuterAndInner;
bc.region = belt_unflatten(
belt_brim_region(belt_flatten(footprint, bc.frame), has_outer, has_inner,
width, gap, leading, lateral, bc.frame),
bc.frame);
if (bt == btLeadingEdgeOnly && ! bc.region.empty())
// The cut is the uphill edge of the first layer's contact band: everything
// past it belongs to later contacts.
bc.region = belt_brim_clip_leading_edge(bc.region, bc.frame,
bc.ctx.cutoff_u(object.layers().front()->print_z));
if (bc.region.empty())
return;
bc.region_bbox = get_extents(bc.region);
// 3. Line lattice. Fixed pitch in the flattened metric, anchored at the
// footprint's leading-most edge so lines stay collinear across
// disconnected islands and across the apron prologue.
bc.pitch_u = std::max<coord_t>(1, coord_t(bc.brim_flow.scaled_spacing() * bc.frame.cos_tilt()));
bc.in_plane_pitch = unscale<double>(bc.pitch_u) * bc.frame.u_stretch();
{
const BoundingBox fbb = get_extents(footprint);
const bool low_side = bc.frame.shear > 0.;
bc.u_anchor = bc.frame.from_axis == 0 ? (low_side ? fbb.min.x() : fbb.max.x())
: (low_side ? fbb.min.y() : fbb.max.y());
}
// 4. Bands coincident with an object layer.
std::vector<ExtrusionEntityCollection> by_layer(nlayers);
std::vector<ExPolygons> areas_by_layer(nlayers);
for (size_t i = 0; i < nlayers; ++ i) {
const Layer &layer = *object.layers()[i];
const Polygons obstacles = belt_brim_obstacles(print, object, bc.region_bbox, layer.print_z, 0.5 * layer.height);
belt_brim_band_paths(bc, layer.print_z, layer.height, obstacles, by_layer[i], areas_by_layer[i]);
}
// 5. Apron prologue: the part of the region downhill of the object's first
// layer, which has no object layer to ride on.
std::vector<BeltBrimBand> prologue;
{
const Layer &first = *object.layers().front();
const coordf_t h = first.height;
const bool low_side = bc.frame.shear > 0.;
const coord_t u_lead_s = bc.frame.from_axis == 0
? (low_side ? bc.region_bbox.min.x() : bc.region_bbox.max.x())
: (low_side ? bc.region_bbox.min.y() : bc.region_bbox.max.y());
const coordf_t u_lead = unscale<double>(u_lead_s);
// print_z at which the belt surface crosses the region's leading edge.
const coordf_t z_lead = bc.ctx.shear_factor() * u_lead
+ bc.ctx.floor_offset() + bc.ctx.z_shift();
if (h > EPSILON)
for (coordf_t z = first.print_z - h; z > z_lead - h; z -= h) {
const Polygons obstacles = belt_brim_obstacles(print, object, bc.region_bbox, z, 0.5 * h);
BeltBrimBand band;
band.print_z = z;
band.height = h;
belt_brim_band_paths(bc, z, h, obstacles, band.fills, band.areas);
if (! band.fills.empty())
prologue.emplace_back(std::move(band));
}
// Lowest Z first, so collect_layers_to_print sees them in print order.
std::reverse(prologue.begin(), prologue.end());
}
object.set_belt_brim(std::move(by_layer), std::move(areas_by_layer), std::move(prologue));
}
} // namespace Slic3r