From 767db71500ddd3098baf758adcf78a60bb0b4c9a Mon Sep 17 00:00:00 2001 From: harrierpigeon Date: Tue, 8 Sep 2026 23:58:47 -0500 Subject: [PATCH] Belt: fix three tree-support bugs, one of which blocked slicing entirely 1. Belt tree support could not slice at all. layer_initialize() hardcodes layer 0's bottom_z to 0, encoding "below layer 0 is the build plate at z = 0". True for a flat bed; false for a belt, whose virtual support layers legitimately extend below zero. The bottom-most belt layer therefore got height = print_z - 0 = -9.8, which reached Flow::with_height() and threw FlowErrorNegativeFlow. A 3DBenchy, a mushroom, an L-bracket and an extruded L all failed identically with negative flow / return -100. Only a bare cube sliced, because its support never reached that far down. The bottom is now taken from the previous layer's z, and only a layer 0 whose print_z is itself negative gets a synthesised bottom below it. Every non-negative print_z -- every non-belt configuration -- keeps exactly the previous 0, so this is behaviour-preserving off a belt by construction. An earlier form used min(0., layer_z(0) - layer_height), which regressed flat beds whenever the initial layer was thinner than the layer height. 3DBenchy on a 45-degree belt with organic tree support: fails to slice -> 247 support blocks / 168,596 extrusions. 2. Support generated against the belt, and against belt-tilted walls. A plain 20mm cube on a 45-degree belt generated 86 support blocks and 46,307 support extrusions. Three causes, all gated on the belt floor being active: a. The build-plate tilt compensation shifted the lower layer the wrong way. tan(build_plate_tilt_*) carries a magnitude but no direction, and the sign chosen moved the lower layer away from the newly appearing material rather than under it, doubling the mismatch. The shift now comes from belt_floor_shear_factor / belt_floor_from_axis, which carry sign and axis exactly. Non-belt tilted beds keep the previous behaviour. b. Material resting on the belt was treated as unsupported. The belt surface is now unioned into the effective lower layer, sampled at the bottom of the layer -- a layer meets the belt across its thickness and print_z is the top. The half-plane is clipped to the layer's bounding box first: unioning a +/-1000mm half-plane raw with 20mm-scale geometry put a huge dynamic range through Clipper and left intermittent artefacts every few layers. c. The object's first slice can be empty on a belt (the bottom vertex is a sub-extrudable sliver), leaving the layer above with an empty predecessor even though it rests on the belt. (b) already covers that per island. What did need fixing is sharp-tail detection, which tests each island against the raw lower slices; with an empty predecessor that test is trivially true and every belt-contact island read as a sharp tail. It now tests against the same effective lower layer. An earlier form instead skipped the whole layer when the point of get_extents(curr_polys) -- the bounding box of the union of every island -- nearest the belt was in contact. That was wrong in a way worth recording: one island resting on the belt could suppress overhang and sharp-tail detection for a separate island floating well above it. Every decision here is per-island. Cube on belt: 46,307 -> 0 support extrusions. Same cube non-belt: 0 before and after. Benchy on belt still 247 blocks / 168,596 extrusions and a mushroom 111 / 82,157, so false positives are removed without suppressing true ones. Non-belt is unchanged by measurement, not only by the belt_ovh_active gate: the same mushroom sliced on a Cartesian printer before and after gives 65,866 support extrusions and 68,717 total extrusions both times, the two G-code files differing in exactly one line -- the object's plate position. 3. m_anti_overhang was filled and read in different index spaces. It is consumed in the same index space as m_layer_outlines, where object layer i lives at num_raft_layers + i, but was filled in object-layer space. Every entry landed num_raft_layers too low (50 for a 20mm cube at bed Y=50) and the topmost object layers got none. The belt injection also ran before m_raft_layers was extended, so it could not have known the offset. The array is now shifted as a whole and the injection moved after the raft extension. This also repairs user support blockers under a raft, which is not belt-specific: it changes behaviour for any ordinary raft, not just the belt's virtual one, and should be reviewed as a general fix. Measured effect on the cube was small on its own (46,307 -> 46,334 before the other fixes) because m_anti_overhang only feeds calculate_placable; kept as a correctness fix on its own merits. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_011jgzj1sf53KMLPweZ8yeUQ --- src/libslic3r/Support/TreeModelVolumes.cpp | 55 ++++++++------- src/libslic3r/Support/TreeSupport.cpp | 75 +++++++++++++++++++-- src/libslic3r/Support/TreeSupportCommon.hpp | 17 ++++- 3 files changed, 117 insertions(+), 30 deletions(-) diff --git a/src/libslic3r/Support/TreeModelVolumes.cpp b/src/libslic3r/Support/TreeModelVolumes.cpp index b1e3cdd6dd..e56682fd3c 100644 --- a/src/libslic3r/Support/TreeModelVolumes.cpp +++ b/src/libslic3r/Support/TreeModelVolumes.cpp @@ -95,29 +95,6 @@ TreeModelVolumes::TreeModelVolumes( #else { m_anti_overhang = print_object.slice_support_blockers(); - // Belt floor: add belt surface polygons to anti_overhang so support - // is never generated inside the belt. Only in global shear mode — - // in local mode the belt floor clipping handles everything and - // anti_overhang at the bottom layers would block all support. - { - const auto &sp = print_object.slicing_parameters(); - const auto &pcfg = print_object.print()->config(); - BeltFloorContext ctx; - ctx.init_local(sp, pcfg, print_object.belt_global_z_offset()); - if (ctx.is_active() - && std::abs(print_object.belt_global_z_offset()) > EPSILON - && pcfg.belt_support_floor_mode.value == BeltSupportFloorMode::GeneratorOnly) { - size_t num_layers_needed = print_object.layer_count(); - // Ensure m_anti_overhang is large enough. - if (m_anti_overhang.size() < num_layers_needed) - m_anti_overhang.resize(num_layers_needed, Polygons{}); - for (size_t layer_idx = 0; layer_idx < num_layers_needed; ++layer_idx) { - double print_z = print_object.get_layer(layer_idx)->print_z - - print_object.belt_global_z_offset(); - append(m_anti_overhang[layer_idx], ctx.surface_polygon(print_z)); - } - } - } TreeSupportMeshGroupSettings mesh_settings(print_object); const TreeSupportSettings config{ mesh_settings, print_object.slicing_parameters() }; m_current_min_xy_dist = config.xy_min_distance; @@ -147,6 +124,38 @@ TreeModelVolumes::TreeModelVolumes( } } } + // Belt floor: add belt surface polygons to anti_overhang so support is + // never generated inside the belt. + // + // This MUST run after m_raft_layers is final. m_anti_overhang is consumed + // in the same index space as m_layer_outlines -- object layer i lives at + // index num_raft_layers + i -- but slice_support_blockers() returns it in + // object-layer space. Without the shift below, every entry lands + // num_raft_layers too low: with the belt raft that is tens of layers, so + // the belt suppression is applied to the wrong layers entirely and the + // topmost object layers get none at all. + { + const size_t num_raft = m_raft_layers.size(); + const size_t num_obj = print_object.layer_count(); + if (num_raft > 0 && ! m_anti_overhang.empty()) + // Shift the support blockers into the same space. + m_anti_overhang.insert(m_anti_overhang.begin(), num_raft, Polygons{}); + const auto &sp = print_object.slicing_parameters(); + const auto &pcfg = print_object.print()->config(); + BeltFloorContext ctx; + ctx.init_local(sp, pcfg, print_object.belt_global_z_offset()); + if (ctx.is_active() + && std::abs(print_object.belt_global_z_offset()) > EPSILON + && pcfg.belt_support_floor_mode.value == BeltSupportFloorMode::GeneratorOnly) { + if (m_anti_overhang.size() < num_raft + num_obj) + m_anti_overhang.resize(num_raft + num_obj, Polygons{}); + for (size_t i = 0; i < num_obj; ++i) { + const double print_z = print_object.get_layer(i)->print_z + - print_object.belt_global_z_offset(); + append(m_anti_overhang[num_raft + i], ctx.surface_polygon(print_z)); + } + } + } m_current_outline_idx = 0; m_layer_outlines.emplace_back(mesh_settings, std::vector{}); diff --git a/src/libslic3r/Support/TreeSupport.cpp b/src/libslic3r/Support/TreeSupport.cpp index 3b7743407c..d472681c41 100644 --- a/src/libslic3r/Support/TreeSupport.cpp +++ b/src/libslic3r/Support/TreeSupport.cpp @@ -13,6 +13,7 @@ #include "SVG.hpp" #include "TreeSupportCommon.hpp" #include "TreeSupport.hpp" +#include #include "TreeSupport3D.hpp" #include "BeltFloorContext.hpp" #include @@ -712,6 +713,17 @@ void TreeSupport::detect_overhangs(bool check_support_necessity/* = false*/) const double tilt_x_rad = Geometry::deg2rad(print_cfg.build_plate_tilt_x.value); const double tilt_y_rad = Geometry::deg2rad(print_cfg.build_plate_tilt_y.value); const bool has_tilt = std::abs(tilt_x_rad) > EPSILON || std::abs(tilt_y_rad) > EPSILON; + + // Belt printers: the object is pre-rotated by the belt angle before slicing, so a wall + // that is vertical in the world advances by one layer height per layer in the sliced + // frame. The build-plate tilt shift above compensates for that, but its direction has to + // follow the belt shear -- the sign and axis are already known exactly from the slicing + // parameters, so take them from there rather than from tan(build_plate_tilt), which + // carries a magnitude but no direction. Non-belt tilted beds keep the existing behaviour. + BeltFloorContext ovh_belt_ctx; + const bool belt_ovh_active = ovh_belt_ctx.init(m_slicing_params, print_cfg); + const double belt_shear = ovh_belt_ctx.shear_factor(); + const int belt_axis = ovh_belt_ctx.from_axis(); // FIXME this is a fudge constant! double support_tree_tip_diameter = 0.8; auto enforcer_overhang_offset = scaled(support_tree_tip_diameter); @@ -855,15 +867,61 @@ void TreeSupport::detect_overhangs(bool check_support_necessity/* = false*/) ExPolygons& lower_polys = lower_layer->lslices_extrudable; // Apply build plate tilt: shift lower layer polygons to simulate tilted gravity + // + // On a belt the object's very first slice can come out empty (the bottom + // vertex is a sub-extrudable sliver), leaving the layer above it with an + // empty predecessor even though it rests on the belt. That case needs no + // special handling here: the belt surface is unioned into effective_lower + // below and sampled at the bottom of the layer, so a contacting island is + // covered and a genuinely floating one still reports its overhang. Doing it + // that way keeps the decision per-island -- an earlier whole-layer skip, + // conditioned on the nearest point of the *union* of the cross-section, + // let one contacting island silence a separate floating one. ExPolygons shifted_lower; - if (has_tilt) { + if (belt_ovh_active || has_tilt) { shifted_lower = lower_polys; // copy const double lh = lower_layer->height; - Point tilt_shift(coord_t(scale_(lh * tan(tilt_y_rad))), - coord_t(scale_(lh * tan(tilt_x_rad)))); + Point tilt_shift(0, 0); + if (belt_ovh_active) { + // Advance the lower layer along the belt by exactly the amount a + // world-vertical wall moves per layer, so such a wall stops reading + // as an overhang. Sign comes from the shear, not from a tilt angle. + const coord_t d = coord_t(-scale_(lh * belt_shear)); + if (belt_axis == 0) tilt_shift.x() = d; else tilt_shift.y() = d; + } else { + tilt_shift = Point(coord_t(scale_(lh * tan(tilt_y_rad))), + coord_t(scale_(lh * tan(tilt_x_rad)))); + } translate(shifted_lower, tilt_shift); } - const ExPolygons &effective_lower = has_tilt ? shifted_lower : lower_polys; + ExPolygons effective_lower = (belt_ovh_active || has_tilt) ? shifted_lower : lower_polys; + + // Belt printers: material resting on the belt is held up by the belt, not by + // the layer below it, so the belt surface counts as support from underneath. + // Without this the object's belt-contact face reads as a fresh overhang on + // every layer -- the leading strip that produced the spurious support nub. + if (belt_ovh_active) { + // surface_polygon() is a +/-1000mm half-plane. Unioning that raw with + // 20mm-scale geometry and then offsetting it puts a huge dynamic range + // through Clipper, which left intermittent artefacts every few layers. + // Clip it to the layer's own bounding box first. + // Evaluate the belt surface at the BOTTOM of the layer, not its top: + // a layer meets the belt across its whole thickness, and print_z is the + // top. On the object's first layer -- which is thicker, and whose lower + // layer is empty -- using print_z left the leading 0.37mm uncovered and + // produced the one remaining spurious overhang. + Polygons belt_surface = ovh_belt_ctx.surface_polygon(layer->print_z - layer->height); + if (! belt_surface.empty()) { + BoundingBox clip_bb = get_extents(curr_polys); + clip_bb.merge(get_extents(lower_polys)); + clip_bb.offset(scale_(10.)); + belt_surface = intersection(belt_surface, Polygons{ clip_bb.polygon() }); + if (! belt_surface.empty()) { + append(effective_lower, union_ex(belt_surface)); + effective_lower = union_ex(effective_lower); + } + } + } // normal overhang ExPolygons lower_layer_offseted = offset_ex(effective_lower, support_offset_scaled, SUPPORT_SURFACES_OFFSET_PARAMETERS); @@ -882,8 +940,13 @@ void TreeSupport::detect_overhangs(bool check_support_necessity/* = false*/) for (const ExPolygon& expoly : curr_polys) { bool is_sharp_tail = false; // 1. nothing below - // this is a sharp tail region if it's floating and non-ignorable - if (!overlaps(offset_ex(expoly, 0.1 * extrusion_width_scaled), lower_polys)) { + // this is a sharp tail region if it's floating and non-ignorable. + // On a belt, "below" has to include the belt itself and the + // shear-advanced lower layer, or every belt-contact island reads as + // a sharp tail -- which is what the empty-predecessor skip above was + // really masking. effective_lower is exactly that notion of below. + const ExPolygons &tail_lower = belt_ovh_active ? effective_lower : lower_polys; + if (!overlaps(offset_ex(expoly, 0.1 * extrusion_width_scaled), tail_lower)) { is_sharp_tail = !offset_ex(expoly, -0.1 * extrusion_width_scaled).empty(); } diff --git a/src/libslic3r/Support/TreeSupportCommon.hpp b/src/libslic3r/Support/TreeSupportCommon.hpp index 07dcb324b8..979adcd9ed 100644 --- a/src/libslic3r/Support/TreeSupportCommon.hpp +++ b/src/libslic3r/Support/TreeSupportCommon.hpp @@ -651,7 +651,22 @@ inline SupportGeneratorLayer& layer_initialize( const size_t layer_idx) { layer_new.print_z = layer_z(slicing_params, config, layer_idx); - layer_new.bottom_z = layer_idx > 0 ? layer_z(slicing_params, config, layer_idx - 1) : 0; + // Layer 0 has no layer below it, so its bottom is the build plate at z = 0 -- + // true for a flat bed, false for a belt, whose virtual support layers extend + // below zero. Taking 0 there made the bottom-most belt layer's height come out + // as its own (negative) print_z, which reached Flow::with_height() and threw + // FlowErrorNegativeFlow, so tree support could not slice any belt model whose + // branches reached down that far. + // + // Only the negative case is corrected. An earlier version used + // min(0, print_z - layer_height), which also fires whenever the initial layer + // is THINNER than the regular layer height -- e.g. 0.2 over 0.3, both + // independently configurable -- and silently changed flat-bed support layer + // heights. Keying on the sign leaves every non-negative print_z on exactly + // the previous value of 0. + layer_new.bottom_z = layer_idx > 0 ? layer_z(slicing_params, config, layer_idx - 1) : 0.; + if (layer_idx == 0 && layer_new.print_z < 0.) + layer_new.bottom_z = layer_new.print_z - slicing_params.layer_height; layer_new.height = layer_new.print_z - layer_new.bottom_z; return layer_new; }