Belt printer: supports reach the belt under a leading overhang

The slicing frame of a belt object started at its lowest vertex, but the
belt under the leading end of an overhang lies below that, by the
overhang's length times the tilt's shear.  Every support generator works
in layers at z >= 0, so none of them could reach it: normal supports
stopped at the object's own lowest layer, and the two tree generators
each carried a stack of hacks to extend themselves below it (a post-hoc
copy of the lowest base area in TreeSupport, "virtual belt raft layers"
in TreeSupport3D/TreeModelVolumes), sized from the pre-rotation bbox
and capped at global z = 0, which is only right for the trailing half
of the belt.

Start the frame at the lowest belt-floor point under the footprint
instead, less a 10 mm margin along the belt for the base of a support
column (BeltSliceStrategy::apply_preslice_transforms and
BeltTransformPipeline::compute_belt_height_and_floor agree on it).  The
layers between it and the first vertex come out empty, which belt
slicing already tolerates, and the generators need no extension at all:

- normal supports: the generator anchors its layer grid at the frame
  origin, so run it in the object frame and shift the global belt Z
  offset onto the result afterwards, as organic supports already did.
  With the offset on the object layers a top contact at negative z
  turned the intermediate-layer count negative and the generator
  allocated layers until the kernel killed it (any overhang in the
  leading half of the belt).  Drop the first-layer flange expansion on a
  belt: the first support layer is the leading tip of the support, not
  a flange, and inflating it put lines in the air ahead of the belt.
- classic tree: a node now keeps dropping until its whole circle is in
  the belt, so the branch tapers to a tip on the belt instead of
  stopping, a radius above it, when its centre crosses.
- organic: the belt is no longer a support blocker.  A blocker is a
  collision, and a branch descending onto one slides off it, down the
  tilted belt and ahead of the part; the belt is where branches end,
  which the per-layer m_belt_floor clipping already does.

The belt brim is generated after the parallel support step instead of
inside it: belt_brim_obstacles() reads every object's layers and support
layers, which another object's support step rebuilds (and, now, shifts)
at the same time.  This is the race behind the Windows arm64 segfault
in "Belt brim of each object precedes its perimeters on its own
filament".

Also: the belt tilt axis moves to Developer mode as its own row (a
shared line is shown by its first option's mode), first_layer_plane
band thickness, belt_support_floor_mode, belt_preslice_global and
gcode_back_transform are retired and presumed on, the gravity arrow is
a plain line along the up direction, and the "Show raw G-code (belt
only)" preview toggle is gone.

Regression test: "Belt supports reach the belt under a leading
overhang" slices a cube with a fin whose underside is parallel to the
layers, 20 mm ahead of the cube and up to 41 mm of slicing Z above the
belt, for normal, organic and classic tree supports, and checks that
the lowest support layer sits on the belt beneath its own lines.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
harrierpigeon
2026-10-06 22:14:25 -05:00
co-authored by Claude Fable 5.1
parent 8039d4d2ac
commit dda58b07cd
37 changed files with 412 additions and 618 deletions
+4 -8
View File
@@ -25,7 +25,6 @@ void BeltGCode::write_belt_header(GCodeOutputStream &file, const Print &print)
// for the physical tilt the G-code viewer uses to enable belt view.
file.write_format("; belt_slice_rotation = %s\n", full_cfg.opt_serialize("belt_slice_rotation").c_str());
file.write_format("; belt_slice_rotation_angle = %.1f\n", print.config().belt_slice_rotation_angle.value);
file.write_format("; belt_preslice_global = %d\n", print.config().belt_preslice_global.value ? 1 : 0);
// Machine-frame transform: shear (tan) + scale (1/cos) derived from the belt
// tilt angle (or belt_frame_tilt_angle when decoupled).
file.write_format("; belt_frame_tilt_decouple = %d\n", print.config().belt_frame_tilt_decouple.value ? 1 : 0);
@@ -34,13 +33,10 @@ void BeltGCode::write_belt_header(GCodeOutputStream &file, const Print &print)
void BeltGCode::on_set_origin(const PrintObject * /*obj*/, const Point & /*inst_shift*/)
{
// Global pre-slice mode (matches the per-instance Z-offset added in
// PrintObjectSlice.cpp): transform the origin through the belt pipeline so
// that back_transform(T * origin) = origin (correct machine position). The
// back_transform applied during G-code emission is the inverse of the
// forward transform.
if (!m_config.belt_preslice_global.value)
return;
// Matches the per-instance Z-offset added in PrintObjectSlice.cpp: transform
// the origin through the belt pipeline so that back_transform(T * origin) =
// origin (correct machine position). The back_transform applied during
// G-code emission is the inverse of the forward transform.
// Adjust origin: transform through belt forward pipeline so that
// the back-transform correctly recovers model-space positions.
+18
View File
@@ -38,6 +38,20 @@ void BeltSliceStrategy::apply_preslice_transforms(Transform3d &trafo,
// coordinates rather than object-space coordinates, so volumes translated along
// the slicer's Z axis would be silently excluded from the bound check.
//
// The lift is measured to the lowest point of the SUPPORT region, not of the
// mesh: the belt floor (z = shear * u in this rotated frame, u the from-axis
// coordinate) runs below every vertex, and under the leading end of an
// overhang it lies below the lowest vertex by up to the overhang's length
// times the shear. Supports have to reach that floor, and every support
// generator works in layers at z >= 0, so z = 0 has to be the lowest floor
// point under the footprint. The layers between it and the first vertex
// come out empty, which belt slicing already tolerates (the bottom corner
// of a tilted part is a point). Vertices on the belt have z == floor, so
// for a part resting on the belt this is simply the floor at its leading
// extreme, less the frame margin (see BeltTransformPipeline::frame_margin).
BeltTransformPipeline::BeltFloorParams floor;
const bool has_floor = BeltTransformPipeline::floor_shear(config, floor);
double min_z = std::numeric_limits<double>::max();
for (const ModelVolume *mv : model_volumes) {
if (!mv->is_model_part()) continue;
@@ -47,8 +61,12 @@ void BeltSliceStrategy::apply_preslice_transforms(Transform3d &trafo,
Vec3d vm = v.cast<double>();
Vec3d pt = vol_trafo * vm;
min_z = std::min(min_z, pt.z());
if (has_floor)
min_z = std::min(min_z, floor.shear_factor * (floor.from_axis == 0 ? pt.x() : pt.y()));
}
}
if (has_floor && min_z != std::numeric_limits<double>::max())
min_z -= BeltTransformPipeline::frame_margin(floor);
const double z_shift_val = (min_z < 0. && min_z != std::numeric_limits<double>::max()) ? -min_z : 0.;
if (z_shift_val > 0.) {
Transform3d z_shift = Transform3d::Identity();
+53 -21
View File
@@ -50,6 +50,37 @@ Transform3d BeltTransformPipeline::build_forward_transform(const PrintConfig &co
// Template avoids duplicating the math for the two config types.
namespace {
// Belt floor in the rotated slicer frame: the image of z_machine = 0 under R.
// R(+α, X): point (·, y, 0) → (·, cos α · y, sin α · y) ⇒ z = tan(α) · y_s
// R(+α, Y): point (x, ·, 0) → (cos α · x, ·, -sin α · x) ⇒ z = -tan(α) · x_s
// R(+α, Z): point (·, ·, 0) → (·, ·, 0); no tilt → no floor
void belt_floor_shear(BeltRotationAxis rot_axis, double angle_rad, BeltTransformPipeline::BeltFloorParams &out)
{
double sin_a = std::sin(angle_rad), cos_a = std::cos(angle_rad);
switch (rot_axis) {
case BeltRotationAxis::X:
out.shear_factor = (std::abs(cos_a) > EPSILON) ? sin_a / cos_a : 0.;
out.from_axis = 1; // Y
break;
case BeltRotationAxis::Y:
out.shear_factor = (std::abs(cos_a) > EPSILON) ? -sin_a / cos_a : 0.;
out.from_axis = 0; // X
break;
case BeltRotationAxis::Z:
default:
out.shear_factor = 0.0;
out.from_axis = 1;
break;
}
}
// Z of the belt floor directly under a point of the rotated (unshifted) frame.
inline double belt_floor_z(const BeltTransformPipeline::BeltFloorParams &fp, const Vec3d &pt)
{
return fp.shear_factor * (fp.from_axis == 0 ? pt.x() : pt.y());
}
template<typename Config>
BeltTransformPipeline::BeltHeightResult compute_belt_height_and_floor_impl(
const Config &config, const BoundingBoxf3 &bb, double original_height)
@@ -93,38 +124,28 @@ BeltTransformPipeline::BeltHeightResult compute_belt_height_and_floor_impl(
default: unit_axis = Vec3d::UnitX(); break;
}
Matrix3d R = Eigen::AngleAxisd(angle_rad, unit_axis).toRotationMatrix();
belt_floor_shear(rot_axis, angle_rad, result.floor_params);
// The slicing frame starts at the lowest point of the support region: the
// lowest belt-floor point under the footprint, not the lowest vertex. The
// belt under the leading end of an overhang lies below every vertex of the
// part, and supports have to be able to reach it (see
// BeltSliceStrategy::apply_preslice_transforms for the exact vertex-scan
// counterpart of this bbox estimate).
double min_rz = std::numeric_limits<double>::max();
double max_rz = std::numeric_limits<double>::lowest();
for (int i = 0; i < 8; ++i) {
Vec3d c((i & 1) ? bb.max.x() : bb.min.x(),
(i & 2) ? bb.max.y() : bb.min.y(),
(i & 4) ? bb.max.z() : bb.min.z());
double z = (R * c).z();
Vec3d rc = R * c;
double z = rc.z();
min_rz = std::min(min_rz, z);
max_rz = std::max(max_rz, z);
min_rz = std::min(min_rz, belt_floor_z(result.floor_params, rc));
}
min_rz -= BeltTransformPipeline::frame_margin(result.floor_params);
result.object_height = max_rz - min_rz;
// Belt floor in slicer-frame is the image of z_machine = 0 under R.
// R(+α, X): point (·, y, 0) → (·, cos α · y, sin α · y) ⇒ z = tan(α) · y_s
// R(+α, Y): point (x, ·, 0) → (cos α · x, ·, -sin α · x) ⇒ z = -tan(α) · x_s
// R(+α, Z): point (·, ·, 0) → (·, ·, 0); no tilt → no floor
double sin_a = std::sin(angle_rad), cos_a = std::cos(angle_rad);
switch (rot_axis) {
case BeltRotationAxis::X:
result.floor_params.shear_factor = (std::abs(cos_a) > EPSILON) ? sin_a / cos_a : 0.;
result.floor_params.from_axis = 1; // Y
break;
case BeltRotationAxis::Y:
result.floor_params.shear_factor = (std::abs(cos_a) > EPSILON) ? -sin_a / cos_a : 0.;
result.floor_params.from_axis = 0; // X
break;
case BeltRotationAxis::Z:
default:
result.floor_params.shear_factor = 0.0;
result.floor_params.from_axis = 1;
break;
}
result.floor_params.z_shift = bb.min.z() + ((min_rz < 0.) ? -min_rz : 0.);
return result;
@@ -144,4 +165,15 @@ BeltTransformPipeline::BeltHeightResult BeltTransformPipeline::compute_belt_heig
return compute_belt_height_and_floor_impl(config, bbox, original_height);
}
bool BeltTransformPipeline::floor_shear(const PrintConfig &config, BeltFloorParams &out)
{
out = BeltFloorParams{};
const BeltRotationAxis rot_axis = config.belt_slice_rotation.value;
const double rot_angle = config.belt_slice_rotation_angle.value;
if (rot_axis == BeltRotationAxis::None || std::abs(rot_angle) <= EPSILON)
return false;
belt_floor_shear(rot_axis, Geometry::deg2rad(rot_angle), out);
return std::abs(out.shear_factor) > EPSILON;
}
} // namespace Slic3r
+13
View File
@@ -108,6 +108,19 @@ public:
double z_shift = 0.0;
};
// Shear factor and from-axis of the belt floor in the rotated slicer frame
// (z_floor = shear_factor * u, u = the from-axis coordinate), for the
// rotation the config selects. z_shift is left at 0. Returns false (and
// zero shear) when the config has no tilt.
static bool floor_shear(const PrintConfig &config, BeltFloorParams &out);
// How far below the lowest belt-floor point under the footprint the slicing
// frame starts, in slicing Z. A support column meeting the belt is wider at
// its base than at its tip, so under a leading overhang the base reaches ahead
// of the part along the belt, and the layers that trim it to the belt plane
// lie below that lowest point: 10 mm along the belt.
static double frame_margin(const BeltFloorParams &fp) { return 10. * std::abs(fp.shear_factor); }
// Result of computing belt height + floor params.
struct BeltHeightResult {
double object_height; // Effective object height after shear/scale
+3 -7
View File
@@ -957,14 +957,10 @@ protected:
return on_first_layer() ? 0 : layer_id();
}
// Band thickness for the *effective layer index* only. Two separate
// thresholds: on_first_layer(point) tests against initial_layer_print_height,
// while effective_layer_index_for_point() counts bands of
// first_layer_plane_thickness. Conflating them would apply first-layer
// treatment through a whole 1mm band on a 0.2mm first layer.
// Band thickness for the *effective layer index*: one first layer height, so
// "the first N layers" means the same height above the belt as on a flat bed.
double first_layer_band_mm() const {
double band = m_config.first_layer_plane_thickness.value;
if (band <= 0.) band = m_config.initial_layer_print_height.value;
const double band = m_config.initial_layer_print_height.value;
return band > 0. ? band : 0.2;
}
+1 -6
View File
@@ -10,12 +10,7 @@ bool BeltBackTransform::init_from_config(const PrintConfig &config)
m_active = false;
m_inverse = Transform3d::Identity();
if (!config.belt_printer.value || !config.gcode_back_transform.value)
return false;
// The back-transform undoes the global pre-slice rotation; without global
// mode the slicing frame is not a common frame to undo.
if (!config.belt_preslice_global.value)
if (!config.belt_printer.value)
return false;
// Build the forward pipeline (the rotation) and store its inverse.
+1 -1
View File
@@ -14,7 +14,7 @@ namespace Slic3r {
// Initialized once from PrintConfig, then applied per-point in
// BeltKinematics::to_machine() before axis remapping.
//
// Active when gcode_back_transform and belt_preslice_global are both set.
// Active on belt printers with a non-identity pre-slice rotation.
class BeltBackTransform
{
public:
+2 -2
View File
@@ -3220,8 +3220,8 @@ void GCodeProcessor::apply_config(const DynamicPrintConfig& config)
const auto *belt = config.option<ConfigOptionBool>("belt_printer");
if (belt != nullptr) {
static const char *belt_keys[] = {
"belt_printer", "belt_slice_rotation", "belt_slice_rotation_angle", "belt_preslice_global",
"gcode_remap_x", "gcode_remap_y", "gcode_remap_z", "gcode_back_transform",
"belt_printer", "belt_slice_rotation", "belt_slice_rotation_angle",
"gcode_remap_x", "gcode_remap_y", "gcode_remap_z",
"belt_frame_tilt_decouple", "belt_frame_tilt_angle",
};
for (const char *key : belt_keys)
+2 -4
View File
@@ -1561,11 +1561,9 @@ static std::vector<std::string> s_Preset_printer_options {
"printer_technology",
"printable_area", "extruder_printable_area", "support_parallel_printheads", "parallel_printheads_count", "parallel_printheads_bed_exclude_areas", "bed_exclude_area","bed_custom_texture", "bed_custom_model", "build_plate_tilt_x", "build_plate_tilt_y", "belt_printer", "belt_printer_infinite_y",
"belt_slice_rotation", "belt_slice_rotation_angle",
"gcode_remap_x", "gcode_remap_y", "gcode_remap_z", "gcode_back_transform",
"gcode_remap_x", "gcode_remap_y", "gcode_remap_z",
"belt_frame_tilt_decouple", "belt_frame_tilt_angle",
"belt_preslice_global",
"first_layer_plane_thickness",
"belt_support_floor_offset", "belt_support_floor_mode",
"belt_support_floor_offset",
"enable_belt_purge_tower",
"gcode_flavor", "gcode_skip_config_block",
"fan_kickstart", "part_cooling_fan_min_pwm", "fan_speedup_time", "fan_speedup_overhangs",
+9 -16
View File
@@ -176,8 +176,6 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n
"gcode_remap_z",
// Machine-frame transform (derived from belt tilt; only affects G-code output).
"belt_frame_tilt_decouple", "belt_frame_tilt_angle",
"gcode_back_transform",
"first_layer_plane_thickness",
// Only inflates the GUI bed volume, like printable_area.
"belt_printer_infinite_y",
//BBS
@@ -388,12 +386,10 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n
// Belt printer transform options change the mesh geometry before slicing.
|| opt_key == "belt_printer"
|| opt_key == "belt_slice_rotation"
|| opt_key == "belt_slice_rotation_angle"
|| opt_key == "belt_preslice_global") {
|| opt_key == "belt_slice_rotation_angle") {
osteps.emplace_back(posSlice);
} else if (
opt_key == "belt_support_floor_offset"
|| opt_key == "belt_support_floor_mode") {
opt_key == "belt_support_floor_offset") {
osteps.emplace_back(posSupportMaterial);
} else if (
opt_key == "print_sequence"
@@ -1908,14 +1904,6 @@ StringObjectException Print::validate(std::vector<StringObjectException> *warnin
"which is the edge it is meant to anchor. Set the gap to 0 when using leading "
"brim length."),
"brim_object_gap", object->model_object());
// Unconditional: this suppresses the WHOLE belt brim, not just the apron, so a
// user asking for any brim at all needs to be told they are getting none.
if (! object->belt_brim_instances_compatible())
warn(L("This object's copies are spaced along the belt, so they would each need "
"their own brim and none is generated. Print them as separate objects, or "
"arrange the copies side by side across the belt."),
"brim_type", object->model_object());
}
if (this->has_belt_brim() && m_objects.size() > 1)
warn(L("Leading brim length extends ahead of each object along the belt, and Arrange does "
@@ -3061,7 +3049,7 @@ void Print::process(long long *time_cost_with_cache, bool use_cache)
std::set<PrintObject*> re_slicing_objects;
// Belt global modes couple each object's bed position into its layer Z values,
// so sharing layers between "identical" objects is wrong.
bool belt_no_share = m_config.belt_printer.value && m_config.belt_preslice_global.value;
bool belt_no_share = m_config.belt_printer.value;
if (!use_cache) {
for (int index = 0; index < object_count; index++)
{
@@ -3221,7 +3209,8 @@ void Print::process(long long *time_cost_with_cache, bool use_cache)
for (int i = range.begin(); i < range.end(); i++) {
PrintObject* obj = m_objects[i];
if (need_slicing_objects.count(obj) != 0) {
obj->generate_support_material();
// The belt brim follows sequentially below.
obj->generate_support_material(false);
}
else {
if (obj->set_started(posSupportMaterial))
@@ -3230,6 +3219,10 @@ void Print::process(long long *time_cost_with_cache, bool use_cache)
}
}
);
// The belt brim keeps clear of every object's layers and support layers,
// so it runs once no support step is rebuilding them any more.
for (PrintObject *obj : m_objects)
obj->generate_belt_brim();
if (m_pipeline_plugin_active)
for (size_t i = 0; i < m_objects.size(); ++i)
+9 -2
View File
@@ -439,7 +439,6 @@ public:
unsigned int belt_brim_filament() const;
// False when this object's instances sit at different points ALONG the belt, which
// would need a separate set of bands each. Public so validate() can explain it.
bool belt_brim_instances_compatible() const;
const std::vector<ExtrusionEntityCollection>& belt_brim_by_layer() const { return m_belt_brim_by_layer; }
const std::vector<ExPolygons>& belt_brim_areas_by_layer() const { return m_belt_brim_areas_by_layer; }
const std::vector<BeltBrimBand>& belt_brim_prologue() const { return m_belt_brim_prologue; }
@@ -583,7 +582,13 @@ private:
void ironing();
bool need_z_contouring() const;
void contour_z();
void generate_support_material();
// with_belt_brim = false leaves the belt brim to generate_belt_brim(), for a
// caller that runs the support step of several objects in parallel.
void generate_support_material(bool with_belt_brim = true);
// The belt brim keeps clear of every object's layers and support layers, so
// it has to run after all support steps finished. A no-op unless a support
// step left it pending.
void generate_belt_brim();
void estimate_curled_extrusions();
void simplify_extrusion_path();
@@ -712,6 +717,8 @@ private:
// Belt printer: global Z offset applied to this object's layers for shear positioning.
double m_belt_global_z_offset { 0.0 };
// generate_support_material(false) finished and generate_belt_brim() has not run yet.
bool m_belt_brim_pending { false };
// Belt printer: min_z of mesh after belt shear (before Z-shift), for z_offset calc.
double m_belt_min_z { 0.0 };
// Belt printer: XY correction from global pre-slice mode, applied to G-code origin.
+4 -6
View File
@@ -1842,9 +1842,9 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
for (ModelObject *model_object : m_model.objects) {
ModelObjectStatus &model_object_status = const_cast<ModelObjectStatus&>(model_object_status_db.reuse(*model_object));
// Orca: Updated for XYZ filament shrink compensation
// Belt global mode: force each instance into its own PrintObject
// so each gets independent layer Z values.
bool belt_force_separate = m_config.belt_printer.value && m_config.belt_preslice_global.value;
// Belt printers: force each instance into its own PrintObject so each
// gets independent layer Z values (its bed position is folded into them).
bool belt_force_separate = m_config.belt_printer.value;
model_object_status.print_instances = print_objects_from_model_object(*model_object, this->shrinkage_compensation(), belt_force_separate);
std::vector<const PrintObjectStatus*> old;
old.reserve(print_object_status_db.count(*model_object));
@@ -1933,9 +1933,7 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
// Belt printer global mode: when any object's instances shifted,
// recompute m_belt_global_z_offset for ALL objects (it depends on
// min_shift across all objects, so one move affects everyone).
if (belt_instances_shifted
&& m_config.belt_printer.value
&& m_config.belt_preslice_global.value) {
if (belt_instances_shifted && m_config.belt_printer.value) {
for (PrintObject *object : m_objects)
update_apply_status(object->invalidate_step(posSlice));
}
+6 -58
View File
@@ -398,12 +398,6 @@ static t_config_enum_values s_keys_map_RemapAxis {
};
CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(RemapAxis)
static t_config_enum_values s_keys_map_BeltSupportFloorMode {
{ "none", int(BeltSupportFloorMode::None) },
{ "generator_only", int(BeltSupportFloorMode::GeneratorOnly) },
};
CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(BeltSupportFloorMode)
static t_config_enum_values s_keys_map_SupportMaterialPattern {
{ "rectilinear", smpRectilinear },
{ "rectilinear-grid", smpRectilinearGrid },
@@ -7390,7 +7384,7 @@ void PrintConfigDef::init_fff_params()
def->enum_keys_map = &ConfigOptionEnum<BeltRotationAxis>::get_enum_values();
def->enum_values = {"none", "x", "y", "z"};
def->enum_labels = {L("None"), L("X"), L("Y"), L("Z")};
def->mode = comAdvanced;
def->mode = comDevelop;
def->set_default_value(new ConfigOptionEnum<BeltRotationAxis>(BeltRotationAxis::X));
def = this->add("belt_slice_rotation_angle", coFloat);
@@ -7451,38 +7445,6 @@ void PrintConfigDef::init_fff_params()
// by per-axis keys: it is derived from the belt tilt (belt_slice_rotation axis
// + angle, or belt_frame_tilt_angle when decoupled) in MachineFrameTransform.
def = this->add("gcode_back_transform", coBool);
def->label = L("G-code back-transform");
def->category = L("Printable space");
def->tooltip = L("Undo the pre-slice mesh transform before applying the G-code axis remap "
"and machine-frame shear/scale. Required for the standard belt-printer "
"rotation pipeline.");
def->mode = comExpert;
def->set_default_value(new ConfigOptionBool(true));
def = this->add("belt_preslice_global", coBool);
def->label = L("Global mesh transforms");
def->category = L("Printable space");
def->tooltip = L("When enabled, pre-slice belt transforms (remap, shear, scale) account for "
"each object's bed position, producing correct machine coordinates without "
"relying on origin snap. Each instance gets its own PrintObject.");
def->mode = comExpert;
def->set_default_value(new ConfigOptionBool(true));
def = this->add("first_layer_plane_thickness", coFloat);
def->label = L("First layer band thickness");
def->category = L("Printable space");
def->tooltip = L("Belt printers only. Every tilted layer touches the belt, so the first-layer "
"settings apply to a band above the belt surface rather than to the first "
"slicing layer. This is the thickness of one band, in mm: the unit by which "
"'No cooling for the first N layers' and similar layer-count thresholds are "
"multiplied. -1 means use the first layer height.");
def->sidetext = L("mm");
def->min = -1;
def->max = 100;
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionFloat(-1.0));
// Belt support floor debug controls
def = this->add("belt_support_floor_offset", coFloat);
def->label = L("Support Floor Z offset");
@@ -7494,19 +7456,6 @@ void PrintConfigDef::init_fff_params()
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionFloat(0));
{
auto def = this->add("belt_support_floor_mode", coEnum);
def->label = L("Floor mode");
def->category = L("Printable space");
def->tooltip = L("Controls belt floor awareness for supports. 'None' disables belt floor logic. "
"'Generator only' stops support generation at the belt floor plane.");
def->enum_keys_map = &ConfigOptionEnum<BeltSupportFloorMode>::get_enum_values();
def->enum_values = {"none", "generator_only"};
def->enum_labels = {L("None"), L("Generator only")};
def->mode = comDevelop;
def->set_default_value(new ConfigOptionEnum<BeltSupportFloorMode>(BeltSupportFloorMode::GeneratorOnly));
}
def = this->add("enable_belt_purge_tower", coBool);
def->label = L("Enable belt purge tower");
def->category = L("Multimaterial");
@@ -9440,9 +9389,6 @@ void PrintConfigDef::handle_legacy(t_config_option_key &opt_key, std::string &va
//BBS: handle legacy options
if (opt_key == "curr_bed_type" && value == "SuperTack Plate") {
value = "Supertack Plate";
} else if (opt_key == "belt_support_floor_mode" && (value == "clip_only" || value == "both")) {
// Never implemented; both behaved like "none".
value = "none";
} else if (opt_key == "enable_wipe_tower") {
opt_key = "enable_prime_tower";
} else if (opt_key == "wipe_tower_width") {
@@ -9682,11 +9628,13 @@ void PrintConfigDef::handle_legacy(t_config_option_key &opt_key, std::string &va
"smooth_coefficient", "overhang_totally_speed", "silent_mode",
"overhang_speed_classic",
"anisotropic_surfaces", // superseded by top_surface_fill_order / bottom_surface_fill_order
// Belt printer keys retired before the first release: the global switches collapsed
// into belt_preslice_global, the pre-slice axis remap and the support Z offset mode
// were removed.
// Belt printer keys retired before the first release: the global-mode and
// back-transform switches are presumed on, and the pre-slice axis remap, the
// support Z offset mode, the support floor mode (always on) and the first-layer
// plane evaluator were removed.
"belt_slice_rotation_global", "preslice_remap_x", "preslice_remap_y", "preslice_remap_z", "preslice_remap_global",
"belt_support_z_offset_mode", "first_layer_plane", "first_layer_plane_offset",
"belt_preslice_global", "gcode_back_transform", "belt_support_floor_mode", "first_layer_plane_thickness",
};
if (ignore.find(opt_key) != ignore.end()) {
-11
View File
@@ -294,12 +294,6 @@ enum class RemapAxis
RevX = 6, RevY = 7, RevZ = 8, // Reversed: max - pos
};
enum class BeltSupportFloorMode
{
None, // No belt floor awareness
GeneratorOnly, // Only in tree support drop_nodes/contact_points
};
enum SupportMaterialPattern {
smpDefault,
smpRectilinear, smpRectilinearGrid, smpHoneycomb,
@@ -746,7 +740,6 @@ CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(IroningType)
CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(SlicingMode)
CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(BeltRotationAxis)
CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(RemapAxis)
CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(BeltSupportFloorMode)
CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(SupportMaterialPattern)
CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(SupportMaterialStyle)
CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(SupportMaterialInterfacePattern)
@@ -1875,11 +1868,7 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE(
((ConfigOptionEnum<RemapAxis>, gcode_remap_x))
((ConfigOptionEnum<RemapAxis>, gcode_remap_y))
((ConfigOptionEnum<RemapAxis>, gcode_remap_z))
((ConfigOptionBool, gcode_back_transform))
((ConfigOptionBool, belt_preslice_global))
((ConfigOptionFloat, first_layer_plane_thickness))
((ConfigOptionFloat, belt_support_floor_offset))
((ConfigOptionEnum<BeltSupportFloorMode>, belt_support_floor_mode))
// Width (machine X, across the belt) of the auto-generated belt purge prism.
((ConfigOptionFloat, belt_purge_tower_width))
// Belt-printer-only "type" of purge tower: enables the auto-generated belt
+49 -32
View File
@@ -984,7 +984,7 @@ void PrintObject::detect_overhangs_for_lift()
}
}
void PrintObject::generate_support_material()
void PrintObject::generate_support_material(bool with_belt_brim)
{
if (this->set_started(posSupportMaterial)) {
this->clear_support_layers();
@@ -1032,12 +1032,25 @@ void PrintObject::generate_support_material()
// those must exist before ToolOrdering is built at psWipeTower - one step
// ahead of psSkirtBrim. The brim options already invalidate
// posSupportMaterial, so this needs no extra invalidation edges.
make_belt_brim(*this);
m_print->throw_if_canceled();
m_belt_brim_pending = true;
if (with_belt_brim)
this->generate_belt_brim();
this->set_done(posSupportMaterial);
}
}
void PrintObject::generate_belt_brim()
{
if (! m_belt_brim_pending)
return;
// belt_brim_obstacles() looks up the layers and support layers of every object
// on the plate by print_z. Another object's support step rebuilds those (and
// temporarily shifts its layer Z values), so this must not overlap with it.
make_belt_brim(*this);
m_print->throw_if_canceled();
m_belt_brim_pending = false;
}
void PrintObject::estimate_curled_extrusions()
{
if (this->set_started(posEstimateCurledExtrusions)) {
@@ -1245,8 +1258,6 @@ bool PrintObject::has_belt_brim() const
// this keeps it brimless whatever its config says, so a brim on the parts never blocks purging.
if (m_config.belt_purge_tower_object.value)
return false;
if (! this->belt_brim_instances_compatible())
return false;
if (m_config.brim_type == btNoBrim)
return false;
// An inner-only brim has no leading/extra geometry: leading_brim_length and
@@ -1280,30 +1291,6 @@ unsigned int PrintObject::belt_brim_filament() const
return brim_filament == 0 ? 1u : brim_filament;
}
bool PrintObject::belt_brim_instances_compatible() const
{
// One set of bands is shared by every instance of this object, so they must all sit at
// the same height on the belt. Moving an instance ALONG the belt axis changes its
// physical belt-floor Z and would put its brim at the wrong height; moving it ACROSS
// the belt does not, so side-by-side copies are fine.
//
// belt_force_separate() in PrintApply.cpp already gives one instance per PrintObject
// whenever a global belt flag is set, which the shipped belt profiles do - this only
// matters for configurations that do not.
if (m_instances.size() <= 1)
return true;
// From the config, not m_slicing_params: this runs while those can be stale. A tilt
// about Y runs the belt along X, any other tilt along Y (see compute_belt_height_and_floor).
const int axis = m_print->config().belt_slice_rotation.value == BeltRotationAxis::Y ? 0 : 1;
const Point &ref = m_instances.front().shift;
for (const PrintInstance &inst : m_instances) {
const coord_t along = axis == 0 ? inst.shift.x() - ref.x() : inst.shift.y() - ref.y();
if (std::abs(along) > SCALED_EPSILON)
return false;
}
return true;
}
void PrintObject::clear_belt_brim()
{
m_belt_brim_by_layer.clear();
@@ -4741,11 +4728,41 @@ void PrintObject::_generate_support_material()
tree_support.generate();
}
else {
PrintObjectSupportMaterial support_material(this, m_slicing_params);
support_material.generate(*this);
// The normal generator anchors its layer grid at the slicing frame origin
// (SlicingParameters: first layer at first_print_layer_height, raft at
// z = 0), so it has to see the object layers in that frame. On a belt the
// object layers carry the global Z offset (PrintObject::slice()), which is
// negative for the leading half of the belt: a top contact below z = 0
// then turns the intermediate-layer count negative and the generator
// allocates layers until memory runs out. Lift the offset off the object
// layers and the belt floor for the duration of the run and put it back
// on everything, including the new support layers, afterwards (organic
// tree support is shifted the same way below).
const double global_z = m_belt_global_z_offset;
const bool unshift = std::abs(global_z) > EPSILON;
auto shift_object_frame = [this, global_z](double sign) {
for (Layer *layer : m_layers)
layer->print_z += sign * global_z;
m_slicing_params.belt_floor_z_shift += sign * global_z;
};
if (unshift)
shift_object_frame(-1.);
try {
PrintObjectSupportMaterial support_material(this, m_slicing_params);
support_material.generate(*this);
} catch (...) {
if (unshift)
shift_object_frame(1.);
throw;
}
if (unshift) {
shift_object_frame(1.);
for (SupportLayer *sl : m_support_layers)
sl->print_z += global_z;
}
}
// Global Z offset for support layers:
// - Normal support: layers already inherit global_z_offset from object layers.
// - Normal support: generated in the object frame above and shifted afterwards.
// - Non-organic tree support (slim/strong/hybrid): plan_layer_heights() reads
// from globally-offset object layers, so support layers already have it.
// - Organic tree support: generate_tree_support_3D() computes its own Z values
+1 -2
View File
@@ -976,7 +976,6 @@ void PrintObject::slice()
const auto &pcfg = this->print()->config();
BOOST_LOG_TRIVIAL(trace) << "Belt global check: belt_printer=" << pcfg.belt_printer.value
<< " belt_slice_rotation=" << int(pcfg.belt_slice_rotation.value)
<< " belt_preslice_global=" << pcfg.belt_preslice_global.value
<< " object=" << this->model_object()->name;
if (pcfg.belt_printer.value) {
@@ -1026,7 +1025,7 @@ void PrintObject::slice()
global_z_offset += centering_z_corr;
}
if (pcfg.belt_preslice_global.value) {
{
// Global pre-slice mode: compute full correction c = (T.linear() - I) * d
// where T is the belt forward transform and d is the bed position, so
// objects at different bed positions print at different machine Z values
+7 -1
View File
@@ -320,7 +320,13 @@ SupportGeneratorLayersPtr generate_raft_base(
// How much to inflate the support columns to be stable. This also applies to the 1st layer, if no raft layers are to be printed.
const float inflate_factor_fine = float(scale_((slicing_params.raft_layers() > 1) ? 0.5 : EPSILON));
const float inflate_factor_1st_layer = std::max(0.f, float(scale_(object.config().raft_first_layer_expansion)) - inflate_factor_fine);
// On a belt the first support layer is the leading tip of the support, a sliver
// where the belt crosses the layer, not a flange on a flat bed: inflating it
// puts lines in the air ahead of the belt crossing (and into the belt behind
// it). The belt brim takes the adhesion role instead.
const bool belt_floor_active = std::abs(slicing_params.belt_floor_shear_factor) > EPSILON;
const float inflate_factor_1st_layer = belt_floor_active ? 0.f :
std::max(0.f, float(scale_(object.config().raft_first_layer_expansion)) - inflate_factor_fine);
SupportGeneratorLayer *contacts = top_contacts .empty() ? nullptr : top_contacts .front();
SupportGeneratorLayer *interfaces = interface_layers .empty() ? nullptr : interface_layers .front();
SupportGeneratorLayer *base_interfaces = base_interface_layers.empty() ? nullptr : base_interface_layers.front();
+1 -4
View File
@@ -2768,8 +2768,7 @@ SupportGeneratorLayersPtr PrintObjectSupportMaterial::bottom_contact_layers_and_
//const auto expansion_to_slice = m_support_material_flow.scaled_spacing() / 2 + 25;
const SupportGridParams grid_params(*m_object_config, m_support_params.support_material_flow);
const bool buildplate_only = ! buildplate_covered.empty();
const bool has_belt_floor = std::abs(m_slicing_params.belt_floor_shear_factor) > EPSILON
&& m_print_config->belt_support_floor_mode.value == BeltSupportFloorMode::GeneratorOnly;
const bool has_belt_floor = std::abs(m_slicing_params.belt_floor_shear_factor) > EPSILON;
// Allocate empty surface areas, one per object layer.
layer_support_areas.assign(object.total_layer_count(), Polygons());
@@ -3311,8 +3310,6 @@ static void trim_support_layers_by_belt_floor(
BeltFloorContext ctx;
if (!ctx.init(slicing_params, print_config))
return;
if (print_config.belt_support_floor_mode.value != BeltSupportFloorMode::GeneratorOnly)
return;
tbb::parallel_for(tbb::blocked_range<size_t>(0, support_layers.size()),
[&](const tbb::blocked_range<size_t> &range) {
+10 -49
View File
@@ -117,58 +117,20 @@ TreeModelVolumes::TreeModelVolumes(
m_increase_until_radius = config.increase_radius_until_radius;
m_radius_0 = config.getRadius(0);
m_raft_layers = config.raft_layers;
// Belt printer: add virtual belt raft layers below the object, matching
// the extra layers added in generate_support_areas() so both use the
// same layer indexing.
{
const auto &sp2 = print_object.slicing_parameters();
const auto &pcfg2 = print_object.print()->config();
double belt_sf = sp2.belt_floor_shear_factor;
if (std::abs(belt_sf) > EPSILON && std::abs(print_object.belt_global_z_offset()) > EPSILON
&& pcfg2.belt_support_floor_mode.value == BeltSupportFloorMode::GeneratorOnly) {
double bb_min_z = std::abs(belt_remapped_bbox(*print_object.model_object(), pcfg2).min.z());
double extra_depth = bb_min_z + 10.;
int num_extra = std::max(0, (int)std::ceil(extra_depth / sp2.layer_height));
if (num_extra > 0) {
std::vector<coordf_t> belt_layers;
belt_layers.reserve(num_extra);
for (int i = num_extra; i >= 1; --i)
belt_layers.push_back(sp2.first_object_layer_height - i * sp2.layer_height);
m_raft_layers.insert(m_raft_layers.begin(), belt_layers.begin(), belt_layers.end());
}
}
}
// Belt floor: add belt surface polygons to anti_overhang so support is
// never generated inside the belt.
// Support blockers are 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 them in object-layer space. Shift them.
//
// 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.
// The belt surface is deliberately NOT a blocker. A blocker is a collision,
// and a branch descending onto a collision slides off it: on a belt that
// walks the branch down the tilted surface, ahead of the part, until it
// reaches the bottom layer floating in mid-air. The belt is where branches
// END: organic_draw_branches() clips their slices with m_belt_floor and the
// first clipped slice is the contact.
{
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;
@@ -192,8 +154,7 @@ TreeModelVolumes::TreeModelVolumes(
const auto &pcfg2 = print_object.print()->config();
BeltFloorContext ctx;
ctx.init_local(slicing_params, pcfg2, print_object.belt_global_z_offset());
if (ctx.is_active()
&& pcfg2.belt_support_floor_mode.value == BeltSupportFloorMode::GeneratorOnly) {
if (ctx.is_active()) {
m_belt_floor = ctx.compute_per_layer_floors(num_layers, [&](size_t layer_idx) -> double {
// Object layers: local print_z (subtract global offset).
if (layer_idx >= num_raft_layers)
+37 -149
View File
@@ -716,6 +716,25 @@ double TreeSupport::belt_floor_print_z(const Point &pos_slicing) const
return ctx.floor_print_z(pos_slicing);
}
bool TreeSupport::belt_node_landed(const Point &pos_slicing, double radius, double print_z) const
{
BeltFloorContext ctx;
if (!ctx.init(m_slicing_params, *m_print_config))
return false;
return print_z <= ctx.floor_print_z(pos_slicing) - std::abs(ctx.shear_factor()) * std::max(0., radius);
}
bool TreeSupport::belt_polygon_landed(const ExPolygon &poly, double print_z) const
{
BeltFloorContext ctx;
if (!ctx.init(m_slicing_params, *m_print_config))
return false;
double min_floor = std::numeric_limits<double>::max();
for (const Point &pt : poly.contour.points)
min_floor = std::min(min_floor, ctx.floor_print_z(pt));
return print_z <= min_floor;
}
#define SUPPORT_SURFACES_OFFSET_PARAMETERS jtSquare, 0.
void TreeSupport::detect_overhangs(bool check_support_necessity/* = false*/)
{
@@ -1616,8 +1635,7 @@ void TreeSupport::generate_toolpaths()
// 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;
const bool belt_floor_active = belt_ctx.init(m_slicing_params, *m_print_config);
// generate tree support tool paths
tbb::parallel_for(
@@ -1898,133 +1916,6 @@ void TreeSupport::generate()
// Belt floor: extend support below the object's first layer by creating
// additional support layers with geometry copied from the lowest content
// layer and clipped at the belt surface. These layers bypass the tree
// algorithm entirely — they're pure geometry added after draw_circles().
{
BeltFloorContext ctx;
if (ctx.init(m_slicing_params, *m_print_config)
&& m_print_config->belt_support_floor_mode.value == BeltSupportFloorMode::GeneratorOnly) {
const auto &sp = m_slicing_params;
// Find the lowest non-empty, non-brim support layer.
ExPolygons source_areas;
double source_z = 0;
int layers_with_content = 0;
for (size_t i = 0; i < m_object->support_layer_count(); ++i) {
SupportLayer *sl = m_object->get_support_layer(i);
if (sl && !sl->base_areas.empty()) {
layers_with_content++;
if (layers_with_content >= 2) {
source_areas = sl->base_areas;
source_z = sl->print_z;
break;
}
}
}
// Fallback to first content layer.
if (source_areas.empty()) {
for (size_t i = 0; i < m_object->support_layer_count(); ++i) {
SupportLayer *sl = m_object->get_support_layer(i);
if (sl && !sl->base_areas.empty()) {
source_areas = sl->base_areas;
source_z = sl->print_z;
break;
}
}
}
// ORCA-Belt calibration: a counter-rotated calibration object
// stands on a support wedge that lies entirely below the object's
// first layer, where the tree pipeline has no layers at all — so
// no support content can exist yet. Seed the extension directly
// from the floating portion of the first layer (anything more
// than one layer height above the belt floor). For objects whose
// first layer rests on the belt the floating region is empty and
// behavior is unchanged.
double first_z = m_object->support_layer_count() > 0 ? m_object->get_support_layer(0)->print_z : 0.;
bool seeded = false;
if (source_areas.empty() && m_object_config->enable_support.value && !m_object->layers().empty()) {
// The layer grid may start with an empty ghost layer just below
// the object (grid rounding against the belt global Z offset) —
// anchor the seed to the first layer that has geometry. Object
// layer print_z and the floor plane are both in the globally
// offset frame here (belt_floor_z_shift was adjusted alongside
// the layer Z values in PrintObject::slice()).
const Layer *first_layer = nullptr;
for (const Layer *l : m_object->layers())
if (!l->lslices_extrudable.empty()) { first_layer = l; break; }
if (first_layer != nullptr) {
ExPolygons floating = diff_ex(first_layer->lslices_extrudable,
ctx.surface_polygon(first_layer->bottom_z() - first_layer->height));
if (!floating.empty()) {
source_areas = std::move(floating);
first_z = first_layer->bottom_z();
seeded = true;
}
}
}
if (!source_areas.empty()) {
BoundingBoxf3 bb = belt_remapped_bbox(*m_object->model_object(), m_object->print()->config());
double from_extent = std::abs(bb.min(ctx.from_axis()));
double bb_min_z = std::abs(bb.min.z());
// Depth = from-axis extent + pre-shear bbox Z offset (ensure_on_bed
// distance) + 10mm safety margin. The 10mm is a bodge to avoid
// small cutoff artifacts — ideally computed exactly from belt geometry.
double extra_depth = std::min(from_extent + bb_min_z + 10., std::max(0., first_z));
if (seeded) {
// Seeded wedge: the depth is known exactly — down to the lowest
// belt-floor point under the floating footprint. The bbox
// heuristic above under-estimates it for meshes centered
// around their origin (every object loaded through the GUI).
double min_floor = first_z;
for (const ExPolygon &ep : source_areas)
for (const Point &pt : ep.contour.points)
min_floor = std::min(min_floor, ctx.floor_print_z(pt));
extra_depth = std::min(std::max(0., first_z), first_z - min_floor + 2.);
}
int num_extra = std::max(0, (int)std::ceil(extra_depth / sp.layer_height));
// Seeded wedge: top layers become a dense support interface so the
// object's floating first layer bridges a roof, not sparse infill.
const int interface_layers = seeded ? std::max(0, m_object_config->support_interface_top_layers.value) : 0;
ExPolygons prev_areas = source_areas;
// Build belt extension layers (lowest Z first).
SupportLayerPtrs belt_ext_layers;
for (int i = num_extra; i >= 1 && !prev_areas.empty(); --i) {
double print_z = first_z - i * sp.layer_height;
if (print_z < -sp.layer_height) continue;
Polygons belt_surface = ctx.surface_polygon(print_z);
ExPolygons clipped = diff_ex(source_areas, belt_surface);
if (clipped.empty()) continue;
SupportLayer *sl = new SupportLayer(0, 0, m_object, sp.layer_height, print_z, -1);
sl->base_areas = clipped;
// Populate area_groups — generate_toolpaths() iterates these,
// not base_areas directly.
// Note: base areas only get infill when support_base_pattern
// is explicitly set (with the default pattern tree bases are
// walls-only) — the calibration flow sets rectilinear.
const bool roof = i <= interface_layers;
for (auto &expoly : sl->base_areas) {
sl->area_groups.emplace_back(&expoly, roof ? SupportLayer::RoofType : SupportLayer::BaseType, 0);
if (roof)
sl->area_groups.back().interface_id = i & 1;
}
sl->lslices = clipped;
sl->lslices_bboxes.reserve(clipped.size());
for (const ExPolygon &ep : clipped)
sl->lslices_bboxes.emplace_back(get_extents(ep));
belt_ext_layers.push_back(sl);
}
// Insert at the front of support_layers (they're already in Z order).
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());
for (size_t i = 0; i < sl_vec.size(); ++i)
sl_vec[i]->set_id(i);
}
}
}
}
profiler.stage_start(STAGE_GENERATE_TOOLPATHS);
m_object->print()->set_status(70, _u8L("Generating support"));
generate_toolpaths();
@@ -2288,8 +2179,7 @@ void TreeSupport::draw_circles()
// 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;
const bool belt_floor_active = belt_ctx.init(m_slicing_params, *m_print_config);
if (m_object->support_layer_count() <= m_raft_layers)
return;
@@ -2484,8 +2374,7 @@ void TreeSupport::draw_circles()
// is 0 so init() and init_local() coincide — this is a no-op there.
{
BeltFloorContext ctx;
if (ctx.init(m_slicing_params, *m_print_config)
&& m_print_config->belt_support_floor_mode.value == BeltSupportFloorMode::GeneratorOnly) {
if (ctx.init(m_slicing_params, *m_print_config)) {
Polygons belt_surface = ctx.surface_polygon(ts_layer->print_z);
base_areas = diff_ex(base_areas, belt_surface);
roof_areas = diff_ex(roof_areas, belt_surface);
@@ -2962,9 +2851,7 @@ void TreeSupport::drop_nodes()
const size_t tip_layers = base_radius / layer_height; //The number of layers to be shrinking the circle to create a tip. This produces a 45 degree angle.
const coordf_t radius_sample_resolution = m_ts_data->m_radius_sample_resolution;
const bool support_on_buildplate_only = config.support_on_build_plate_only.value;
const auto belt_floor_mode = m_print_config->belt_support_floor_mode.value;
const bool has_belt_floor = std::abs(m_slicing_params.belt_floor_shear_factor) > EPSILON
&& belt_floor_mode == BeltSupportFloorMode::GeneratorOnly;
const bool has_belt_floor = std::abs(m_slicing_params.belt_floor_shear_factor) > EPSILON;
const size_t bottom_interface_layers = number_of_support_interface_bottom_layers(config);
SupportNode::diameter_angle_scale_factor = diameter_angle_scale_factor;
float DO_NOT_MOVER_UNDER_MM = is_slim ? 0 : 5; // do not move contact points under 5mm
@@ -3200,10 +3087,12 @@ void TreeSupport::drop_nodes()
node_parent = p_node->parent ? p_node : neighbour;
// Make sure the next pass doesn't drop down either of these (since that already happened).
node_parent->merged_neighbours.push_front(node_parent == p_node ? neighbour : p_node);
// Belt floor: don't drop merged node below belt surface.
// Treat as object-surface termination (not buildplate) so
// the node gets floor/interface areas instead of base pads.
if (has_belt_floor && print_z_next <= belt_floor_print_z(next_position)) {
// Belt floor: a merged node ends once its whole circle is in the belt
// (its slices are clipped to the belt plane in draw_circles(), so it
// tapers to a tip on the belt). Treat as object-surface termination
// (not buildplate) so the node gets floor/interface areas instead of
// base pads.
if (has_belt_floor && belt_node_landed(next_position, std::max(node.radius, neighbour->radius), print_z_next)) {
std::scoped_lock lock(m_ts_data->m_mutex);
node_parent->to_buildplate = false;
neighbour->valid = false;
@@ -3287,9 +3176,9 @@ void TreeSupport::drop_nodes()
ExPolygons overhangs_next = diff_clipped({ node.overhang }, get_collision(0, obj_layer_nr_next));
for(auto& overhang:overhangs_next) {
Point next_pt = overhang.contour.centroid();
// Belt floor: don't drop polygon node below belt surface.
// Belt floor: a polygon node ends once all of it is in the belt.
// Treat as object-surface termination (not buildplate).
if (has_belt_floor && print_z_next <= belt_floor_print_z(next_pt)) {
if (has_belt_floor && belt_polygon_landed(overhang, print_z_next)) {
p_node->to_buildplate = false;
continue;
}
@@ -3440,9 +3329,11 @@ void TreeSupport::drop_nodes()
if (is_outside) { next_layer_vertex = candidate_vertex; }
}
}
// Belt floor: don't drop regular node below belt surface.
// Belt floor: a node ends once its whole circle is in the belt; until
// then it keeps dropping and draw_circles() clips each layer's circle
// to the belt plane, so the branch tapers to a tip on the belt.
// Treat as object-surface termination (not buildplate).
if (has_belt_floor && print_z_next <= belt_floor_print_z(next_layer_vertex)) {
if (has_belt_floor && belt_node_landed(next_layer_vertex, node.radius, print_z_next)) {
p_node->to_buildplate = false;
return; // from parallel_for_each lambda
}
@@ -3771,9 +3662,7 @@ void TreeSupport::generate_contact_points()
const coordf_t max_bridge_length = scale_(config.max_bridge_length.value);
coord_t radius_scaled = scale_(base_radius);
bool on_buildplate_only = m_object_config->support_on_build_plate_only.value;
const auto belt_floor_mode = m_print_config->belt_support_floor_mode.value;
const bool has_belt_floor = std::abs(m_slicing_params.belt_floor_shear_factor) > EPSILON
&& belt_floor_mode == BeltSupportFloorMode::GeneratorOnly;
const bool has_belt_floor = std::abs(m_slicing_params.belt_floor_shear_factor) > EPSILON;
//First generate grid points to cover the entire area of the print.
BoundingBox bounding_box = m_object->bounding_box();
@@ -4044,8 +3933,7 @@ TreeSupportData::TreeSupportData(const PrintObject &object, coordf_t xy_distance
BeltFloorContext ctx;
double local_print_z = layer->print_z - object.belt_global_z_offset();
if (ctx.init_local(object.slicing_parameters(), object.print()->config(),
object.belt_global_z_offset())
&& object.print()->config().belt_support_floor_mode.value == BeltSupportFloorMode::GeneratorOnly) {
object.belt_global_z_offset())) {
Polygons belt_surface = ctx.surface_polygon(local_print_z);
for (auto &p : belt_surface)
outline.emplace_back(ExPolygon(p));
+7
View File
@@ -463,6 +463,13 @@ private:
// Belt printer: compute the belt floor print_z at a given XY position (in slicing coords).
// Returns -infinity if belt floor is not active.
double belt_floor_print_z(const Point &pos_slicing) const;
// Whether a node's whole circle (radius in mm) sits at or below the belt at
// print_z. The belt is a tilted plane, so the circle's leading edge crosses it
// |shear| * radius lower than its centre; stopping a node when its centre crosses
// would leave that edge floating a radius above the belt.
bool belt_node_landed(const Point &pos_slicing, double radius, double print_z) const;
// The same for a polygon: every point of it is at or below the belt.
bool belt_polygon_landed(const ExPolygon &poly, double print_z) const;
+1 -40
View File
@@ -3462,37 +3462,6 @@ static void generate_support_areas(Print &print, TreeSupport* tree_support, cons
// this struct is used to easy retrieve setting. No other function except those in TreeModelVolumes and generate_initial_areas() have knowledge of the existence of multiple meshes being processed.
//FIXME this is a copy
// Contains config settings to avoid loading them in every function. This was done to improve readability of the code.
// Belt printer: add virtual "belt raft" layers below the object so
// organic branches can extend below the model's first layer and
// terminate at the belt surface instead of creating a flat base at Z=0.
{
PrintObject &po = *print.get_object(processing.second.front());
const auto &sp = po.slicing_parameters();
const auto &pcfg = po.print()->config();
BeltFloorContext ctx;
ctx.init_local(sp, pcfg, po.belt_global_z_offset());
if (ctx.is_active() && std::abs(po.belt_global_z_offset()) > EPSILON
&& pcfg.belt_support_floor_mode.value == BeltSupportFloorMode::GeneratorOnly) {
// z_shift_local is the belt surface height at Y=0 in local coords.
// Extend below the belt so the base expansion and build-plate
// termination happen inside the belt region and get clipped.
// Use the distance from the pre-shear bbox min Z to the part's
// post-shear min Z, plus 10mm for base expansion headroom.
double bb_min_z = std::abs(belt_remapped_bbox(*po.model_object(), pcfg).min.z());
double extra_depth = bb_min_z + 10.;
int num_extra = std::max(0, (int)std::ceil(extra_depth / sp.layer_height));
if (num_extra > 0) {
// Insert belt raft layers at the front, from lowest Z to highest.
std::vector<coordf_t> belt_layers;
belt_layers.reserve(num_extra);
for (int i = num_extra; i >= 1; --i)
belt_layers.push_back(sp.first_object_layer_height - i * sp.layer_height);
// Prepend to existing raft_layers (if any).
auto &rl = processing.first.raft_layers;
rl.insert(rl.begin(), belt_layers.begin(), belt_layers.end());
}
}
}
const TreeSupportSettings &config = processing.first;
BOOST_LOG_TRIVIAL(info) << "Processing support tree mesh group " << counter + 1 << " of " << grouped_meshes.size() << " containing " << grouped_meshes[counter].second.size() << " meshes.";
auto t_start = std::chrono::high_resolution_clock::now();
@@ -3687,8 +3656,7 @@ static void generate_support_areas(Print &print, TreeSupport* tree_support, cons
const auto &pcfg = print_object.print()->config();
BeltFloorContext ctx;
ctx.init_local(sp, pcfg, print_object.belt_global_z_offset());
if (ctx.is_active()
&& pcfg.belt_support_floor_mode.value == BeltSupportFloorMode::GeneratorOnly) {
if (ctx.is_active()) {
tbb::parallel_for_each(layers_sorted.begin(), layers_sorted.end(), [&](SupportGeneratorLayer *layer) {
if (!layer || layer->polygons.empty())
return;
@@ -3984,13 +3952,6 @@ void organic_draw_branches(
const double tiny_area = tiny_area_threshold();
//FIXME parallelize?
for (LayerIndex i = 0; i < LayerIndex(slices.size()); ++i) {
// ORCA: safety offset when trimming collision/bed to improve robustness.
slices[i] = diff_clipped(slices[i], volumes.getCollision(0, layer_begin + i, true), ApplySafetyOffset::Yes); // FIXME parent_uses_min || draw_area.element->state.use_min_xy_dist);
slices[i] = intersection(slices[i], volumes.m_bed_area, ApplySafetyOffset::Yes);
// Belt floor: clip branch slices against the belt surface plane.
LayerIndex belt_idx = layer_begin + i;
if (belt_idx < LayerIndex(volumes.m_belt_floor.size()) && !volumes.m_belt_floor[belt_idx].empty())
slices[i] = diff(slices[i], volumes.m_belt_floor[belt_idx]);
remove_small(slices[i], tiny_area);
}
+14 -13
View File
@@ -36,6 +36,7 @@
#include <tuple>
#include "libslic3r/Preset.hpp"
#include "libslic3r/Config.hpp"
#include <Eigen/Geometry>
#if BOOST_VERSION >= 107800
#include <boost/timer/timer.hpp>
@@ -766,25 +767,25 @@ void Bed3D::render_gravity_arrow(const Transform3d& view_matrix, const Transform
m_gravity_arrow.reset();
return;
}
const Vec3d gravity_dir = -up_dir;
// Build the arrow model (same dimensions as the axis arrows)
if (!m_gravity_arrow.is_initialized()) {
const float stem_length = Axes::DefaultStemLength;
const float tip_radius = Axes::DefaultTipRadius;
const float tip_length = Axes::DefaultTipLength;
const float stem_radius = stem_length / 75.f; // same ratio as axis cylinders
m_gravity_arrow.init_from(stilized_arrow(16, tip_radius, tip_length, stem_radius, stem_length));
// A plain line along the tilted "up" direction -- the way the layers lean, i.e.
// the gantry -- drawn like the bed axes (no tip: the other direction is not
// possible) and shorter than them, so it reads as a hint inside the YZ corner.
const float length = 0.6f * m_axes.get_total_length();
if (!m_gravity_arrow.is_initialized() || m_gravity_arrow_length != length) {
m_gravity_arrow.reset();
m_gravity_arrow.init_from(smooth_cylinder(16, /*Radius*/ length / 75.f, length));
m_gravity_arrow_length = length;
}
// The arrow model points along +Z by default. Compute rotation to align with gravity_dir.
// Rotation axis = cross(+Z, gravity_dir), angle = acos(dot(+Z, gravity_dir))
// The cylinder model points along +Z. Compute the rotation that aligns it with
// up_dir: rotation axis = cross(+Z, up_dir), angle = acos(dot(+Z, up_dir)).
Vec3d from = Vec3d::UnitZ();
Vec3d to = gravity_dir;
Vec3d to = up_dir;
double dot = from.dot(to);
Transform3d rot = Transform3d::Identity();
if (dot < -0.9999) {
// Nearly opposite — rotate 180° around X
// Nearly opposite -- rotate 180 degrees around X
rot = Eigen::AngleAxisd(M_PI, Vec3d::UnitX()) * rot;
} else if (dot < 0.9999) {
Vec3d axis = from.cross(to).normalized();
@@ -800,7 +801,7 @@ void Bed3D::render_gravity_arrow(const Transform3d& view_matrix, const Transform
shader->start_using();
const Camera& camera = wxGetApp().plater()->get_camera();
Transform3d model_matrix = rot;
Transform3d model_matrix = Eigen::Translation3d(m_axes.get_origin()) * rot;
shader->set_uniform("view_model_matrix", camera.get_view_matrix() * model_matrix);
shader->set_uniform("projection_matrix", camera.get_projection_matrix());
+1
View File
@@ -116,6 +116,7 @@ private:
GLModel m_model;
Vec3d m_model_offset{ Vec3d::Zero() };
GLModel m_gravity_arrow;
float m_gravity_arrow_length{ 0.f };
Axes m_axes;
float m_scale_factor{ 1.0f };
+13 -53
View File
@@ -1318,13 +1318,11 @@ std::vector<int> GCodeViewer::get_plater_extruder()
// Belt printers: compute the full machine->model back-transform from the print
// config, so the "designed" (upright) G-code preview maps each toolpath vertex
// back to Cartesian space. The G-code forward pipeline is (BeltKinematics::
// to_machine_coords): gcode = MachineFrame( AxisRemap( X ) ), with X = model if
// gcode_back_transform (write already un-rotated to Cartesian) else BeltForward(
// model). So the inverse is:
// model = [BeltForward^-1 if !gcode_back_transform] . AxisRemap^-1 . MachineFrame^-1
// All parts are config-driven affines -> handles any rotation/shear/scale/axis-
// remap combination. (origin-snap is a per-instance translation that only shifts
// position, not orientation, so it is intentionally omitted.)
// to_machine_coords): gcode = MachineFrame( AxisRemap( X ) ), with X the model
// already un-rotated to Cartesian by the back-transform. So the inverse is:
// model = AxisRemap^-1 . MachineFrame^-1
// (origin-snap is a per-instance translation that only shifts position, not
// orientation, so it is intentionally omitted.)
static Transform3d compute_belt_back_transform(const PrintConfig& cfg)
{
if (!cfg.belt_printer.value)
@@ -1360,11 +1358,7 @@ static Transform3d compute_belt_back_transform(const PrintConfig& cfg)
}
const Transform3d ar_inv = ar.inverse();
Transform3d bf_inv = Transform3d::Identity();
if (!cfg.gcode_back_transform.value)
bf_inv = BeltTransformPipeline::build_forward_transform(cfg).inverse();
return bf_inv * ar_inv * mf_inv;
return ar_inv * mf_inv;
}
//BBS: always load shell at preview
@@ -1393,11 +1387,7 @@ void GCodeViewer::load_as_gcode(const GCodeProcessorResult& gcode_result, const
m_viewer.set_dim_previous_layers_brightness(0.01f * std::stoi(get_app_config()->get("preview_dim_previous_layers_brightness")));
// avoid processing if called with the same gcode_result.
// On a belt printer the toolpath geometry fed to libvgcode also depends on the
// designed/raw view state (the back-transform is applied in convert), so the
// same result is converted again only when that view has been toggled.
const bool same_belt_view = !m_belt_view_enabled || m_last_belt_show_designed == m_belt_show_designed;
if (m_last_result_id == gcode_result.id && wxGetApp().is_editor() && same_belt_view) {
if (m_last_result_id == gcode_result.id && wxGetApp().is_editor()) {
//BBS: add logs
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": the same id %1%, return directly, result %2% ") % m_last_result_id % (&gcode_result);
@@ -1439,22 +1429,20 @@ void GCodeViewer::load_as_gcode(const GCodeProcessorResult& gcode_result, const
}
// convert data from PrusaSlicer format to libvgcode format.
// Belt printers: when the "designed (upright) view" is active, back-transform
// the toolpath geometry into model/Cartesian space using the general belt
// inverse (handles any mesh rotation + shear + axis remap). When off, the raw
// machine-frame G-code is shown (useful for debugging the transform itself).
// Belt printers: back-transform the toolpath geometry into model/Cartesian
// space using the general belt inverse (handles the mesh rotation, shear and
// axis remap), so the part is shown upright, the way it was designed.
const bool is_belt = m_belt_view_enabled && print.config().belt_printer.value;
Transform3d belt_inv = (is_belt && m_belt_show_designed)
? compute_belt_back_transform(print.config()) : Transform3d::Identity();
Transform3d belt_inv = is_belt ? compute_belt_back_transform(print.config()) : Transform3d::Identity();
// Belt: move positions are stored as gcode_Z + belt_z_origin (the start G-code's
// purge-blob advance baked into the machine-Z origin by its G92 Z0 resets). Subtract
// that constant before the linear back-transform so every toolpath maps to the model's
// belt coordinate. Without it the back-transform mixes the offset with the gantry-Y
// term, leaving a per-move designed-Y error that min-corner anchoring cannot remove
// when a bridge/keel move happens to cancel it at the bbox minimum.
if (is_belt && m_belt_show_designed && gcode_result.belt_z_origin != 0.0f)
if (is_belt && gcode_result.belt_z_origin != 0.0f)
belt_inv = belt_inv * Transform3d(Eigen::Translation3d(Vec3d(0.0, 0.0, -double(gcode_result.belt_z_origin))));
const bool apply_belt = is_belt && m_belt_show_designed
const bool apply_belt = is_belt
&& !belt_inv.matrix().isApprox(Transform3d::Identity().matrix());
if (apply_belt) {
// The linear belt back-transform recovers the print's shape and orientation but not
@@ -1691,7 +1679,6 @@ void GCodeViewer::load_as_gcode(const GCodeProcessorResult& gcode_result, const
//BBS: move the id to the end of reset
m_last_result_id = gcode_result.id;
m_last_belt_show_designed = m_belt_show_designed;
m_gcode_result = &gcode_result;
m_move_type_counts.fill(0);
for (auto& move_type_times : m_move_type_times)
@@ -5146,33 +5133,6 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv
if (m_nozzle_nums > 1 && (m_viewer.get_view_type() == libvgcode::EViewType::Summary || m_viewer.get_view_type() == libvgcode::EViewType::ColorPrint)) // ORCA show only on summary and filament tab
render_legend_color_arr_recommen(window_padding);
// Belt printer: toggle for viewing designed (upright) vs. machine-frame G-code.
// Rendered with a separator and hint text so users can find it easily.
if (m_belt_view_enabled) {
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
ImGui::Dummy({ window_padding, 0 });
ImGui::SameLine();
ImGui::TextColored(ImVec4(0.f, 0.59f, 0.53f, 1.f), "%s", _u8L("Belt printer").c_str());
ImGui::Dummy({ window_padding, 0 });
ImGui::SameLine();
// Checked = show the raw machine-frame G-code (designed/upright view off). Worded to
// match the canvas-toolbar menu item "Show raw G-code (belt only)". m_belt_show_designed
// is the inverse of this checkbox, so bind a temporary and flip it on change.
bool show_raw = !m_belt_show_designed;
const std::string key = wxGetApp().shortcuts().display(Shortcut::ToggleBeltRawGcode);
const std::string label = _u8L("Show raw G-code (belt only)") + (key.empty() ? std::string() : " [" + key + "]");
if (ImGui::Checkbox(label.c_str(), &show_raw)) {
m_belt_show_designed = !show_raw;
// The designed-view back-transform is baked into the toolpath geometry at load
// time, so the toggle only takes effect once the preview is re-converted. Defer
// the refresh to the next event-loop tick (CallAfter) to avoid re-entering the
// preview load from inside legend rendering.
if (Plater* plater = wxGetApp().plater())
plater->CallAfter([plater]() { plater->refresh_belt_view(); });
}
}
legend_height = ImGui::GetCurrentWindow()->Size.y;
imgui.end();
-6
View File
@@ -199,8 +199,6 @@ private:
std::vector<int> m_plater_extruder;
bool m_gl_data_initialized{ false };
unsigned int m_last_result_id{ 0 };
// Belt printers: the view the loaded result was converted for (see load_as_gcode).
bool m_last_belt_show_designed{ true };
//BBS: save m_gcode_result as well
const GCodeProcessorResult* m_gcode_result;
std::array<unsigned int, static_cast<size_t>(EMoveType::Count)> m_move_type_counts{};
@@ -265,8 +263,6 @@ mutable bool m_no_render_path { false };
bool m_belt_view_enabled = false;
float m_belt_angle_deg = 0.f;
bool m_belt_show_designed = true; // Toggle: designed (upright, back-transformed) view by default;
// turn off (hotkey B) to inspect the raw machine-frame G-code.
libvgcode::Viewer m_viewer;
// ORCA: section view, as the viewer has it. What it cuts away casts no shadow.
@@ -406,8 +402,6 @@ public:
void set_belt_printer(bool enabled, float angle_deg) { m_belt_view_enabled = enabled; m_belt_angle_deg = angle_deg; }
bool is_belt_view() const { return m_belt_view_enabled && m_belt_angle_deg > 0.f; }
void toggle_belt_show_designed() { if (m_belt_view_enabled) m_belt_show_designed = !m_belt_show_designed; }
bool is_belt_show_designed() const { return m_belt_show_designed; }
size_t get_extruders_count() { return m_extruders_count; }
void push_combo_style();
-24
View File
@@ -3778,16 +3778,6 @@ bool GLCanvas3D::handle_shortcut(const KeyChord& chord)
m_dirty = true;
request_extra_frame();
break;
case Shortcut::ToggleBeltRawGcode:
// Same state as the legend checkbox and the canvas-toolbar menu item. The designed-view
// back-transform is baked into the toolpaths at load time, so the preview is re-converted.
if (m_gcode_viewer.is_belt_view()) {
m_gcode_viewer.toggle_belt_show_designed();
if (Plater* plater = wxGetApp().plater())
plater->refresh_belt_view();
m_dirty = true;
}
break;
case Shortcut::ToggleOneLayerMode:
get_gcode_viewer().get_layers_slider()->switch_one_layer_mode();
m_dirty = true;
@@ -10190,20 +10180,6 @@ void GLCanvas3D::_render_canvas_toolbar()
ImGui::TextColored(enable ? ImVec4(1,1,1,1) : ImGui::GetStyleColorVec4(ImGuiCol_TextDisabled), "%s", into_u8(condition ? ImGui::VisibleIcon : ImGui::HiddenIcon).c_str());
};
// Belt printers, G-code preview only: toggle the designed (upright) view vs the raw
// machine-frame G-code. Same state as the shortcut and the legend checkbox; the reload is
// deferred (CallAfter) so the preview is not rebuilt mid-render.
if (m_canvas_type == ECanvasType::CanvasPreview && m_gcode_viewer.is_belt_view()) {
create_menu_item( _utf8(L("Show raw G-code (belt only)")),
true,
!m_gcode_viewer.is_belt_show_designed(), // eye lit = raw machine-frame G-code (designed view off)
[this, p]{
m_gcode_viewer.toggle_belt_show_designed();
p->CallAfter([p]{ p->refresh_belt_view(); });
}
);
ImGui::Separator();
}
create_menu_item( _utf8(L("3D Navigator")),
m_canvas_type != ECanvasType::CanvasAssembleView, // not work on assembly
-11
View File
@@ -369,17 +369,6 @@ void Preview::reload_print(bool only_gcode)
m_only_gcode = only_gcode;
}
void Preview::refresh_belt_view()
{
// Re-run the G-code preview conversion so the belt "designed view" toggle takes effect
// (the back-transform is baked into the toolpath geometry in GCodeViewer::load_as_gcode,
// whose same-result cache also keys on the view state, so the re-convert runs).
// Reset m_loaded_print to bypass the "already loaded" guard the way reload_print does, but
// keep the current layer (Z) range and only-gcode mode so the view doesn't jump on toggle.
m_loaded_print = nullptr;
load_print(true /*keep_z_range*/, m_only_gcode);
}
//BBS: always load shell at preview
void Preview::load_shells(const Print& print, bool force_previewing)
{
-1
View File
@@ -149,7 +149,6 @@ public:
void load_print(bool keep_z_range = false, bool only_gcode = false);
void reload_print(bool only_gcode = false);
// Belt printers: re-convert the G-code preview so the "designed view" toggle takes effect.
void refresh_belt_view();
//BBS: always load shell at preview
void load_shells(const Print& print, bool force_previewing = false);
void reset_shells();
-5
View File
@@ -17885,11 +17885,6 @@ void Plater::reload_print()
p->preview->reload_print();
}
void Plater::refresh_belt_view()
{
p->preview->refresh_belt_view();
}
// BBS
wxString Plater::get_project_name()
{
-1
View File
@@ -388,7 +388,6 @@ public:
// Belt printers: re-run the G-code preview conversion so the "designed view" toggle
// (hotkey B / legend checkbox) takes effect; the back-transform is applied to the
// toolpath geometry at load time. Keeps the current layer range and only-gcode mode.
void refresh_belt_view();
// SoftFever
void calib_pa(const Calib_Params& params);
-1
View File
@@ -149,7 +149,6 @@ constexpr std::array<ShortcutInfo, size_t(Shortcut::Count)> shortcut_table = {{
SHORTCUT(ShowWireframe, "show_wireframe", L("Show/Hide wireframe"), CANVAS, { WXK_RETURN, CTRL_SHIFT }),
SHORTCUT(ToggleGcodeWindow, "toggle_gcode_window", L("On/Off G-code window"), PREVIEW, { 'C' }),
SHORTCUT(ToggleOneLayerMode, "toggle_one_layer_mode", L("On/Off one layer mode of the vertical slider"), PREVIEW, { 'L' }),
SHORTCUT(ToggleBeltRawGcode, "toggle_belt_raw_gcode", L("Show raw G-code (belt only)"), PREVIEW, { 'B' }),
// Application
SHORTCUT(Preferences, "preferences", L("Preferences"), GLOBAL, PREFERENCES_CHORD),
+1 -1
View File
@@ -46,7 +46,7 @@ enum class Shortcut : uint8_t {
// Camera
ViewDefault, ViewTop, ViewBottom, ViewFront, ViewRear, ViewLeft, ViewRight, ViewPlate, ZoomIn, ZoomOut, Mouse3DSettings,
// Display
ShowLabels, ShowWireframe, ToggleGcodeWindow, ToggleOneLayerMode, ToggleBeltRawGcode,
ShowLabels, ShowWireframe, ToggleGcodeWindow, ToggleOneLayerMode,
// Application
Preferences, Search, SwitchView, CollapseSidebar, ReloadDevicePage, KeyboardShortcuts,
// Speed Dial
+8 -23
View File
@@ -5250,25 +5250,12 @@ void TabPrinter::build_fff()
// Belt tilt: the sole mesh-side transform and the single source of truth for
// the physical tilt (drives bed rendering and support gravity tilt too).
// Isometric rotation, no distortion; the back-transform inverts it before the
// machine-frame remap.
{
Line line = { L("Belt tilt"),
L("Belt tilt axis and angle, applied as a mesh rotation before "
"slicing. Also drives bed rendering and support gravity tilt. "
"Isometric (no distortion); the back-transform inverts it before "
"the machine-frame remap.") };
line.label_path = "printer_basic_information_belt_printer#belt-tilt";
line.append_option(belt_og->get_option("belt_slice_rotation"));
line.append_option(belt_og->get_option("belt_slice_rotation_angle"));
belt_og->append_line(line);
}
belt_og->append_single_option_line("belt_preslice_global", "printer_basic_information_belt_printer#global-mesh-transforms");
belt_og->append_single_option_line("gcode_back_transform", "printer_basic_information_belt_printer#g-code-back-transform");
belt_og->append_single_option_line("first_layer_plane_thickness", "printer_basic_information_belt_printer#first-layer-band-thickness");
// Support floor: split across lines so each setting's own mode controls
// its visibility (floor_mode = Develop, floor_offset = Advanced).
// machine-frame remap. The angle is what a user checks against the machine;
// the axis is a profile-level kinematics choice, so it is Develop-only. They
// are separate rows because a shared line is shown by its first option's mode.
belt_og->append_single_option_line("belt_slice_rotation_angle", "printer_basic_information_belt_printer#tilt-angle");
belt_og->append_single_option_line("belt_slice_rotation", "printer_basic_information_belt_printer#tilt-axis");
belt_og->append_single_option_line("belt_support_floor_offset", "printer_basic_information_belt_printer#support-floor-z-offset");
belt_og->append_single_option_line("belt_support_floor_mode", "printer_basic_information_belt_printer#floor-mode");
// Machine-frame transform: the shear (cot) + scale (1/sin) that map
// Cartesian G-code into the printer's physical machine frame are derived
@@ -6326,6 +6313,7 @@ void TabPrinter::toggle_options()
bool expert_or_above = (m_mode >= comExpert);
toggle_line("belt_printer_infinite_y", is_belt);
// Belt tilt: the sole mesh-side belt transform (visible by default in belt mode).
toggle_line("belt_slice_rotation_angle", is_belt);
toggle_line("belt_slice_rotation", is_belt);
// Remap, back-transform, and global mesh-transforms toggles are gated by belt
@@ -6333,9 +6321,8 @@ void TabPrinter::toggle_options()
// ConfigOptionMode in PrintConfig.cpp. The axis remap is Develop-only: a
// printer profile sets it once for its kinematics, and a wrong value sends
// the gantry outside the machine.
for (auto el : {"gcode_remap_x", "gcode_remap_y", "gcode_remap_z", "gcode_back_transform"})
for (auto el : {"gcode_remap_x", "gcode_remap_y", "gcode_remap_z"})
toggle_line(el, is_belt);
toggle_line("belt_preslice_global", is_belt);
// Rotation is the only mesh-side belt transform. Gray out its angle when no
// rotation axis is selected.
@@ -6348,10 +6335,8 @@ void TabPrinter::toggle_options()
toggle_line("belt_frame_tilt_angle",
is_belt && expert_or_above && m_config->opt_bool("belt_frame_tilt_decouple"));
toggle_line("first_layer_plane_thickness", is_belt);
for (auto el : {"belt_support_floor_mode", "belt_support_floor_offset"})
toggle_line(el, is_belt);
toggle_line("belt_support_floor_offset", is_belt);
const bool support_parallel_printheads = printer_cfg.opt_bool("support_parallel_printheads");
toggle_line("parallel_printheads_count", support_parallel_printheads);
-3
View File
@@ -56,7 +56,6 @@ TEST_CASE("Belt machine coordinates retain a non-45-degree slicing angle", "[GCo
config.belt_printer.value = true;
config.belt_slice_rotation.value = BeltRotationAxis::X;
config.belt_slice_rotation_angle.value = 30.;
config.gcode_back_transform.value = true;
config.gcode_remap_x.value = RemapAxis::PosX;
config.gcode_remap_y.value = RemapAxis::PosZ;
config.gcode_remap_z.value = RemapAxis::PosY;
@@ -1026,10 +1025,8 @@ SCENARIO("Belt: the first travel does not lift through the uninitialised origin"
// Machine-frame + slicer->world back-transform config (X tilt, 45 deg).
PrintConfig belt_config;
belt_config.belt_printer.value = true;
belt_config.gcode_back_transform.value = true;
belt_config.belt_slice_rotation.value = BeltRotationAxis::X;
belt_config.belt_slice_rotation_angle.value = 45.0;
belt_config.belt_preslice_global.value = true;
belt_config.belt_frame_tilt_decouple.value = false;
belt_config.belt_frame_tilt_angle.value = 45.0;
+98 -4
View File
@@ -30,6 +30,10 @@
#include "libslic3r/BuildVolume.hpp"
#include "libslic3r/Support/TreeModelVolumes.hpp"
#include "libslic3r/Support/TreeSupportCommon.hpp"
#include "libslic3r/Support/BeltFloorContext.hpp"
#include "libslic3r/ExtrusionEntity.hpp"
#include "libslic3r/Polyline.hpp"
#include <limits>
#include "libslic3r/Polygon.hpp"
#include "libslic3r/Model.hpp"
#include "libslic3r/GCodeReader.hpp"
@@ -1041,16 +1045,12 @@ TEST_CASE("Belt-only keys at non-default values leave non-belt G-code unchanged"
{ "belt_printer_infinite_y", 0 },
{ "belt_slice_rotation", "y" },
{ "belt_slice_rotation_angle", 30 },
{ "belt_preslice_global", 0 },
{ "gcode_remap_x", "rev_x" },
{ "gcode_remap_y", "pos_z" },
{ "gcode_remap_z", "pos_y" },
{ "gcode_back_transform", 0 },
{ "belt_frame_tilt_decouple", 1 },
{ "belt_frame_tilt_angle", 30 },
{ "first_layer_plane_thickness", 1 },
{ "belt_support_floor_offset", -5 },
{ "belt_support_floor_mode", "none" },
{ "enable_belt_purge_tower", 1 },
{ "belt_purge_tower_width", 10 },
{ "leading_brim_length", 10 },
@@ -1242,3 +1242,97 @@ TEST_CASE("Organic tree supports place a support blocker at its own height above
CHECK(collides(last + 1));
CHECK(collides(last + num_raft));
}
// A part with an overhang on its LEADING side (the end that prints first) needs
// supports below the object's own lowest slicing layer: the belt under that overhang
// is reached before the object's first contact with it, so the support layers sit at
// a lower slicing Z than any object layer. A generator that stops at the object's
// first layer, or at global Z = 0, leaves those supports floating above the belt.
TEST_CASE("Belt supports reach the belt under a leading overhang", "[Print][belt][Support][Regression]")
{
// default resolves to organic for tree support; tree_hybrid is the classic tree.
const char *support_type = GENERATE("normal(auto)", "tree(auto)");
const char *support_style = GENERATE("default", "organic", "tree_hybrid");
if (std::string(support_type) == "normal(auto)" && std::string(support_style) != "default")
return; // organic and tree_hybrid are tree styles
DYNAMIC_SECTION(support_type << " / " << support_style) {
// A 20 mm cube with a 2 mm thick fin that leaves its top edge and reaches
// 20 mm toward -Y, the end of the part that prints first, climbing at 45 deg
// as it goes (from z = 18 at the cube to z = 38 at the tip). With the layers
// leaning toward -Y at 45 deg the fin's underside is parallel to the layers:
// a ceiling 20 x 28 mm in one layer, with nothing but air between it and the
// belt, which lies up to 41 mm (of slicing Z) below the object's own lowest
// point. Support has to span all of it.
indexed_triangle_set its = its_make_cube(20., 20., 20.);
indexed_triangle_set fin = its_make_cube(20., 20., 2.);
Transform3d shear = Transform3d::Identity();
shear.matrix() << 1., 0., 0., 0.,
0., 1., 0., -20.,
0., -1., 1., 38.,
0., 0., 0., 1.;
its_transform(fin, shear);
its_merge(its, fin);
TriangleMesh mesh(std::move(its));
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
config.set_deserialize_strict({
{ "belt_printer", 1 },
{ "belt_slice_rotation", "x" },
{ "belt_slice_rotation_angle", 45 },
{ "gcode_remap_x", "rev_x" },
{ "gcode_remap_y", "pos_z" },
{ "gcode_remap_z", "pos_y" },
{ "layer_height", 0.2 },
{ "initial_layer_print_height", 0.2 },
{ "skirt_loops", 0 },
{ "z_hop", 0 },
{ "enable_support", 1 },
{ "support_type", support_type },
{ "support_style", support_style },
{ "support_threshold_angle", 30 },
{ "machine_start_gcode", "T[initial_tool]\n" },
{ "layer_change_gcode", "G92 E0\n" },
});
Print print;
Model model;
init_print({ mesh }, print, model, config);
// On the bed, not at its corner: organic tree support clips its branches to
// the bed outline, and the fixture leaves the object at the origin.
model.objects.front()->instances.front()->set_offset(Vec3d(100., 100., 0.));
print.apply(model, config);
print.set_status_silent();
print.process();
const PrintObject &object = *print.objects().front();
REQUIRE(! object.layers().empty());
BeltFloorContext floor;
REQUIRE(floor.init(object.slicing_parameters(), print.config()));
// The lowest support layer that prints anything, and the belt floor beneath it.
const SupportLayer *lowest = nullptr;
for (const SupportLayer *layer : object.support_layers())
if (! layer->support_fills.empty() && (lowest == nullptr || layer->print_z < lowest->print_z))
lowest = layer;
REQUIRE(lowest != nullptr);
double floor_under_lowest = std::numeric_limits<double>::max();
for (const ExtrusionEntity *entity : lowest->support_fills.flatten().entities)
for (const Polyline &pl : entity->as_polylines())
for (const Point &pt : pl.points)
floor_under_lowest = std::min(floor_under_lowest, floor.floor_print_z(pt));
// The object's lowest geometry. The slicing frame starts at the lowest
// belt-floor point under the footprint, so the layers below the leading
// tip of the overhang are empty.
double first_object_z = std::numeric_limits<double>::max();
for (const Layer *layer : object.layers())
if (! layer->lslices.empty()) { first_object_z = layer->print_z; break; }
REQUIRE(first_object_z < std::numeric_limits<double>::max());
INFO("lowest support z " << lowest->print_z << ", floor under it " << floor_under_lowest
<< ", first object layer " << first_object_z);
// Well below the object's own lowest layer (the belt under the tip of the fin
// is ~41 mm of slicing Z below the cube's leading edge, which rests on it)...
CHECK(lowest->print_z < first_object_z - 5.);
// ...and resting on the belt: within a few layers of the floor beneath its own lines.
CHECK(lowest->print_z - floor_under_lowest < 4. * 0.2 + EPSILON);
CHECK(lowest->print_z - floor_under_lowest > -0.2 - EPSILON);
}
}
+39 -53
View File
@@ -715,9 +715,10 @@ static double first_role_z(const std::string &gcode, const std::string &role)
return z;
}
// Number of object layers that carry a belt brim band. Each such band is emitted as one
// contiguous brim pass, so for a single object whose first-contact layer carries a band
// (the apron prologue folds into that layer's pass) this equals role_passes(gcode, "brim").
// Number of object layers that carry a belt brim band. Every band prints at its own
// layer Z, so this equals role_layers(gcode, "brim") (plus any apron bands below the
// first object layer). It is not a pass count: the bands on the empty lead-in layers
// ahead of the object's first contact print back to back, so they fold into one pass.
static int nonempty_belt_brim_layers(const PrintObject &object)
{
int n = 0;
@@ -727,6 +728,30 @@ static int nonempty_belt_brim_layers(const PrintObject &object)
return n;
}
// Number of distinct Z heights at which `role` extrudes: one per layer that prints it.
static int role_layers(const std::string &gcode, const std::string &role)
{
std::set<long> zs;
GCodeReader reader;
reader.parse_buffer(gcode, [&](GCodeReader &self, const GCodeReader::GCodeLine &line) {
if (! line.extruding(self) || line.dist_XY(self) <= EPSILON)
return;
if (line.comment().find(role) != std::string_view::npos)
zs.insert(std::lround(self.z() * 1000.));
});
return int(zs.size());
}
// Apron bands below the object's first layer that print something.
static int belt_brim_apron_bands(const PrintObject &object)
{
int n = 0;
for (const BeltBrimBand &band : object.belt_brim_prologue())
if (! band.fills.empty())
++ n;
return n;
}
// For each active tool, the ordinal (1-based, over extruding moves) of the FIRST move whose
// role comment contains `role`. Lets a per-object ordering check key off the object's
// unique wall filament.
@@ -801,12 +826,15 @@ TEST_CASE("Belt brim on an object layer precedes its perimeters, once", "[SkirtB
CHECK(seq[0] == "brim");
CHECK(seq[1] == "perimeter");
// Exactly once: every band is one contiguous pass (the apron prologue folds into the
// first layer's), so the pass count equals the number of layers carrying a band - not
// twice it, which double-emission would give, nor fewer, which a dropped band would.
const int bands = nonempty_belt_brim_layers(*print.objects().front());
// Exactly once: every band prints at its own layer Z, so the number of Z heights with
// brim equals the number of bands - not fewer, which a dropped band would give. (A
// double emission would print twice at one Z: the pass count below catches that for
// the bands that sit on layers with perimeters.)
const PrintObject &object = *print.objects().front();
const int bands = nonempty_belt_brim_layers(object);
REQUIRE(bands > 0);
CHECK(role_passes(gc, "brim") == bands);
CHECK(role_layers(gc, "brim") == bands + belt_brim_apron_bands(object));
CHECK(role_passes(gc, "brim") <= bands);
}
// B - single extruder (filament id 1). Every band must survive the 1-based -> 0-based
@@ -826,9 +854,10 @@ TEST_CASE("Belt brim on a single extruder emits every band once", "[SkirtBrim][b
init_print({ cube(20) }, print, model, config);
const std::string gc = gcode(print);
const int expected = nonempty_belt_brim_layers(*print.objects().front());
const PrintObject &object = *print.objects().front();
const int expected = nonempty_belt_brim_layers(object) + belt_brim_apron_bands(object);
REQUIRE(expected > 0);
CHECK(role_passes(gc, "brim") == expected);
CHECK(role_layers(gc, "brim") == expected);
CHECK(belt_tools_for_role(gc, "brim") == std::set<int>{ 0 }); // filament 1 -> tool 0
}
@@ -1231,49 +1260,6 @@ TEST_CASE("Belt apron survives another object printing at the same Z", "[SkirtBr
CHECK(two >= 1.8 * one);
}
TEST_CASE("Belt brim allows instances placed across the belt", "[SkirtBrim][belt]")
{
// Only movement ALONG the belt changes an instance's belt-floor Z, so copies placed
// side by side ACROSS it share one set of bands and must still get a brim. The first
// version of this guard refused every multi-instance object outright, silently
// dropping the brim.
//
// The global belt flags are off here so the instances stay in one PrintObject; with
// them on, PrintApply splits each instance into its own object and the case cannot
// arise at all.
auto multi_instance_has_brim = [](double dx, double dy) {
DynamicPrintConfig config = belt_brim_config();
config.set_deserialize_strict({
{ "belt_preslice_global", 0 },
{ "brim_type", "outer_only" },
{ "brim_width", 4 },
{ "brim_object_gap", 0 },
});
Print print;
Model model;
ModelObject *object = model.add_object();
object->name += "object.stl";
object->add_volume(cube(20));
object->add_instance()->set_offset(Vec3d(80., 80., 0.));
object->add_instance()->set_offset(Vec3d(80. + dx, 80. + dy, 0.));
object->ensure_on_bed();
print.auto_assign_extruders(object);
print.apply(model, config);
print.validate();
print.set_status_silent();
print.process();
REQUIRE(print.objects().size() == 1);
REQUIRE(print.objects().front()->instances().size() == 2);
return print.objects().front()->has_belt_brim();
};
// X is across the belt when the tilt is about X, since the shear then runs along Y.
CHECK(multi_instance_has_brim(40., 0.));
// Y is along the belt: the copies sit at different belt heights and would each need
// their own bands, so the brim is refused (and validate() warns).
CHECK_FALSE(multi_instance_has_brim(0., 40.));
}
TEST_CASE("Belt brim coexists with support material", "[SkirtBrim][belt]")
{
// Supports put extra layers into the same z stream as the apron bands, which is what