Belt Printing: Bugfix: Solid Organic Tree Base, Slim Tree Skirt, Renderer (#14395)

* fix tree support brim
* treesupport3d part 1: more diagnostic logging.  (todo once things are fixed: remove this / gate it properly)
* make area under Z=0 in rotated slice pipeline not solid
* fix solid Z=0 layer for belt printers
* fix renderer
* clean up logging
* final review pass
This commit is contained in:
Joseph Robertson
2026-06-24 22:01:29 -05:00
committed by GitHub
parent 0ef7c6d581
commit 2ca843a38e
5 changed files with 121 additions and 31 deletions

View File

@@ -1775,7 +1775,14 @@ void generate_support_toolpaths(
bool sheath = support_params.with_sheath;
bool no_sort = false;
bool done = false;
if (base_layer.layer->bottom_z < EPSILON) {
// Belt printers have no flat bed first layer — the belt is the tilted
// build surface — so the dense raft_first_layer_density flange must not
// fire anywhere, including the layer at z=0 (the belt-surface line).
// (belt_floor_shear_factor is non-zero only when belt_printer is on.)
// For every other printer type, support z is never negative, so this
// matches the original "first layer at z=0" behaviour unchanged.
const bool is_belt_printer = std::abs(slicing_params.belt_floor_shear_factor) > EPSILON;
if (! is_belt_printer && base_layer.layer->bottom_z < EPSILON) {
// Base flange (the 1st layer).
filler = filler_first_layer;
filler->angle = Geometry::deg2rad(float(config.support_angle.value + 90.));

View File

@@ -172,11 +172,17 @@ TreeModelVolumes::TreeModelVolumes(
if (ctx.is_active()
&& pcfg2.belt_support_floor_mode.value == BeltSupportFloorMode::GeneratorOnly) {
m_belt_floor = ctx.compute_per_layer_floors(num_layers, [&](size_t layer_idx) -> double {
// Use local print_z (subtract global offset from object layer).
return (layer_idx >= num_raft_layers)
? print_object.get_layer(layer_idx - num_raft_layers)->print_z
- print_object.belt_global_z_offset()
: 0.;
// Object layers: local print_z (subtract global offset).
if (layer_idx >= num_raft_layers)
return print_object.get_layer(layer_idx - num_raft_layers)->print_z
- print_object.belt_global_z_offset();
// Belt raft layers (below the object): each carries its own
// local print_z in m_raft_layers. The belt floor is a tilted
// plane, so the half-plane to clip grows as print_z drops —
// using 0 here clipped every below-object layer against the
// Z=0 belt surface, under-clipping the raft region and leaving
// a dense support mass below the floor.
return (layer_idx < m_raft_layers.size()) ? m_raft_layers[layer_idx] : 0.;
});
}
}

View File

@@ -1506,6 +1506,16 @@ void TreeSupport::generate_toolpaths()
// ORCA: base angle used for explicit interlaced interface orientation.
const float base_support_angle = Geometry::deg2rad(object_config.support_angle.value);
// Belt floor: the lowest support layer rests on the moving, tilted belt, not
// on a flat bed — so it must NOT get the bed first-layer treatment (a brim on
// interface areas, a first-layer-flow sheath at raft_first_layer_density on
// base areas). That treatment draws a loop along the Z=0 belt-floor line that
// reads as a stray brim/skirt. Gate those layer_id==0 special cases off when
// the belt floor is active; false on non-belt printers so behavior is unchanged.
BeltFloorContext belt_ctx;
const bool belt_floor_active = belt_ctx.init(m_slicing_params, *m_print_config)
&& m_print_config->belt_support_floor_mode.value == BeltSupportFloorMode::GeneratorOnly;
// generate tree support tool paths
tbb::parallel_for(
tbb::blocked_range<size_t>(m_raft_layers, m_object->support_layer_count()),
@@ -1539,7 +1549,7 @@ void TreeSupport::generate_toolpaths()
filler_interface->angle = base_support_angle + M_PI_2; // default interface angle is perpendicular to support angle
if (area_group.type != SupportLayer::BaseType) {
// interface
if (layer_id == 0) {
if (layer_id == 0 && !belt_floor_active) {
Flow flow = m_raft_layers == 0 ? m_object->print()->brim_flow() : support_flow;
ExtrusionRole brim_role = (area_group.type == SupportLayer::RoofType && !area_group.interface_as_base) ?
erSupportMaterialInterface : erSupportMaterial;
@@ -1623,7 +1633,7 @@ void TreeSupport::generate_toolpaths()
}
else {
// base_areas
bool support_base_on_bed = (layer_id == 0 && m_raft_layers == 0);
bool support_base_on_bed = (layer_id == 0 && m_raft_layers == 0 && !belt_floor_active);
Flow flow = support_base_on_bed ? m_support_params.first_layer_flow : support_flow;
bool need_infill = with_infill;
if(m_object_config->support_base_pattern==smpDefault)
@@ -1644,7 +1654,7 @@ void TreeSupport::generate_toolpaths()
std::unique_ptr<ExtrusionEntityCollection> base_eec = std::make_unique<ExtrusionEntityCollection>();
base_eec->no_sort = true;
ExtrusionEntitiesPtr &base_dst = base_eec->entities;
if (layer_id == 0) {
if (layer_id == 0 && !belt_floor_active) {
float density = float(m_object_config->raft_first_layer_density.value * 0.01);
fill_expolygons_with_sheath_generate_paths(base_dst, loops, filler_support.get(), density, erSupportMaterial, flow,
m_support_params, true, false);
@@ -1916,9 +1926,6 @@ void TreeSupport::generate()
if (!belt_ext_layers.empty()) {
auto &sl_vec = m_object->support_layers();
sl_vec.insert(sl_vec.begin(), belt_ext_layers.begin(), belt_ext_layers.end());
BOOST_LOG_TRIVIAL(debug) << "[BELT-CALIB] wedge ext layers=" << belt_ext_layers.size()
<< " z=" << belt_ext_layers.front()->print_z << ".." << belt_ext_layers.back()->print_z
<< " seeded=" << seeded;
}
}
}
@@ -2177,6 +2184,16 @@ void TreeSupport::draw_circles()
coordf_t support_extrusion_width = m_support_params.support_extrusion_width;
const float tree_brim_width = config.tree_support_brim_width.value;
// Belt floor: the first object layer is not on a flat bed — it rests on the
// tilted, moving belt. So the first-object-layer adhesion features (the tree
// support brim, the hybrid first-layer base expansion) must be suppressed:
// their expanded contact rings project to a stray brim/skirt loop sitting in
// the Z=0 belt plane around the support footprint. false on non-belt printers,
// so behavior there is unchanged.
BeltFloorContext belt_ctx;
const bool belt_floor_active = belt_ctx.init(m_slicing_params, *m_print_config)
&& m_print_config->belt_support_floor_mode.value == BeltSupportFloorMode::GeneratorOnly;
if (m_object->support_layer_count() <= m_raft_layers)
return;
BOOST_LOG_TRIVIAL(info) << "draw_circles for object: " << m_object->model_object()->name;
@@ -2287,7 +2304,7 @@ void TreeSupport::draw_circles()
circle.points[i] = circle.points[i] * scale + node.position;
}
}
if (obj_layer_nr == 0 && m_raft_layers == 0) {
if (obj_layer_nr == 0 && m_raft_layers == 0 && !belt_floor_active) {
double brim_width = !config.tree_support_auto_brim ? tree_brim_width : std::max(MIN_BRANCH_RADIUS_FIRST_LAYER, std::min(node.radius + node.dist_mm_to_top / (scale * branch_radius) * 0.5, MAX_BRANCH_RADIUS_FIRST_LAYER) - node.radius);
auto tmp=offset(circle, scale_(brim_width));
if(!tmp.empty())
@@ -2352,12 +2369,19 @@ void TreeSupport::draw_circles()
base_areas = intersection_ex(base_areas, m_machine_border);
// Belt floor: clip tree support polygons by the belt surface plane.
// ts_layer->print_z is at LOCAL Z (global offset applied later in
// _generate_support_material), so use init_local to subtract
// belt_global_z_offset from z_shift.
// Non-organic tree support layers inherit their print_z from the
// (already globally-offset) object layers — see plan_layer_heights()
// and add_tree_support_layer(); only ORGANIC layers get the global
// Z offset applied later in _generate_support_material(). So here
// ts_layer->print_z is in the GLOBAL frame and we must use init()
// (global), not init_local(): mixing a local-frame clip plane with
// a global print_z displaces the cutoff line by belt_global_z_offset
// along the shear axis, leaving an un-clipped wedge of support below
// the belt floor. In per-object (non-global) mode belt_global_z_offset
// is 0 so init() and init_local() coincide — this is a no-op there.
{
BeltFloorContext ctx;
if (ctx.init_local(m_slicing_params, *m_print_config, m_object->belt_global_z_offset())
if (ctx.init(m_slicing_params, *m_print_config)
&& m_print_config->belt_support_floor_mode.value == BeltSupportFloorMode::GeneratorOnly) {
Polygons belt_surface = ctx.surface_polygon(ts_layer->print_z);
base_areas = diff_ex(base_areas, belt_surface);
@@ -2500,7 +2524,7 @@ void TreeSupport::draw_circles()
// part. area_poly is collected from ePolygon nodes above, which are the normal
// support nodes in Hybrid mode. Apply the expansion before area_groups and
// lslices are built so toolpaths and brim avoidance use the same footprint.
if (layer_nr == 0 && m_raft_layers == 0 && m_support_params.support_style == smsTreeHybrid &&
if (layer_nr == 0 && m_raft_layers == 0 && !belt_floor_active && m_support_params.support_style == smsTreeHybrid &&
m_object_config->raft_first_layer_expansion.value > 0.f) {
ExPolygons expanded_base_areas;
const float inflate_factor_1st_layer = float(scale_(m_object_config->raft_first_layer_expansion.value));

View File

@@ -4016,12 +4016,20 @@ void organic_draw_branches(
std::vector<BottomExtraSlice> bottom_extra_slices;
Polygons rest_support;
coord_t bottom_radius = support_element_radius(config, *branch.path.front());
// Belt printer (GeneratorOnly belt floor): the tilted belt surface is the
// build surface, so a branch should terminate ON the belt with a thin tip,
// not stamp its full footprint straight down to Z=0 and weld neighbouring
// branches into a solid floor slab. m_belt_floor is only populated in that
// mode, so it doubles as the gate (no effect on other printer types).
const bool belt_mode = !volumes.m_belt_floor.empty();
// Don't propagate further than 1.5 * bottom radius.
//LayerIndex layers_propagate_max = 2 * bottom_radius / config.layer_height;
LayerIndex layers_propagate_max = 5 * bottom_radius / config.layer_height;
LayerIndex layer_bottommost = branch.path.front()->state.verylost ?
LayerIndex layer_bottommost = (branch.path.front()->state.verylost && !belt_mode) ?
// If the tree bottom is hanging in the air, bring it down to some surface.
0 :
// In belt mode never force-drop to Z=0 (the belt clip below handles
// termination); otherwise the "verylost" branch welds into the slab.
//FIXME the "verylost" branches should stop when crossing another support.
std::max(0, layer_begin - layers_propagate_max);
double support_area_min_radius = M_PI * sqr(double(config.branch_radius));
@@ -4033,14 +4041,28 @@ void organic_draw_branches(
Polygons collision = volumes.getCollision(0, collision_layer, false);
rest_support = diff_clipped(rest_support.empty() ? slice_front_contact : rest_support, collision, ApplySafetyOffset::Yes);
// Belt floor: clip propagated support at belt surface.
if (layer_idx < LayerIndex(volumes.m_belt_floor.size()) && !volumes.m_belt_floor[layer_idx].empty())
bool belt_cut = false;
if (layer_idx < LayerIndex(volumes.m_belt_floor.size()) && !volumes.m_belt_floor[layer_idx].empty()) {
double area_before = area(rest_support);
rest_support = diff(rest_support, volumes.m_belt_floor[layer_idx]);
// The belt counts as "reached" only when it actually removes part
// of this branch's footprint. The belt half-plane is non-empty at
// every near-belt layer, so testing non-emptiness alone would
// terminate a laterally-distant branch ~1 layer above true contact,
// leaving a gap. Require a real area reduction instead.
belt_cut = belt_mode && area(rest_support) < area_before - tiny_area;
}
remove_small(rest_support, tiny_area);
double rest_support_area = area(rest_support);
if (rest_support_area < support_area_stop)
// Don't propagate a fraction of the tree contact surface.
break;
bottom_extra_slices.push_back({ rest_support, rest_support_area });
// Belt mode: once the belt surface actually starts cutting this branch
// it has reached the belt — keep this last (belt-clipped) slice as the
// contact and stop, rather than stamping the footprint further down.
if (belt_cut)
break;
}
// Now remove those bottom slices that are not supported at all.
#if 0
@@ -4058,7 +4080,10 @@ void organic_draw_branches(
}
}
#endif
if (config.settings.support_floor_layers > 0) {
// Belt mode: no solid support-floor pad under these branches — it is what
// welds neighbouring belt-terminating branches into the dense Z=0 slab.
// They simply taper out onto the tilted belt as distributed thin contacts.
if (!belt_mode && config.settings.support_floor_layers > 0) {
Polygons contacts;
if (!bottom_extra_slices.empty()) {
const int contact_idx = int(bottom_extra_slices.size()) - 1; // Use the lowest contact slice as the footprint.
@@ -4097,7 +4122,10 @@ void organic_draw_branches(
}
// ORCA: retain bottom contacts even when no placeable areas intersect.
if (branch.has_root && config.support_rests_on_model && branch.path.front()->state.layer_idx > 0 &&
// Skipped in belt mode (m_belt_floor populated) so we don't re-introduce a
// solid floor pad for branches that terminate on the tilted belt surface.
if (volumes.m_belt_floor.empty() &&
branch.has_root && config.support_rests_on_model && branch.path.front()->state.layer_idx > 0 &&
config.settings.support_floor_layers > 0 && config.z_distance_bottom_layers > 0 &&
bottom_contacts.empty() && !slice_front_contact.empty())
bottom_contacts.emplace_back(slice_front_contact);

View File

@@ -1289,16 +1289,41 @@ void GCodeViewer::load_as_gcode(const GCodeProcessorResult& gcode_result, const
size_t n_filtered = 0, n_clip = 0;
const double y_lo = model_bb.defined ? model_bb.min.y() - ANCHOR_CLIP_MARGIN_MM : -1e30;
const double y_hi = model_bb.defined ? model_bb.max.y() + ANCHOR_CLIP_MARGIN_MM : 1e30;
for (const GCodeProcessorResult::MoveVertex& mv : gcode_result.moves)
if (mv.type == EMoveType::Extrude && mv.layer_id >= 1) { // skip layer-0 prime/skirt
++n_filtered;
const Vec3d p = belt_inv * mv.position.cast<double>();
tp_bb_full.merge(p);
if (p.y() >= y_lo && p.y() <= y_hi) {
tp_bb_clip.merge(p);
++n_clip;
// The anchor aligns the toolpath body onto the object-only model bbox, so it must be
// built from OBJECT-body toolpaths only. Support, skirt, brim and wipe-tower extrusions
// are not part of the model mesh and extend past it (support especially reaches well
// beyond the object on a belt), so including them drags tp_bb.min and shifts the whole
// back-transformed g-code off the mesh — but only when those features are present. That
// is the "g-code shifts vs mesh as soon as supports are enabled" bug. Filter them out.
auto is_object_body = [](ExtrusionRole r) {
return r != erSupportMaterial && r != erSupportMaterialInterface
&& r != erSupportTransition
&& r != erSkirt && r != erBrim && r != erWipeTower;
};
auto accumulate = [&](bool body_only) {
tp_bb_clip = BoundingBoxf3();
tp_bb_full = BoundingBoxf3();
n_filtered = 0;
n_clip = 0;
for (const GCodeProcessorResult::MoveVertex& mv : gcode_result.moves)
if (mv.type == EMoveType::Extrude && mv.layer_id >= 1 // skip layer-0 prime/skirt
&& (!body_only || is_object_body(mv.extrusion_role))) {
++n_filtered;
const Vec3d p = belt_inv * mv.position.cast<double>();
tp_bb_full.merge(p);
if (p.y() >= y_lo && p.y() <= y_hi) {
tp_bb_clip.merge(p);
++n_clip;
}
}
}
};
accumulate(/*body_only=*/true);
// Fallback: a plate with no object-body extrusions above layer 0 (e.g. a
// support-only object) would leave tp_bb undefined and silently skip the
// anchor. Re-accumulate over ALL extrude moves so the min-corner anchor is
// still computed rather than letting the preview float by the placement offset.
if (n_filtered == 0)
accumulate(/*body_only=*/false);
// Use the clipped bbox when it still holds the bulk of the moves (outliers removed).
// If the object sits far from the belt entry the toolpaths are grossly offset and the
// clip drops most of them — fall back to the full bbox so the min-corner anchor still