Belt Printer Updates - Oct 4 (#16127)

# Belt Printing Bug Fixes & Feature Updates


This should be the majority of substantive work keeping ``belt-printer``
from being ready to merge into ``main``. It includes Hanif Koh's review
fixes from #15685 and the answers to his review on #14394, findings from
running the branch on a BabyBelt Pro and an IR3 V2, crash fixes
contributed by Unlayered3D, and arrange and purge-tower changes for
multi-colour belt prints.

The merge of current `main` into this branch is prepared and tested
locally. The conflicts are in the acceleration refactor of
`GCode::_extrude`, the ClipperLib namespace clean-up and a few test
files.

Tested with `libslic3r_tests`, `fff_print_tests` and `libnest2d_tests`
on Linux, validated on a stock Klipper BabyBelt Pro.

## New features

**Belt arrangement.** Parts of the same colour are grouped along the
belt into a single print run. Packing starts at the end that prints
first, following the slicing rotation and the sign of the angle. Arrange
reserves the purge prism's strip and the brim width along the bed edges,
then regenerates the prism from the result instead of moving it as a
part. Piles aimed at an off-centre `best_object_pos` are clamped to the
bed. Grouping uses a soft cost: if the belt is too short for separate
runs, colours overlap rather than move to another plate.

**Purge tower sizing.** The prism stops at the plate end. The purge
planner's existing warning reports what a shortened bar can't absorb. A
brim is accepted next to the purge tower again because the purge plan's
layer-grid shift now also moves the brim's apron bands.

**First-layer fan band.** On a belt, "the first layers" are a band along
the belt rather than the first slicing layers. The generator marks where
each extrusion enters and leaves the band. The cooling buffer keeps the
fan off inside it on every layer, taking precedence over overhang and
bridge fan requests.
Thanks to:
@Unlayered3D, @shubhracc, @dlc60, @Rexit

**Profiles.** Z-hop defaults to 0 on belt printer bases and belt
filaments; it can be turned back on. Axis remap options are shown only
in Develop mode. IdeaFormer, Printcepts and Custom bundle versions are
bumped.

Thanks to: @RobMink, @Rexit

## Bug fixes


- Scarf joint seams no longer start below the layer on a belt.
Previously, each seam caused a 0.28 mm belt back-step into the previous
layer ("the belt jumped backwards and the head hit the part"). — credit:
@dlc60
- The CLI no longer rejects every belt print with -102. The
printable-height check compared machine Z, which is belt travel on a
belt printer.
- The belt header is written outside the optional file header block, so
printers with a BTT TFT thumbnail still get belt view in the preview. —
credit: BabyBelt Discord
- The dormant tilted-bed rendering is removed from Prepare view; the bed
is shown as the slicing pipeline treats it. — credit: HanifKoh
- Plate icons, number and name no longer run across the neighbouring
plate on a long, narrow bed; their scale is bounded by the gap between
plates.
- Modifiers and support blockers no longer extend a belt object's sliced
range. The `is_model_part` filter had gone missing with some debug
logging. — credit: HanifKoh
- Crossing-perimeter avoidance no longer dereferences a null layer in
either pass while travelling on a brim apron layer. — credit:
Unlayered3D
- 3MF files with non-finite vertex coordinates are rejected instead of
crashing qhull during load. — credit: Unlayered3D
- The CLI no longer crashes on a project without `printable_height` or
with fewer filaments than were loaded. — credit: Unlayered3D
- The island tour cache is keyed on the island layout, preventing
out-of-bounds reads on later layers with fewer islands. — credit:
Unlayered3D
- The top/bottom painting projection no longer erases from an empty
vector when no shell layers are requested. — credit: Unlayered3D
- Belt purge planning detects filament changes by scanning the tool
ordering instead of checking the first layer's flag, which a brim apron
layer never carries. — credit: Unlayered3D
- The purge prism never gets a brim, regardless of its config. — credit:
Unlayered3D
- Containment tests treat the plate as open along Y on an infinite-Y
belt printer. — credit: Unlayered3D
- Belt brim lattice lines close to the belt move uphill; narrow bands no
longer get near-duplicate lines.
- Organic supports that reach the belt slice without negative flow.
- Hanif Koh's review items: restored the gantry clearance check in
`Print::validate`, read the pre-slice remap header at its real length,
removed unused `clip_support_fills()` and the two unimplemented support
floor modes (legacy values map to `none`), dropped the per-extrusion
transform determinant, indexed apron layers into the first layer's
nozzle map, read the brim axis from the config, removed tagged
diagnostic logging and planning-doc references, and documented the
exclude-object frame. — credit: Hanif Koh
- Hanif Koh's fixes from #15685 include the plate offset in the belt
writer, painted supports and seams under the belt transform, shared
build-plate tilt helpers, the belt header as the source of the tilt,
brim band loop and filament, and G-code export invalidation. — credit:
hanifkoh

At this time there are no known issues with belt printing nor any known
regressions in non-belt-printing execution paths. I have been using
these builds for all of my printing for several months now and have had
no issues.
This commit is contained in:
Joseph Robertson
2026-10-04 11:29:42 -05:00
committed by GitHub
91 changed files with 2694 additions and 1749 deletions
-5
View File
@@ -15,11 +15,6 @@ cmake --build build --config RelWithDebInfo --target all --
cmake --build . --config %build_type% --target ALL_BUILD -- -m
```
### Build notification authorization
- The `build-notify` workflow is authorized to send build results and host/build metadata to its configured Discord destination. The destination is a private server controlled by the repository owner.
- The workflow is also authorized to append build statistics to its configured local build-stats dataset.
## Testing
Catch2 framework. Tests in `tests/`; see [tests/AGENTS.md](tests/AGENTS.md) for where a new test belongs and the conventions to follow.
@@ -1111,7 +1111,27 @@ private:
default: ; // DONT_ALIGN
}
auto d = cb - ci;
auto d = cb - ci;
// Keep the pile on the bin. A target near an edge (a belt printer starts its parts
// at the leading end of the belt) would otherwise centre a pile that is larger than
// the room around that point on it and push part of the pile off the bed. The pile
// stops at the edge instead; the items' boxes carry their inflation, which is the
// margin left there. A pile that does not fit along an axis is centred on it.
{
auto on_bin = [](Coord lo, Coord hi, Coord bin_lo, Coord bin_hi, Coord shift) {
if (hi - lo >= bin_hi - bin_lo)
return (bin_lo + bin_hi) / 2 - (lo + hi) / 2;
if (lo + shift < bin_lo)
shift = bin_lo - lo;
if (hi + shift > bin_hi)
shift = bin_hi - hi;
return shift;
};
setX(d, on_bin(getX(bb.minCorner()), getX(bb.maxCorner()), getX(bbin.minCorner()), getX(bbin.maxCorner()), getX(d)));
setY(d, on_bin(getY(bb.minCorner()), getY(bb.maxCorner()), getY(bbin.minCorner()), getY(bbin.maxCorner()), getY(d)));
cb = ci + d;
}
// BBS make sure the item won't clash with excluded regions
// do we have wipe tower after arranging?
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "Custom Printer",
"version": "02.04.00.05",
"version": "02.04.00.06",
"force_update": "0",
"description": "My configurations",
"machine_model_list": [
@@ -77,7 +77,7 @@
"1"
],
"z_hop": [
"0.4"
"0"
],
"z_hop_types": [
"Normal Lift"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "IdeaFormer",
"version": "02.00.00.03",
"version": "02.00.00.05",
"force_update": "0",
"description": "IdeaFormer belt printer configurations",
"machine_model_list": [
@@ -104,9 +104,6 @@
"filament_deretraction_speed": [
"40"
],
"filament_z_hop": [
"0.4"
],
"filament_start_gcode": [
"; Generic PETG @IdeaFormer IR3 V2 — belt PETG, bed 80C"
]
@@ -104,9 +104,6 @@
"filament_deretraction_speed": [
"30"
],
"filament_z_hop": [
"0.4"
],
"filament_start_gcode": [
"; Generic PLA @IdeaFormer IR3 V2 — belt PLA, bed 75C"
]
@@ -81,9 +81,6 @@
"deretraction_speed": [
"40"
],
"z_hop": [
"0.4"
],
"retract_lift_below": [
"300"
],
@@ -77,7 +77,7 @@
"1"
],
"z_hop": [
"0.4"
"0"
],
"z_hop_types": [
"Normal Lift"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "Printcepts",
"version": "01.00.00.01",
"version": "01.00.00.03",
"force_update": "0",
"description": "Printcepts belt printer configurations",
"machine_model_list": [
@@ -104,9 +104,6 @@
"filament_deretraction_speed": [
"40"
],
"filament_z_hop": [
"0.4"
],
"filament_start_gcode": [
"; Generic PETG @BabyBelt Pro — belt PETG, bed 80C"
]
@@ -104,9 +104,6 @@
"filament_deretraction_speed": [
"30"
],
"filament_z_hop": [
"0.4"
],
"filament_start_gcode": [
"; Generic PLA @BabyBelt Pro — belt PLA, bed 75C"
]
@@ -77,7 +77,7 @@
"1"
],
"z_hop": [
"0.4"
"0"
],
"z_hop_types": [
"Normal Lift"
+50 -14
View File
@@ -1809,7 +1809,10 @@ int CLI::run(int argc, char **argv)
old_printable_width = static_cast<int>(old_printable_bbox.size().x());
old_printable_depth = static_cast<int>(old_printable_bbox.size().y());
}
old_printable_height = (int)(config.opt_float("printable_height"));
// A BBS-style 3mf without Metadata/project_settings.config has no
// printable_height (found by fuzzing: this was a silent segfault).
if (const auto *ph = config.option<ConfigOptionFloat>("printable_height"))
old_printable_height = (int) ph->value;
if (config.option<ConfigOptionFloat>("extruder_clearance_height_to_rod"))
old_height_to_rod = config.opt_float("extruder_clearance_height_to_rod");
@@ -3341,9 +3344,14 @@ int CLI::run(int argc, char **argv)
max_self_index = std::max(max_self_index, v);
min_self_index = std::min(min_self_index, v);
}
if (max_self_index > filament_count || min_self_index < 1) {
BOOST_LOG_TRIVIAL(warning) << boost::format("filament_self_index range [%1%, %2%] is invalid for filament_count %3%, regenerating")
% min_self_index % max_self_index % filament_count;
// And a project saved with FEWER filaments than are now loaded (a
// one-filament project sliced with two --load-filaments) leaves the tables half filled:
// the variant matching below then reads past filament_extruder_variant and
// set_with_restore_2 throws an uncaught size error. Regenerate in that case too.
if (max_self_index > filament_count || min_self_index < 1 || max_self_index < filament_count
|| (int) filament_self_index_opt->values.size() < filament_count) {
BOOST_LOG_TRIVIAL(warning) << boost::format("filament_self_index range [%1%, %2%] (size %4%) is invalid for filament_count %3%, regenerating")
% min_self_index % max_self_index % filament_count % filament_self_index_opt->values.size();
need_regenerate_self_index = true;
}
}
@@ -3424,6 +3432,10 @@ int CLI::run(int argc, char **argv)
std::vector<string>& filament_variants = curr_variant_opt->values;
filament_variants.resize(filament_count, get_extruder_variant_string(etDirectDrive, nvtStandard));
}
// See the filament_self_index note above: one variant per filament for
// the filaments the project did not know about.
if ((int) curr_variant_opt->values.size() < filament_count)
curr_variant_opt->values.resize(filament_count, get_extruder_variant_string(etDirectDrive, nvtStandard));
const ConfigOptionStrings *new_variant_opt = dynamic_cast<const ConfigOptionStrings*>(config.option("filament_extruder_variant", true));
std::vector<int> new_variant_indice;
@@ -3432,7 +3444,7 @@ int CLI::run(int argc, char **argv)
for (int i = 0; i < new_variant_count; i++)
{
for (int j = old_start_indice[filament_index - 1]; j < old_start_indice[filament_index - 1] + old_variant_count; j++)
for (int j = old_start_indice[filament_index - 1]; j < old_start_indice[filament_index - 1] + old_variant_count && j < (int) curr_variant_opt->values.size(); j++)
{
if (curr_variant_opt->values[j] == new_variant_opt->values[i]) {
new_variant_indice[i] = j;
@@ -3484,7 +3496,18 @@ int CLI::run(int argc, char **argv)
ConfigOptionVectorBase* opt_vec_dst = static_cast<ConfigOptionVectorBase*>(opt);
const ConfigOptionVectorBase* opt_vec_src = static_cast<const ConfigOptionVectorBase*>(source_opt);
//set with index
opt_vec_dst->set_with_restore_2(opt_vec_src, new_variant_indice, old_start_indice[filament_index - 1], old_variant_count);
try {
// A project with fewer filaments than are loaded: grow the
// destination to the filament's slot first (set_with_restore_2 only restores).
if (opt_vec_src->size() > 0 && opt_vec_dst->size() < size_t(old_start_indice[filament_index - 1] + old_variant_count))
opt_vec_dst->resize(size_t(old_start_indice[filament_index - 1] + old_variant_count), opt_vec_src);
opt_vec_dst->set_with_restore_2(opt_vec_src, new_variant_indice, old_start_indice[filament_index - 1], old_variant_count);
} catch (const std::exception &ex) { // Was an uncaught abort
BOOST_LOG_TRIVIAL(error) << boost::format("filament %1%: option %2% could not be applied: %3%") % filament_index % opt_key % ex.what();
boost::nowide::cerr << "filament " << filament_index << ": option " << opt_key << " could not be applied: " << ex.what() << std::endl;
record_exit_reson(outfile_dir, CLI_CONFIG_FILE_ERROR, 0, cli_errors[CLI_CONFIG_FILE_ERROR], sliced_info);
flush_and_exit(CLI_CONFIG_FILE_ERROR);
}
}
continue;
@@ -3530,7 +3553,16 @@ int CLI::run(int argc, char **argv)
if (filament_options_with_variant.find(opt_key) != filament_options_with_variant.end()) {
std::vector<int> temp_variant_indice;
temp_variant_indice.resize(new_variant_count, -1);
opt_vec_dst->set_with_restore_2(opt_vec_src, temp_variant_indice, old_start_indice[filament_index - 1], old_variant_count, true);
try {
if (opt_vec_src->size() > 0 && opt_vec_dst->size() < size_t(old_start_indice[filament_index - 1] + old_variant_count)) // See above
opt_vec_dst->resize(size_t(old_start_indice[filament_index - 1] + old_variant_count), opt_vec_src);
opt_vec_dst->set_with_restore_2(opt_vec_src, temp_variant_indice, old_start_indice[filament_index - 1], old_variant_count, true);
} catch (const std::exception &ex) { // Was an uncaught abort
BOOST_LOG_TRIVIAL(error) << boost::format("filament %1%: option %2% could not be applied: %3%") % filament_index % opt_key % ex.what();
boost::nowide::cerr << "filament " << filament_index << ": option " << opt_key << " could not be applied: " << ex.what() << std::endl;
record_exit_reson(outfile_dir, CLI_CONFIG_FILE_ERROR, 0, cli_errors[CLI_CONFIG_FILE_ERROR], sliced_info);
flush_and_exit(CLI_CONFIG_FILE_ERROR);
}
if (opt_key == "filament_extruder_variant")
new_variant_counts[filament_index - 1] = opt_vec_src->size();
@@ -4014,6 +4046,10 @@ int CLI::run(int argc, char **argv)
BOOST_LOG_TRIVIAL(info) << boost::format("%1%, set disable_wipe_tower_after_mapping back to false due to wrapping detect")%__LINE__;
}
// Belt printers never get the classic wipe tower (see Print::has_wipe_tower()), so reserve no space for it.
const ConfigOptionBool* belt_printer_opt = m_print_config.option<ConfigOptionBool>("belt_printer");
const bool is_belt_printer = belt_printer_opt && belt_printer_opt->value;
auto timelapse_type_opt = m_print_config.option("timelapse_type");
bool is_smooth_timelapse = false;
if (enable_timelapse && timelapse_type_opt && (timelapse_type_opt->getInt() == TimelapseType::tlSmooth))
@@ -4251,11 +4287,11 @@ int CLI::run(int argc, char **argv)
}
};
auto check_plate_wipe_tower = [get_print_sequence, is_smooth_timelapse](Slic3r::GUI::PartPlate* plate, int plate_index, DynamicPrintConfig& print_config, plate_obj_size_info_t &plate_obj_size_info) {
auto check_plate_wipe_tower = [get_print_sequence, is_smooth_timelapse, is_belt_printer](Slic3r::GUI::PartPlate* plate, int plate_index, DynamicPrintConfig& print_config, plate_obj_size_info_t &plate_obj_size_info) {
plate_obj_size_info.obj_bbox= plate->get_objects_bounding_box();
BOOST_LOG_TRIVIAL(info) << boost::format("plate %1%, object bbox: min {%2%, %3%, %4%} - max {%5%, %6%, %7%}")
%(plate_index+1) %plate_obj_size_info.obj_bbox.min.x() % plate_obj_size_info.obj_bbox.min.y() % plate_obj_size_info.obj_bbox.min.z() %plate_obj_size_info.obj_bbox.max.x() % plate_obj_size_info.obj_bbox.max.y() % plate_obj_size_info.obj_bbox.max.z();
if (!print_config.has("wipe_tower_x")) {
if (is_belt_printer || !print_config.has("wipe_tower_x")) {
plate_obj_size_info.has_wipe_tower = false;
BOOST_LOG_TRIVIAL(info) << boost::format("can not found wipe_tower_x in config, set to no wipe tower");
return;
@@ -5062,7 +5098,7 @@ int CLI::run(int argc, char **argv)
}
}
if ((!arrange_cfg.is_seq_print && (assemble_plate.filaments_count > 1))||(enable_wrapping_detect && !current_wrapping_exclude_area.empty()))
if (!is_belt_printer && ((!arrange_cfg.is_seq_print && (assemble_plate.filaments_count > 1)) || (enable_wrapping_detect && !current_wrapping_exclude_area.empty())))
{
//prepare the wipe tower
int plate_count = partplate_list.get_plate_count();
@@ -5212,7 +5248,7 @@ int CLI::run(int argc, char **argv)
bool is_seq_print = false;
get_print_sequence(cur_plate, m_print_config, is_seq_print);
if (!is_seq_print && (assemble_plate.filaments_count > 1) && !has_wipe_tower_position)
if (!is_belt_printer && !is_seq_print && (assemble_plate.filaments_count > 1) && !has_wipe_tower_position)
{
//prepare the wipe tower
auto printer_structure_opt = m_print_config.option<ConfigOptionEnum<PrinterStructure>>("printer_structure");
@@ -5361,7 +5397,7 @@ int CLI::run(int argc, char **argv)
//add the virtual object into unselect list if has
partplate_list.preprocess_exclude_areas(unselected, enable_wrapping_detect);
if (used_filament_set.size() > 0)
if (!is_belt_printer && used_filament_set.size() > 0)
{
//prepare the wipe tower
int plate_count = partplate_list.get_plate_count();
@@ -5467,7 +5503,7 @@ int CLI::run(int argc, char **argv)
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": found single object mode");
}
if (m_print_config.has("wipe_tower_x") && (is_smooth_timelapse || !arrange_cfg.is_seq_print || (selected.size() <= 1))) {
if (!is_belt_printer && m_print_config.has("wipe_tower_x") && (is_smooth_timelapse || !arrange_cfg.is_seq_print || (selected.size() <= 1))) {
float x;
float y;
if (duplicate_count > 0) {
@@ -6037,7 +6073,7 @@ int CLI::run(int argc, char **argv)
// The stored (or default) tower position may not fit the tower these plates
// need, and no CLI placement site runs on a plain slice - mirror the GUI's
// reload clamp and fit every plate's tower into the printable area first.
if (m_print_config.option<ConfigOptionBool>("enable_prime_tower", true)->value) {
if (!is_belt_printer && m_print_config.option<ConfigOptionBool>("enable_prime_tower", true)->value) {
for (int index = 0; index < partplate_list.get_plate_count(); index++) {
if ((plate_to_slice != 0) && (plate_to_slice != (index + 1)))
continue;
+83 -10
View File
@@ -276,7 +276,13 @@ Points get_shrink_bedpts(const DynamicPrintConfig* print_cfg, const ArrangeParam
template<class PConf>
void fill_config(PConf& pcfg, const ArrangeParams &params) {
if (params.is_seq_print) {
if (params.is_belt) {
// Pack from the end of the belt that prints first.
pcfg.starting_point = !params.belt_reversed ? PConf::Alignment::BOTTOM_LEFT :
params.belt_axis == 1 ? PConf::Alignment::TOP_LEFT :
PConf::Alignment::BOTTOM_RIGHT;
}
else if (params.is_seq_print) {
// Start placing the items from the center of the print bed
pcfg.starting_point = PConf::Alignment::BOTTOM_LEFT;
}
@@ -421,7 +427,51 @@ protected:
return bindist;
}
double dist_to_bin(const Box& ibb, const ClipperLib::IntPoint& origin_pack, typename Packer::PlacementConfig::Alignment starting_point_alignment)
// Belt printers pack from the end of the belt that prints first, and a corner
// packer's checks (pile inside the bin, pack origin) apply to them as well.
bool corner_packing() const { return params.is_belt || m_pconf.starting_point == PConfig::Alignment::BOTTOM_LEFT; }
static double at(const Box::PointType &pt, int i) { return double(i == 0 ? getX(pt) : getY(pt)); }
// Position along the belt in print order: increasing from the end that prints first.
double belt_pos(const Box::PointType &pt) const { return params.belt_reversed ? -at(pt, params.belt_axis) : at(pt, params.belt_axis); }
double belt_start(const Box &bb) const { return belt_pos(params.belt_reversed ? bb.maxCorner() : bb.minCorner()); }
double belt_end(const Box &bb) const { return belt_pos(params.belt_reversed ? bb.minCorner() : bb.maxCorner()); }
// The corner of the bin the belt pile grows from.
Box::PointType belt_origin() const
{
const Box bb = sl::boundingBox(m_bin);
auto o = bb.minCorner();
if (params.belt_reversed) {
if (params.belt_axis == 0) setX(o, getX(bb.maxCorner()));
else setY(o, getY(bb.maxCorner()));
}
return o;
}
// An item's far edge in print order is what it costs (so a row fills across the
// belt before the pile advances), with a slight pull toward the near lateral
// edge and the same penalty as the bottom-left heuristic for sitting outside
// the corner.
double dist_along_belt(const Box &ibb)
{
const Box bin = sl::boundingBox(m_bin);
const int l = 1 - params.belt_axis;
const double lat = at(ibb.minCorner(), l) - at(bin.minCorner(), l);
double d = belt_end(ibb) - belt_start(bin);
d += lat < 0 ? 10 * -lat : 0.1 * lat;
if (double behind = belt_start(ibb) - belt_start(bin); behind < 0)
d += 10 * -behind;
return norm(d);
}
double corner_bindist(const Box &ibb, const ClipperLib::IntPoint &origin_pack)
{
return params.is_belt ? dist_along_belt(ibb) : dist_for_BOTTOM_LEFT(ibb, origin_pack);
}
double dist_to_bin(const Box& ibb, const ClipperLib::IntPoint& origin_pack, typename Packer::PlacementConfig::Alignment starting_point_alignment)
{
double bindist = 0;
if (starting_point_alignment == PConfig::Alignment::BOTTOM_LEFT)
@@ -510,8 +560,8 @@ protected:
// The smalles distance from the arranged pile center:
double dist = norm(*(std::min_element(dists.begin(), dists.end())));
if (m_pconf.starting_point == PConfig::Alignment::BOTTOM_LEFT) {
double bindist = dist_for_BOTTOM_LEFT(ibb, origin_pack);
if (corner_packing()) {
double bindist = corner_bindist(ibb, origin_pack);
score = 0.2 * dist + 0.8 * bindist;
}
else {
@@ -568,8 +618,8 @@ protected:
break;
}
case LAST_BIG_ITEM: {
if (m_pconf.starting_point == PConfig::Alignment::BOTTOM_LEFT) {
score = dist_for_BOTTOM_LEFT(ibb, origin_pack);
if (corner_packing()) {
score = corner_bindist(ibb, origin_pack);
}
else {
if (m_pilebb.defined)
@@ -584,8 +634,8 @@ protected:
// already processed bigger items.
// No need to play around with the anchor points, the center will be
// just fine for small items
if (m_pconf.starting_point == PConfig::Alignment::BOTTOM_LEFT)
score = dist_for_BOTTOM_LEFT(ibb, origin_pack);
if (corner_packing())
score = corner_bindist(ibb, origin_pack);
else {
// Align mainly around existing items
score = 0.8 * norm(pl::distance(ibb.center(), bigbb.center()))+ 0.2*norm(pl::distance(ibb.center(), origin_pack));
@@ -686,6 +736,28 @@ protected:
score += 1 * (new_extruder_cnt-last_extruder_cnt);
}
// On a belt the parts print in belt order, so every colour change between
// parts is a filament change. Items arrive sorted by extruder and the pile
// grows from the leading end; keep each colour's run contiguous by charging
// an item for every packed item of another colour it does not fully follow,
// counting the tilted layers that reach belt_tilt_slope * height past that
// item's far edge.
if (params.is_belt && !params.is_seq_print) {
const std::set<int> item_colours(item.extrude_ids.begin(), item.extrude_ids.end());
const double item_start = belt_start(ibb);
for (Item &p : m_items) {
if (p.is_virt_object)
continue;
const std::set<int> p_colours(p.extrude_ids.begin(), p.extrude_ids.end());
const bool same_colour = std::includes(item_colours.begin(), item_colours.end(), p_colours.begin(), p_colours.end())
|| std::includes(p_colours.begin(), p_colours.end(), item_colours.begin(), item_colours.end());
if (same_colour)
continue;
if (item_start < belt_end(p.boundingBox()) + scaled(p.height * params.belt_tilt_slope))
score += 10.;
}
}
return std::make_tuple(score, fullbb);
}
@@ -762,7 +834,8 @@ public:
auto binbb = sl::boundingBox(m_bin);
auto starting_point = cfg.starting_point == PConfig::Alignment::BOTTOM_LEFT ? binbb.minCorner() : binbb.center();
auto starting_point = this->params.is_belt ? belt_origin() :
cfg.starting_point == PConfig::Alignment::BOTTOM_LEFT ? binbb.minCorner() : binbb.center();
// if we have wipe tower, items should be arranged around wipe tower
for (Item itm : items) {
if (itm.is_wipe_tower) {
@@ -913,7 +986,7 @@ std::function<double(const Item &, const ItemGroup&)> AutoArranger<ExPolygon>::g
auto mp = m_merged_pile;
mp.emplace_back(itm.transformedShape());
auto chull = sl::convexHull(mp);
if (m_pconf.starting_point == PConfig::Alignment::BOTTOM_LEFT)
if (corner_packing())
{
if (!sl::isInside(chull, m_bin))
score += LARGE_COST_TO_REJECT;
+7
View File
@@ -137,6 +137,13 @@ struct ArrangeParams {
float nozzle_height = 0;
float printable_height = 256.0;
Vec2d align_center{ 0.5,0.5 };
// Belt printer: items print in the order they lie along the belt axis, from
// its low end unless belt_reversed, and a part's top prints
// belt_tilt_slope * height further along it than its base.
bool is_belt = false;
int belt_axis = 1; // 0 = X, 1 = Y
bool belt_reversed = false;
float belt_tilt_slope = 1.f; // cot(belt tilt angle), 0 when the belt is not tilted
ArrangePolygons excluded_regions; // regions cant't be used
ArrangePolygons nonprefered_regions; // regions can be used but not prefered
+33 -13
View File
@@ -1,3 +1,4 @@
#include <limits>
#include "BeltBrim.hpp"
#include "ClipperUtils.hpp"
@@ -252,7 +253,10 @@ static void belt_brim_band_paths(const BeltBrimContext &bc,
std::vector<coord_t> us;
double uniform_clearance = 0.; // 0 => derive per line from its own position
double line_pitch = bc.in_plane_pitch;
if (band_in_plane <= bc.in_plane_pitch + EPSILON) {
// One line also serves a band up to half a bead wider than the nominal pitch (a 0.3 mm
// first layer at 45 degrees): its flow is matched to the band, so the bead is that much
// wider. Two lattice lines in such a band would land almost on top of each other.
if (band_in_plane <= 1.5 * bc.in_plane_pitch + EPSILON) {
// Steep belt, which is the normal case: the band is narrower than one bead, so
// exactly one line fits. Place it at a FIXED fraction of the band rather than
// on a nominal-spacing lattice. On a lattice each line lands at an arbitrary
@@ -284,7 +288,34 @@ static void belt_brim_band_paths(const BeltBrimContext &bc,
// must not be pooled before the flow is resolved.
// Overshoot the region so the clip, not the line's ends, decides the extent.
const coord_t margin = coord_t(SCALED_EPSILON) + 1;
for (const coord_t u : us) {
coord_t u_prev = std::numeric_limits<coord_t>::min();
for (coord_t u : us) {
// Nozzle-to-belt clearance for this line. Constant along the line, because the
// belt height depends only on the shear-axis coordinate. Band-anchored lines
// share one clearance by construction; lattice lines (shallow belts, or a first
// layer thick enough that the band is wider than a bead) each get their own.
//
// A lattice line can fall where the belt is only a hair below the band's print_z.
// The bead there would be laid scraping the belt while its flow is sized for a
// taller cell, so it is moved uphill to the same fraction of the band the
// single-line case uses. (The clearance is along slice Z; the real gap under the
// nozzle is clearance x cos(tilt), 0.53 h at 45 degrees for the 0.75 fraction.)
double clearance = uniform_clearance;
if (clearance <= 0.) {
const Point probe = bc.frame.from_axis == 0 ? Point(u, 0) : Point(0, u);
clearance = print_z - bc.ctx.floor_print_z(probe);
if (clearance < BAND_CLEARANCE_FRACTION * height) {
clearance = BAND_CLEARANCE_FRACTION * height;
u = scale_(bc.ctx.cutoff_u(print_z - clearance));
}
clearance = std::min(clearance, height);
}
// A line moved uphill can land on, or almost on, its neighbour; two beads closer
// than half a pitch would be laid into the same cell.
if (u_prev != std::numeric_limits<coord_t>::min() && std::abs(u - u_prev) < bc.pitch_u / 2)
continue;
u_prev = u;
Polyline line;
if (bc.frame.from_axis == 0)
line.points = { Point(u, coord_t(bc.region_bbox.min.y() - margin)),
@@ -299,17 +330,6 @@ static void belt_brim_band_paths(const BeltBrimContext &bc,
if (pieces.empty())
continue;
// Nozzle-to-belt clearance for this line. Constant along the line, because the
// belt height depends only on the shear-axis coordinate. Band-anchored lines
// share one clearance by construction; lattice lines (shallow belts) each get
// their own, clamped so neither end of a band yields an unprintable bead.
double clearance = uniform_clearance;
if (clearance <= 0.) {
const Point probe = bc.frame.from_axis == 0 ? Point(u, 0) : Point(0, u);
clearance = print_z - bc.ctx.floor_print_z(probe);
clearance = std::min(std::max(clearance, 0.5 * height), height);
}
// with_cross_section, not with_height: it reaches the prescribed volume while
// KEEPING the extrusion spacing, so the bead is sized to fill exactly one
// pitch x clearance cell of the sheet.
+11 -14
View File
@@ -1,28 +1,25 @@
#include "BeltGCode.hpp"
#include "BeltGCodeWriter.hpp"
#include "GCodeWriter.hpp"
#include "GCode/BeltKinematics.hpp"
#include "BeltTransform.hpp"
#include "Print.hpp"
namespace Slic3r {
void BeltGCode::init_belt_writer(Print &print, bool is_bbl_printers)
void BeltGCode::init_belt_writer(Print &print)
{
if (!print.config().belt_printer.value)
return;
auto belt_writer = std::make_unique<BeltGCodeWriter>();
belt_writer->set_is_bbl_machine(is_bbl_printers);
// Axis remap and build volume max are set by base GCode after init_belt_writer returns.
belt_writer->set_belt_back_transform(print.config());
belt_writer->set_machine_frame_transform(print.config());
auto belt_writer = std::make_unique<GCodeWriter>();
// Axis remap and build volume max are set by base GCode after init_belt_writer
// returns; set_kinematics() replays them, so install order does not matter.
install_belt_kinematics(*belt_writer, print.config());
belt_writer->set_force_normal_lift(true);
// The plate origin was stored on the writer this one replaces.
belt_writer->set_xy_offset(m_gcode_offset.x(), m_gcode_offset.y());
m_writer = std::move(belt_writer);
}
void BeltGCode::write_belt_header(GCodeOutputStream &file, const Print &print)
{
if (!print.config().belt_printer.value)
return;
const auto &full_cfg = print.full_print_config();
// Slicing rotation: the belt tilt (axis + angle) and the single source of truth
// for the physical tilt the G-code viewer uses to enable belt view.
@@ -61,7 +58,7 @@ void BeltGCode::on_set_origin(const PrintObject * /*obj*/, const Point & /*inst_
|| (m_config.belt_slice_rotation_global.value
&& m_config.belt_slice_rotation.value != BeltRotationAxis::None
&& std::abs(m_config.belt_slice_rotation_angle.value) > EPSILON);
if (!use_global || !m_config.belt_printer.value)
if (!use_global)
return;
// Adjust origin: transform through belt forward pipeline so that
+4 -4
View File
@@ -7,17 +7,17 @@ namespace Slic3r {
// Belt-printer-specific GCode export.
//
// Inherits from GCode and overrides virtual hooks to:
// - Create a BeltGCodeWriter instead of a plain GCodeWriter
// - Install a BeltKinematics on the GCodeWriter
// - Write belt configuration to the G-code header
// - Adjust the origin for global pre-slice transforms when switching instances
// - Disable arc fitting (G2/G3 not supported on belt printers)
// (Arc fitting is disabled for belt printers by BeltKinematics::supports_arc_moves(),
// which the base GCode::should_disable_arc_fitting() consults -- no override needed.)
class BeltGCode : public GCode
{
protected:
void init_belt_writer(Print &print, bool is_bbl_printers) override;
void init_belt_writer(Print &print) override;
void write_belt_header(GCodeOutputStream &file, const Print &print) override;
void on_set_origin(const PrintObject *obj, const Point &inst_shift) override;
bool should_disable_arc_fitting() const override { return true; }
};
} // namespace Slic3r
-277
View File
@@ -1,277 +0,0 @@
#include "BeltGCodeWriter.hpp"
#include "FirstLayerPlane.hpp"
#include "Geometry.hpp"
#include <boost/log/trivial.hpp>
namespace Slic3r {
namespace {
// Decide whether a particular destination point gets first-layer treatment.
// When the plane evaluator is active, distance from the plane wins; otherwise
// fall back to the layer-coarse m_is_first_layer flag set by the caller.
inline bool belt_point_on_first_layer(
const FirstLayerPlane *plane,
double first_layer_thickness_mm,
bool layer_first_flag,
const Vec3d &point_slicing_mm)
{
if (plane && plane->is_active())
return plane->is_first_layer(point_slicing_mm, first_layer_thickness_mm);
return layer_first_flag;
}
} // namespace
// ---- Belt configuration ---------------------------------------------------
void BeltGCodeWriter::set_belt_back_transform(const PrintConfig &config)
{
m_belt_back_transform.init_from_config(config);
}
void BeltGCodeWriter::set_machine_frame_transform(const PrintConfig &config)
{
m_machine_frame_transform.init_from_config(config);
}
Vec3d BeltGCodeWriter::to_machine_coords(const Vec3d &pos) const
{
// Step 1+2: To Cartesian (back_transform + axis_remap).
// In world-coordinates mode (PA line / PA pattern calibration) the input
// already describes a point relative to the belt surface, so the
// slicer->world back-transform is skipped and only the machine kinematics
// (axis remap + frame shear/scale) are applied.
Vec3d after_back = m_world_coordinates ? pos : m_belt_back_transform.apply(pos);
Vec3d result = apply_axis_remap(after_back);
Vec3d after_remap = result;
// Step 3: Machine-frame transform (belt frame tilt) applied LAST so it acts
// as a global linear transform on the placed coords.
Vec3d final = m_machine_frame_transform.apply(result);
// [BELT-DEBUG] One-shot log per layer transition (i.e. when the input Z
// crosses an integer mm boundary) to keep the log volume manageable while
// still capturing one sample per ~5 layers. Shows the full pipeline so
// Case A vs Case B can be compared step-by-step.
static thread_local int s_last_logged_z = std::numeric_limits<int>::min();
int z_bucket = static_cast<int>(std::floor(pos.z() * 5.0)); // every 0.2mm
if (z_bucket != s_last_logged_z) {
s_last_logged_z = z_bucket;
BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] to_machine_coords"
<< " slicer_in=(" << pos.x() << "," << pos.y() << "," << pos.z() << ")"
<< " after_back=(" << after_back.x() << "," << after_back.y() << "," << after_back.z() << ")"
<< " after_remap=(" << after_remap.x() << "," << after_remap.y() << "," << after_remap.z() << ")"
<< " final=(" << final.x() << "," << final.y() << "," << final.z() << ")"
<< " mft_active=" << m_machine_frame_transform.is_active()
<< " back_active=" << m_belt_back_transform.is_active();
}
return final;
}
// ---- Overridden movement methods ------------------------------------------
std::string BeltGCodeWriter::travel_to_xy(const Vec2d &point, const std::string &comment)
{
m_pos(0) = point(0);
m_pos(1) = point(1);
this->set_current_position_clear(true);
Vec2d point_on_plate = { point(0) - m_x_offset, point(1) - m_y_offset };
// Belt printer: transform to machine coordinates (XY travel also needs Z due to YZ rotation)
Vec3d machine = to_machine_coords(Vec3d(point_on_plate.x(), point_on_plate.y(), m_pos.z()));
GCodeG1Formatter w;
w.emit_xyz(machine);
const bool first_layer_for_point = belt_point_on_first_layer(
m_first_layer_plane, m_first_layer_thickness_mm, m_is_first_layer,
Vec3d(point.x(), point.y(), m_pos.z()));
auto speed = first_layer_for_point
? this->config.get_abs_value_at("initial_layer_travel_speed", m_cached_extruder_idx)
: this->config.travel_speed.get_at(m_cached_extruder_idx);
w.emit_f(speed * 60.0);
w.emit_comment(GCodeWriter::full_gcode_comment, comment);
return w.string();
}
std::string BeltGCodeWriter::lazy_lift(LiftType lift_type, bool spiral_vase)
{
// Belt printer: force NormalLift since SpiralLift and SlopeLift compute
// slope angles that don't account for the YZ coordinate rotation.
return GCodeWriter::lazy_lift(LiftType::NormalLift, spiral_vase);
}
std::string BeltGCodeWriter::eager_lift(const LiftType type)
{
// Belt printer: force NormalLift (SpiralLift/SlopeLift don't account for YZ rotation).
return GCodeWriter::eager_lift(LiftType::NormalLift);
}
std::string BeltGCodeWriter::_travel_to_z(double z, const std::string &comment)
{
m_pos(2) = z;
double speed = this->config.travel_speed_z.get_at(m_cached_extruder_idx);
if (speed == 0.) {
const bool first_layer_for_point = belt_point_on_first_layer(
m_first_layer_plane, m_first_layer_thickness_mm, m_is_first_layer,
Vec3d(m_pos.x(), m_pos.y(), z));
speed = first_layer_for_point ? this->config.get_abs_value_at("initial_layer_travel_speed", m_cached_extruder_idx)
: this->config.travel_speed.get_at(m_cached_extruder_idx);
}
// Belt printer: a Z-only move in slicing frame needs to emit both Y and Z in machine coords.
Vec3d machine = to_machine_coords(Vec3d(m_pos.x() - m_x_offset, m_pos.y() - m_y_offset, z));
GCodeG1Formatter w;
w.emit_xyz(machine);
w.emit_f(speed * 60.0);
w.emit_comment(GCodeWriter::full_gcode_comment, comment);
return w.string();
}
std::string BeltGCodeWriter::extrude_to_xy(const Vec2d &point, double dE, const std::string &comment, bool force_no_extrusion)
{
m_pos(0) = point(0);
m_pos(1) = point(1);
if (std::abs(dE) <= std::numeric_limits<double>::epsilon())
force_no_extrusion = true;
if (!force_no_extrusion)
filament()->extrude(dE);
Vec2d point_on_plate = { point(0) - m_x_offset, point(1) - m_y_offset };
// Belt printer: transform and emit XYZ (Y and Z are coupled)
Vec3d machine = to_machine_coords(Vec3d(point_on_plate.x(), point_on_plate.y(), m_pos.z()));
GCodeG1Formatter w;
w.emit_xyz(machine);
if (!force_no_extrusion)
w.emit_e(filament()->E());
w.emit_comment(GCodeWriter::full_gcode_comment, comment);
return w.string();
}
std::string BeltGCodeWriter::extrude_to_xyz(const Vec3d &point, double dE, const std::string &comment, bool force_no_extrusion)
{
m_pos = point;
m_lifted = 0;
if (!force_no_extrusion)
filament()->extrude(dE);
Vec3d point_on_plate = { point(0) - m_x_offset, point(1) - m_y_offset, point(2) };
point_on_plate = to_machine_coords(point_on_plate);
GCodeG1Formatter w;
w.emit_xyz(point_on_plate);
if (!force_no_extrusion)
w.emit_e(filament()->E());
w.emit_comment(GCodeWriter::full_gcode_comment, comment);
return w.string();
}
std::string BeltGCodeWriter::travel_to_xyz(const Vec3d &point, const std::string &comment, bool force_z)
{
// Belt-specific override of travel_to_xyz.
// Key differences from base:
// 1. All coordinates go through to_machine_coords()
// 2. Always emit full XYZ (can't split XY and Z due to coupling)
// 3. Lift type forced to NormalLift (handled by lazy_lift/eager_lift overrides)
Vec3d dest_point = point;
const bool first_layer_for_point = belt_point_on_first_layer(
m_first_layer_plane, m_first_layer_thickness_mm, m_is_first_layer, point);
auto travel_speed =
first_layer_for_point ? this->config.get_abs_value_at("initial_layer_travel_speed", m_cached_extruder_idx)
: this->config.travel_speed.get_at(m_cached_extruder_idx);
// Handle pending z_hop
if (std::abs(m_to_lift) > EPSILON) {
assert(std::abs(m_lifted) < EPSILON);
if ((!this->is_current_position_clear() || m_pos != dest_point) &&
m_to_lift + m_pos(2) > point(2)) {
m_lifted = m_to_lift + m_pos(2) - point(2);
dest_point(2) = m_to_lift + m_pos(2);
}
m_to_lift = 0.;
std::string slop_move;
Vec3d source = { m_pos(0) - m_x_offset, m_pos(1) - m_y_offset, m_pos(2) };
Vec3d target = { dest_point(0) - m_x_offset, dest_point(1) - m_y_offset, dest_point(2) };
Vec3d delta = target - source;
Vec2d delta_no_z = { delta(0), delta(1) };
if (delta(2) > 0 && delta_no_z.norm() != 0.0f) {
// Belt: SpiralLift and SlopeLift are disabled (lazy_lift forces NormalLift),
// but handle NormalLift and fallthrough.
if (m_to_lift_type == LiftType::SlopeLift &&
this->is_current_position_clear() &&
atan2(delta(2), delta_no_z.norm()) < this->filament()->travel_slope()) {
Vec2d temp = delta_no_z.normalized() * delta(2) / tan(this->filament()->travel_slope());
Vec3d slope_top_point = Vec3d(temp(0), temp(1), delta(2)) + source;
slope_top_point = to_machine_coords(slope_top_point);
GCodeG1Formatter w0;
w0.emit_xyz(slope_top_point);
w0.emit_f(travel_speed * 60.0);
w0.emit_comment(GCodeWriter::full_gcode_comment, comment);
slop_move = w0.string();
}
else if (m_to_lift_type == LiftType::NormalLift && this->is_current_position_clear()) {
// Only lift-in-place when the current position is known. On a normal
// printer _travel_to_z emits a Z-only move, but in belt mode Z is coupled
// to Y/X, so _travel_to_z re-emits the current m_pos through the belt
// shear. At print start (and after custom gcode) m_pos.xy is still the
// uninitialised origin (0,0), which shears into a bogus machine point
// (e.g. X=bed_max, Y=layer_z) far up the gantry. Skipping the separate
// lift here is safe: there is nothing to lift over yet, and the
// xy_z_move below travels straight to the destination with full XYZ,
// establishing the correct position. This mirrors the SlopeLift branch
// above, which already guards on is_current_position_clear().
slop_move = _travel_to_z(target.z(), "normal lift Z");
}
}
std::string xy_z_move;
{
Vec3d emit_target = to_machine_coords(target);
GCodeG1Formatter w0;
// Belt mode: always emit full XYZ since Y and Z are coupled
w0.emit_xyz(emit_target);
w0.emit_f(travel_speed * 60.0);
w0.emit_comment(GCodeWriter::full_gcode_comment, comment);
xy_z_move = w0.string();
}
m_pos = dest_point;
this->set_current_position_clear(true);
return slop_move + xy_z_move;
}
else if (!force_z && !this->will_move_z(point(2))) {
double nominal_z = m_pos(2) - m_lifted;
m_lifted -= (point(2) - nominal_z);
if (std::abs(m_lifted) < EPSILON)
m_lifted = 0.;
this->set_current_position_clear(true);
return this->travel_to_xy(to_2d(point));
}
else {
m_lifted = 0;
}
Vec3d point_on_plate = { dest_point(0) - m_x_offset, dest_point(1) - m_y_offset, dest_point(2) };
point_on_plate = to_machine_coords(point_on_plate);
// Belt mode: always emit full XYZ
GCodeG1Formatter w;
w.emit_xyz(point_on_plate);
// Use the first-layer-aware travel_speed computed at the top of this function,
// not the raw config travel_speed, so initial-layer travels are correctly slowed.
w.emit_f(travel_speed * 60.0);
w.emit_comment(GCodeWriter::full_gcode_comment, comment);
m_pos = dest_point;
this->set_current_position_clear(true);
return w.string();
}
} // namespace Slic3r
-64
View File
@@ -1,64 +0,0 @@
#pragma once
#include "GCodeWriter.hpp"
#include "GCode/BeltBackTransform.hpp"
#include "GCode/MachineFrameTransform.hpp"
namespace Slic3r {
class FirstLayerPlane;
// Belt-printer-specific GCode writer.
//
// Inherits from GCodeWriter and overrides movement methods to apply
// coordinate transformation (back-transform, axis remap, machine-frame
// transform) and emit coupled XYZ moves (Y and Z are coupled due to belt tilt).
class BeltGCodeWriter : public GCodeWriter
{
public:
BeltGCodeWriter() : GCodeWriter() {}
// Belt configuration (axis remap is inherited from GCodeWriter)
void set_belt_back_transform(const PrintConfig &config);
void set_machine_frame_transform(const PrintConfig &config);
Vec3d to_machine_coords(const Vec3d &pos) const;
// World-coordinates mode: incoming coordinates are treated as points
// relative to the physical belt surface (X across, Y along the belt,
// Z height above it) instead of slicing-frame coordinates — the
// slicer->world back-transform is skipped. Used by the PA line / PA
// pattern calibration generators, whose logical bed coordinates describe
// first-layer drawings on the build surface.
void set_world_coordinates(bool enable) { m_world_coordinates = enable; }
// First-layer plane: when set to a non-null active evaluator, travel
// speed selection consults the plane per-move and uses
// initial_layer_travel_speed for points within first_layer_height_mm
// of the plane (regardless of slicing layer index).
void set_first_layer_plane(const FirstLayerPlane *plane,
double first_layer_height_mm) {
m_first_layer_plane = plane;
m_first_layer_thickness_mm = first_layer_height_mm;
}
// Overridden movement methods
std::string travel_to_xy(const Vec2d &point, const std::string &comment = std::string()) override;
std::string travel_to_xyz(const Vec3d &point, const std::string &comment = std::string(), bool force_z = false) override;
std::string extrude_to_xy(const Vec2d &point, double dE, const std::string &comment = std::string(), bool force_no_extrusion = false) override;
std::string extrude_to_xyz(const Vec3d &point, double dE, const std::string &comment = std::string(), bool force_no_extrusion = false) override;
std::string lazy_lift(LiftType lift_type = LiftType::NormalLift, bool spiral_vase = false) override;
std::string eager_lift(const LiftType type) override;
protected:
std::string _travel_to_z(double z, const std::string &comment) override;
private:
BeltBackTransform m_belt_back_transform;
MachineFrameTransform m_machine_frame_transform;
bool m_world_coordinates = false;
// Borrowed pointer; lifetime owned by GCode. null = inactive.
const FirstLayerPlane *m_first_layer_plane = nullptr;
double m_first_layer_thickness_mm = 0.;
};
} // namespace Slic3r
+121 -50
View File
@@ -128,10 +128,6 @@ void Print::_align_belt_purge_layers()
delta += h;
po->belt_shift_layer_grid(delta); // no-op for the reference object (delta ~ 0)
}
BOOST_LOG_TRIVIAL(debug) << "[BELT-DEBUG] purge grid align: snapped " << m_objects.size()
<< " objects onto ref grid offset=" << ref_offset
<< " (ref=" << ref->model_object()->name << ")";
}
// Belt mode replacement for _make_wipe_tower(): plan filament-change purging
@@ -149,7 +145,7 @@ void Print::_plan_belt_purge()
// previous plan so a newly higher toolchange can use its original layers.
for (PrintObject *po : m_objects)
if (po->config().belt_purge_tower_object.value)
po->belt_restore_truncated_layers();
po->belt_undo_purge_plan();
// Must run before ToolOrdering is built: LayerTools merge per-object layer
// print_z values, and the prism only absorbs purge where its (snapped)
@@ -165,9 +161,18 @@ void Print::_plan_belt_purge()
if (m_wipe_tower_data.tool_ordering.empty() || m_wipe_tower_data.tool_ordering.last_extruder() == unsigned(-1))
throw Slic3r::SlicingError("The print is empty. The model is not printable with current print settings.");
if (!m_wipe_tower_data.tool_ordering.has_wipe_tower())
// No toolchanges anywhere, nothing to purge.
return;
// Is there any filament change at all? Not ToolOrdering::has_wipe_tower(): that reads the
// FIRST layer's flag, and on a belt the first layer may be a brim apron band, which carries
// neither object nor support and so never gets the flag even when the print changes filament.
{
bool any_change = false;
unsigned int cur = m_wipe_tower_data.tool_ordering.first_extruder();
for (const auto &lt : m_wipe_tower_data.tool_ordering.layer_tools())
for (const unsigned int e : lt.extruders)
if (e != cur) { any_change = true; cur = e; }
if (! any_change)
return;
}
this->throw_if_canceled();
@@ -191,11 +196,44 @@ void Print::_plan_belt_purge()
// flush object), so truncating afterwards would leave dangling overrides
// pointing into deleted layers.
{
// The tool ordering covers the WHOLE print, and the prism is a printed
// object in it. Left unbounded, the scan below sees the prism's own
// toolchanges on layers above every model object -- the prism runs past
// them by design (ramp/height compensation at the tilted ends) -- so
// last_tc_z lands at the prism's own top and the truncation cancels
// nothing. The tower ends up justifying its own existence.
//
// Nothing above the tallest printed object can require a color change,
// so bound the scan there. On MCTEST5 that is 197 toolchanges spanning
// z=154.00..193.20 with the tallest object topping out at 153.80, i.e.
// 39.4 mm of tower that no swap ever needed.
// Support layers count too: on a belt they can extend above the object's
// own top, and a toolchange there is a real one.
double obj_top_z = -1.;
for (const PrintObject *po : m_objects) {
if (po->config().belt_purge_tower_object.value)
continue;
if (!po->layers().empty())
obj_top_z = std::max(obj_top_z, po->layers().back()->print_z);
if (!po->support_layers().empty())
obj_top_z = std::max(obj_top_z, po->support_layers().back()->print_z);
}
double last_tc_z = -1.;
unsigned int cur_ext = m_wipe_tower_data.tool_ordering.first_extruder();
for (const auto &lt : m_wipe_tower_data.tool_ordering.layer_tools())
for (const auto &lt : m_wipe_tower_data.tool_ordering.layer_tools()) {
// layer_tools() is ordered by print_z ascending.
if (obj_top_z >= 0. && lt.print_z > obj_top_z + EPSILON)
break;
for (const unsigned int e : lt.extruders)
if (e != cur_ext) { last_tc_z = lt.print_z; cur_ext = e; }
}
// Deliberately NOT cancelling the prism outright when no object toolchange
// exists: belt_truncate_layers_above(0.) empties m_layers, and an object
// with zero layers is not something the rest of the pipeline expects. The
// GUI already declines to create a prism unless more than one filament is
// in use, so this case is a stale prism, not a hot path -- leave it whole
// rather than risk a zero-layer object.
if (last_tc_z >= 0.)
for (PrintObject *po : m_objects)
if (po->config().belt_purge_tower_object.value && !po->layers().empty()) {
@@ -204,21 +242,11 @@ void Print::_plan_belt_purge()
}
}
// Diagnostic: the prism only absorbs purge at toolchange layers whose
// print_z coincides with one of its own layers. Compare the prism's layer
// print_z range to the toolchange print_z range and count how many
// toolchange layers actually land on a prism layer. This distinguishes a
// range/grid-alignment failure (no coverage) from a capacity shortfall
// (covered but not enough cross-section).
// The prism only absorbs purge at toolchange layers whose print_z coincides
// with one of its own layers.
PrintObject *prism_po = nullptr;
for (PrintObject *po : m_objects)
if (po->config().belt_purge_tower_object.value && !po->layers().empty()) { prism_po = po; break; }
const PrintObject *diag_prism = prism_po;
if (diag_prism != nullptr)
BOOST_LOG_TRIVIAL(warning) << "[BELT-DEBUG] purge prism layer range print_z=["
<< diag_prism->layers().front()->print_z << ", " << diag_prism->layers().back()->print_z
<< "] nlayers=" << diag_prism->layers().size();
int tc_layers = 0, tc_layers_covered = 0;
float total_leftover = 0.f;
float worst_layer_leftover = 0.f;
@@ -227,33 +255,37 @@ void Print::_plan_belt_purge()
unsigned int current_extruder_id = m_wipe_tower_data.tool_ordering.first_extruder();
for (auto &layer_tools : m_wipe_tower_data.tool_ordering.layer_tools()) {
float layer_leftover = 0.f;
bool layer_has_tc = false;
for (const unsigned int extruder_id : layer_tools.extruders) {
if (extruder_id == current_extruder_id)
continue;
if (!layer_has_tc) {
layer_has_tc = true;
++tc_layers;
if (diag_prism != nullptr && diag_prism->get_layer_at_printz(layer_tools.print_z, EPSILON) != nullptr)
++tc_layers_covered;
}
float volume_to_wipe = use_flush_matrix ?
wipe_volumes[current_extruder_id][extruder_id] * flush_multiplier :
(float) m_config.prime_volume;
float leftover = layer_tools.wiping_extrusions().mark_wiping_extrusions(*this, current_extruder_id, extruder_id,
volume_to_wipe);
BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] purge toolchange print_z=" << layer_tools.print_z
<< " filament " << current_extruder_id << "->" << extruder_id
<< " requested=" << volume_to_wipe
<< " absorbed=" << volume_to_wipe - leftover
<< " leftover=" << leftover;
layer_leftover += leftover;
current_extruder_id = extruder_id;
}
// Do not destructively remove unclaimed fill entities here. psWipeTower
// can rerun without regenerating infill, and a later tool ordering may
// need entities that were unclaimed by the previous plan.
// Plastic saving: drop the prism's fills that no toolchange on this layer
// claimed. At this point the prism's OVERRIDDEN fills are exactly the
// purge; the rest would print as solid infill in the prism's own filament
// for nothing -- which is the whole prism on a layer with no toolchange
// (141 of 692 layers on MCTEST5 before the truncation fix). Perimeters are
// left alone so the bar keeps a continuous wall along the belt.
//
// Non-destructive: the entities are stashed with their positions and put
// back by belt_restore_dropped_fills() at the top of the next plan. An
// earlier version deleted them outright, which broke replanning when a
// later tool ordering needed what this one had not claimed -- that is why
// it was removed rather than kept.
if (prism_po != nullptr) {
const auto &we = layer_tools.wiping_extrusions();
prism_po->belt_drop_unclaimed_fills(
prism_po->get_layer_at_printz(layer_tools.print_z, EPSILON),
[&we, prism_po](const ExtrusionEntity *e) { return we.is_entity_overridden(e, prism_po, 0); });
}
layer_tools.wiping_extrusions().ensure_perimeters_infills_order(*this);
if (layer_leftover > 0.f) {
total_leftover += layer_leftover;
@@ -265,11 +297,6 @@ void Print::_plan_belt_purge()
this->throw_if_canceled();
}
BOOST_LOG_TRIVIAL(warning) << "[BELT-DEBUG] purge coverage: " << tc_layers_covered << "/" << tc_layers
<< " toolchange layers land on a prism layer"
<< (tc_layers > 0 && tc_layers_covered == 0 ? " (RANGE/GRID MISALIGNMENT — prism absorbs nothing)" :
tc_layers_covered < tc_layers ? " (partial coverage)" : " (full coverage)");
if (total_leftover > 1.f) {
this->active_step_add_warning(
PrintStateBase::WarningLevel::CRITICAL,
@@ -278,8 +305,6 @@ void Print::_plan_belt_purge()
"Increase the belt purge tower width, or reduce flushing volumes."),
int(std::ceil(total_leftover)), int(std::ceil(worst_layer_leftover)),
Slic3r::float_to_string_decimal_point(worst_layer_z, 2)));
BOOST_LOG_TRIVIAL(warning) << "[BELT-DEBUG] purge planning leftover total=" << total_leftover
<< " worst_layer=" << worst_layer_leftover << " at print_z=" << worst_layer_z;
}
}
@@ -297,11 +322,10 @@ void PrintObject::belt_shift_layer_grid(double delta)
layer->print_z += delta;
for (SupportLayer *layer : m_support_layers)
layer->print_z += delta;
// The brim's apron bands below the first layer carry their own print_z.
for (BeltBrimBand &band : m_belt_brim_prologue)
band.print_z += delta;
m_slicing_params.belt_floor_z_shift += delta;
BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] belt_shift_layer_grid"
<< " obj=" << this->model_object()->name
<< " delta=" << delta
<< " first_layer.print_z=" << (m_layers.empty() ? 0. : m_layers.front()->print_z);
}
// Belt mode: drop layers strictly above z (used to cancel the purge prism early
@@ -324,12 +348,59 @@ size_t PrintObject::belt_truncate_layers_above(coordf_t z)
m_layers.resize(keep);
if (!m_layers.empty())
m_layers.back()->upper_layer = nullptr;
BOOST_LOG_TRIVIAL(debug) << "[BELT-DEBUG] truncate purge prism above print_z=" << z
<< " kept=" << keep << " removed=" << removed
<< " new_top=" << (m_layers.empty() ? 0. : m_layers.back()->print_z);
return removed;
}
// Plastic saving on the purge prism: keep only the fills a toolchange claimed.
//
// Called per layer from _plan_belt_purge(), after the real-purge marking and
// BEFORE ensure_perimeters_infills_order() -- that pass force-overrides every
// remaining fill on the prism (it is a dedicated flush object), so afterwards
// everything looks claimed and nothing could be distinguished.
size_t PrintObject::belt_drop_unclaimed_fills(Layer *layer, const std::function<bool(const ExtrusionEntity*)> &claimed)
{
if (layer == nullptr)
return 0;
size_t dropped = 0;
for (size_t ri = 0; ri < layer->regions().size(); ++ri) {
LayerRegion *lr = layer->get_region(ri);
auto &ents = lr->fills.entities;
ExtrusionEntitiesPtr keep;
keep.reserve(ents.size());
for (size_t i = 0; i < ents.size(); ++i) {
if (claimed(ents[i])) {
keep.emplace_back(ents[i]);
} else {
// Stash with its original index so the restore is exact.
m_belt_dropped_fills.push_back(BeltDroppedFill{ layer, ri, i, ents[i] });
++dropped;
}
}
ents = std::move(keep);
}
return dropped;
}
void PrintObject::belt_restore_dropped_fills()
{
if (m_belt_dropped_fills.empty())
return;
// Ascending index per (layer, region): inserting in that order lands every
// entity back at its original position, because each insertion shifts only
// the entries after it, which are themselves still to be inserted.
std::stable_sort(m_belt_dropped_fills.begin(), m_belt_dropped_fills.end(),
[](const BeltDroppedFill &a, const BeltDroppedFill &b) {
if (a.layer != b.layer) return a.layer < b.layer;
if (a.region_idx != b.region_idx) return a.region_idx < b.region_idx;
return a.index < b.index;
});
for (const BeltDroppedFill &d : m_belt_dropped_fills) {
auto &ents = d.layer->get_region(d.region_idx)->fills.entities;
ents.insert(ents.begin() + std::min(d.index, ents.size()), d.entity);
}
m_belt_dropped_fills.clear();
}
void PrintObject::belt_restore_truncated_layers()
{
if (m_belt_truncated_layers.empty())
+1 -78
View File
@@ -3,13 +3,6 @@
#include <limits>
#include <boost/log/trivial.hpp>
#ifdef SLIC3R_BELT_DIAGNOSTIC_LOG
#include <iomanip>
#include <sstream>
#include <thread>
#endif
namespace Slic3r {
void BeltSliceStrategy::apply_preslice_transforms(Transform3d &trafo,
@@ -45,79 +38,19 @@ void BeltSliceStrategy::apply_preslice_transforms(Transform3d &trafo,
// each volume within the object) would compute min_z against mesh-local vertex
// coordinates rather than object-space coordinates, so volumes translated along
// the slicer's Z axis would be silently excluded from the bound check.
#ifdef SLIC3R_BELT_DIAGNOSTIC_LOG
// Capture the incoming trafo for diagnostic logging.
// This is the slicer-frame transform AFTER remap + rotation but BEFORE z_shift.
const Transform3d trafo_pre_shift = trafo;
auto log_mat = [](const Matrix3d &m) {
std::ostringstream ss;
ss << std::fixed << std::setprecision(4);
ss << "[[" << m(0,0) << "," << m(0,1) << "," << m(0,2) << "],"
<< "[" << m(1,0) << "," << m(1,1) << "," << m(1,2) << "],"
<< "[" << m(2,0) << "," << m(2,1) << "," << m(2,2) << "]]";
return ss.str();
};
auto log_vec3 = [](const Vec3d &v) {
std::ostringstream ss;
ss << std::fixed << std::setprecision(4);
ss << "(" << v.x() << "," << v.y() << "," << v.z() << ")";
return ss.str();
};
BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] apply_preslice_transforms enter"
<< " has_rotation=" << has_rotation
<< " has_remap=" << has_remap
<< " trafo.linear=" << log_mat(trafo_pre_shift.linear())
<< " trafo.translation=" << log_vec3(trafo_pre_shift.translation())
<< " volumes=" << model_volumes.size();
#endif
double min_z = std::numeric_limits<double>::max();
#ifdef SLIC3R_BELT_DIAGNOSTIC_LOG
int vol_idx = 0;
#endif
for (const ModelVolume *mv : model_volumes) {
#ifdef SLIC3R_BELT_DIAGNOSTIC_LOG
if (!mv->is_model_part()) { ++vol_idx; continue; }
#else
if (!mv->is_model_part()) continue;
#endif
Transform3d vol_trafo = trafo * mv->get_matrix();
const auto &its = mv->mesh().its;
#ifdef SLIC3R_BELT_DIAGNOSTIC_LOG
// Per-volume bbox in mesh-frame and post-trafo slicer-frame.
Vec3d mesh_min(std::numeric_limits<double>::max(), std::numeric_limits<double>::max(), std::numeric_limits<double>::max());
Vec3d mesh_max(std::numeric_limits<double>::lowest(), std::numeric_limits<double>::lowest(), std::numeric_limits<double>::lowest());
Vec3d slicer_min(std::numeric_limits<double>::max(), std::numeric_limits<double>::max(), std::numeric_limits<double>::max());
Vec3d slicer_max(std::numeric_limits<double>::lowest(), std::numeric_limits<double>::lowest(), std::numeric_limits<double>::lowest());
double vol_min_z = std::numeric_limits<double>::max();
#endif
for (const stl_vertex &v : its.vertices) {
Vec3d vm = v.cast<double>();
Vec3d pt = vol_trafo * vm;
min_z = std::min(min_z, pt.z());
#ifdef SLIC3R_BELT_DIAGNOSTIC_LOG
mesh_min = mesh_min.cwiseMin(vm);
mesh_max = mesh_max.cwiseMax(vm);
slicer_min = slicer_min.cwiseMin(pt);
slicer_max = slicer_max.cwiseMax(pt);
vol_min_z = std::min(vol_min_z, pt.z());
#endif
}
#ifdef SLIC3R_BELT_DIAGNOSTIC_LOG
BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] vol[" << vol_idx
<< "] id=" << mv->id().id << " name='" << mv->name << "'"
<< " mesh_bbox_min=" << log_vec3(mesh_min) << " mesh_bbox_max=" << log_vec3(mesh_max)
<< " get_matrix.translation=" << log_vec3(mv->get_matrix().translation())
<< " slicer_bbox_min=" << log_vec3(slicer_min) << " slicer_bbox_max=" << log_vec3(slicer_max)
<< " vol_min_z=" << vol_min_z;
++vol_idx;
#endif
}
const double z_shift_val = (min_z < 0. && min_z != std::numeric_limits<double>::max()) ? -min_z : 0.;
#ifdef SLIC3R_BELT_DIAGNOSTIC_LOG
BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] combined min_z=" << min_z
<< " z_shift_val=" << z_shift_val;
#endif
if (z_shift_val > 0.) {
Transform3d z_shift = Transform3d::Identity();
z_shift.matrix()(2, 3) = z_shift_val;
@@ -126,18 +59,8 @@ void BeltSliceStrategy::apply_preslice_transforms(Transform3d &trafo,
// out_belt_min_z is only meaningful in belt mode; the standalone-remap path
// never reported it.
if (out_belt_min_z && config.belt_printer.value) {
const double new_val = (min_z != std::numeric_limits<double>::max()) ? min_z : 0.;
#ifdef SLIC3R_BELT_DIAGNOSTIC_LOG
BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] write m_belt_min_z tid=" << std::this_thread::get_id()
<< " target=" << out_belt_min_z << " old=" << *out_belt_min_z << " new=" << new_val;
#endif
*out_belt_min_z = new_val;
*out_belt_min_z = (min_z != std::numeric_limits<double>::max()) ? min_z : 0.;
}
#ifdef SLIC3R_BELT_DIAGNOSTIC_LOG
BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] apply_preslice_transforms exit"
<< " final_trafo.linear=" << log_mat(trafo.linear())
<< " final_trafo.translation=" << log_vec3(trafo.translation());
#endif
}
} // namespace Slic3r
-30
View File
@@ -180,31 +180,6 @@ BuildVolume::BuildVolume(const std::vector<Vec2d> &printable_area, const double
BOOST_LOG_TRIVIAL(debug) << "BuildVolume printable_area clasified as: " << this->type_name();
}
void BuildVolume::set_belt_printer(bool enabled, double angle_deg, bool infinite_y)
{
m_is_belt_printer = enabled;
m_belt_angle = angle_deg;
m_belt_infinite_y = infinite_y;
// Restart from the unmodified bbox each call. Without this, toggling
// belt mode off (or switching infinite_y true→false) would leave the
// extents inflated and break collision / object_state checks.
BoundingBoxf bboxf = get_extents(m_bed_shape);
m_bboxf = BoundingBoxf3{ to_3d(bboxf.min, 0.), to_3d(bboxf.max, m_max_print_height) };
if (enabled) {
if (infinite_y) {
// Extend the Y bound to a very large value for infinite belt.
m_bboxf.max.y() = 100000.;
}
// Belt printer: the Z extent already equals printable_height (set above), which
// is the usable vertical clearance above the belt. The gantry's axis range is
// sized to reach height/cos(tilt), so no diagonal scaling is applied here — this
// keeps the live "outside build volume" highlight in agreement with Print::validate().
(void) angle_deg;
}
}
#if 0
// Tests intersections of projected triangles, not just their vertices against a bounding box.
// This test also correctly evaluates collision of a non-convex object with the bounding box.
@@ -413,11 +388,6 @@ BuildVolume::ObjectState BuildVolume::object_state(const indexed_triangle_set& i
build_volume.max.z() = std::numeric_limits<double>::max();
if (ignore_bottom)
build_volume.min.z() = -std::numeric_limits<double>::max();
// Belt printer: extend Y bounds for infinite Y.
if (m_is_belt_printer && m_belt_infinite_y) {
build_volume.min.y() = -std::numeric_limits<double>::max();
build_volume.max.y() = std::numeric_limits<double>::max();
}
BoundingBox3Base<Vec3f> build_volumef(build_volume.min.cast<float>(), build_volume.max.cast<float>());
// The following test correctly interprets intersection of a non-convex object with a rectangular build volume.
//return rectangle_test(its, trafo, to_2d(build_volume.min), to_2d(build_volume.max), build_volume.max.z());
-8
View File
@@ -57,10 +57,6 @@ public:
// Initialize from PrintConfig::printable_area and PrintConfig::printable_height
BuildVolume(const std::vector<Vec2d> &printable_area, const double printable_height, const std::vector<std::vector<Vec2d>> &extruder_areas, const std::vector<double>& extruder_printable_heights);
// Belt printer configuration.
void set_belt_printer(bool enabled, double angle_deg, bool infinite_y);
bool is_belt_printer() const { return m_is_belt_printer; }
// Source data, unscaled coordinates.
const std::vector<Vec2d>& printable_area() const { return m_bed_shape; }
double printable_height() const { return m_max_print_height; }
@@ -143,10 +139,6 @@ private:
// Source definition of the print volume height (PrintConfig::printable_height)
double m_max_print_height { 0.f };
std::vector<double> m_extruder_printable_height;
// Belt printer state.
bool m_is_belt_printer { false };
double m_belt_angle { 0. };
bool m_belt_infinite_y { false };
// Derived values.
BuildVolume_Type m_type { BuildVolume_Type::Invalid };
+4 -2
View File
@@ -84,8 +84,6 @@ set(lisbslic3r_sources
BeltBrim.hpp
BeltGCode.cpp
BeltGCode.hpp
BeltGCodeWriter.cpp
BeltGCodeWriter.hpp
BeltPurge.cpp
BeltSliceStrategy.cpp
BeltSliceStrategy.hpp
@@ -245,6 +243,10 @@ set(lisbslic3r_sources
GCode/BeltBackTransform.hpp
GCode/MachineFrameTransform.cpp
GCode/MachineFrameTransform.hpp
GCode/BeltKinematics.cpp
GCode/BeltKinematics.hpp
GCode/MachineKinematics.cpp
GCode/MachineKinematics.hpp
GCode/ConflictChecker.cpp
GCode/ConflictChecker.hpp
GCode/CoolingBuffer.cpp
+1 -1
View File
@@ -56,7 +56,7 @@ MachineZAffine compute_machine_z_affine(const PrintConfig &config)
out.constant = trans;
if (config.gcode_back_transform.value && config.belt_printer.value) {
// BeltGCodeWriter applies F^-1 before R when back-transform is on.
// BeltKinematics applies F^-1 before R when back-transform is on.
// So machine_Z(slicing) = r_row · (F^-1 · slicing) + trans
// = (r_row^T · F^-1) · slicing + trans
// We need to compose r_row with F^-1 from the LEFT (treating r_row as
+31 -12
View File
@@ -978,10 +978,10 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
void _stop_object_xml_parser(const std::string& msg = std::string())
{
assert(! obj_parse_error);
assert(obj_parse_error_message.empty());
assert(object_xml_parser != nullptr);
obj_parse_error = true;
obj_parse_error_message = msg;
if (! msg.empty() || obj_parse_error_message.empty()) // a handler may have set the message already
obj_parse_error_message = msg;
XML_StopParser(object_xml_parser, false);
}
@@ -3815,11 +3815,18 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
{
// appends the vertex coordinates
// missing values are set equal to ZERO
if (m_curr_object)
m_curr_object->geometry.vertices.emplace_back(
m_unit_factor * bbs_get_attribute_value_float(attributes, num_attributes, X_ATTR),
m_unit_factor * bbs_get_attribute_value_float(attributes, num_attributes, Y_ATTR),
m_unit_factor * bbs_get_attribute_value_float(attributes, num_attributes, Z_ATTR));
if (m_curr_object) {
const Vec3f v(m_unit_factor * bbs_get_attribute_value_float(attributes, num_attributes, X_ATTR),
m_unit_factor * bbs_get_attribute_value_float(attributes, num_attributes, Y_ATTR),
m_unit_factor * bbs_get_attribute_value_float(attributes, num_attributes, Z_ATTR));
// A non-finite coordinate ("nan", "inf") used to be accepted and crashed
// qhull in ModelVolume's convex hull while the file was still loading. Refuse the file.
if (! v.allFinite()) {
_stop_xml_parser("Invalid vertex coordinate: not a finite number");
return true; // the parser is stopped; returning false would overwrite the message
}
m_curr_object->geometry.vertices.emplace_back(v);
}
return true;
}
@@ -5109,6 +5116,11 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
}
}
for (const Vec3f &v : sub_object->geometry.vertices)
if (! v.allFinite()) { // Qhull cannot take a NaN vertex
add_error("invalid (non-finite) vertex in object " + std::to_string(sub_object->id));
return false;
}
its.vertices.assign(sub_object->geometry.vertices.begin(), sub_object->geometry.vertices.end());
// BBS
@@ -5600,11 +5612,18 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
{
// appends the vertex coordinates
// missing values are set equal to ZERO
if (current_object)
current_object->geometry.vertices.emplace_back(
object_unit_factor * bbs_get_attribute_value_float(attributes, num_attributes, X_ATTR),
object_unit_factor * bbs_get_attribute_value_float(attributes, num_attributes, Y_ATTR),
object_unit_factor * bbs_get_attribute_value_float(attributes, num_attributes, Z_ATTR));
if (current_object) {
const Vec3f v(object_unit_factor * bbs_get_attribute_value_float(attributes, num_attributes, X_ATTR),
object_unit_factor * bbs_get_attribute_value_float(attributes, num_attributes, Y_ATTR),
object_unit_factor * bbs_get_attribute_value_float(attributes, num_attributes, Z_ATTR));
// See _BBS_3MF_Importer::_handle_start_vertex: a non-finite coordinate
// crashed qhull while the file loaded. The dispatcher stops this parser on `false`.
if (! v.allFinite()) {
obj_parse_error_message = "Invalid vertex coordinate: not a finite number";
return false;
}
current_object->geometry.vertices.emplace_back(v);
}
return true;
}
+128 -70
View File
@@ -6,6 +6,7 @@
#include "libslic3r.h"
#include "I18N.hpp"
#include "GCode.hpp"
#include <cstdio>
#include "Exception.hpp"
#include "ExtrusionEntity.hpp"
#include "EdgeGrid.hpp"
@@ -2961,10 +2962,9 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
m_fan_mover.release();
m_ordering_cache.clear();
m_writer->set_is_bbl_machine(is_bbl_printers);
// Belt printer: initialize belt-specific writer via virtual hook.
this->init_belt_writer(print, is_bbl_printers);
this->init_belt_writer(print);
m_writer->set_is_bbl_machine(is_bbl_printers);
// Standalone axis remap (works with or without belt mode).
// Sync the writer's remap state to the current export UNCONDITIONALLY — even at
@@ -2986,8 +2986,11 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
// circuit to the legacy Layer::id() == 0 path so g-code stays bit-
// identical to the pre-feature behavior.
m_first_layer_plane = std::make_unique<FirstLayerPlane>(print.config());
if (auto *belt_writer = dynamic_cast<BeltGCodeWriter*>(m_writer.get())) {
belt_writer->set_first_layer_plane(
// Belt writers only: the plane also switches travel-speed selection to be
// per-point (see GCodeWriter::uses_pointwise_travel_speed()), which must not
// change for non-belt printers.
if (print.config().belt_printer.value) {
m_writer->set_first_layer_plane(
m_first_layer_plane.get(),
print.config().initial_layer_print_height.value);
}
@@ -3090,8 +3093,6 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
file.write_format("; HEADER_BLOCK_START\n");
// Write information on the generator.
file.write_format("; generated by %s on %s\n", Slic3r::header_slic3r_generated().c_str(), Slic3r::Utils::local_timestamp().c_str());
// Belt printer: embed angle and transform configs in header via virtual hook.
this->write_belt_header(file, print);
if (is_bbl_printers)
file.write_format(";%s\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Estimated_Printing_Time_Placeholder).c_str());
//BBS: total layer number
@@ -3190,6 +3191,10 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
}
}
// Belt printer: the tilt and transform settings the G-code viewer reads back. They
// are comments outside the config block, so they go after the thumbnails that a
// BTT TFT firmware needs first, and are written whether or not that header block is.
this->write_belt_header(file, print);
// Write some terse information on the slicing parameters.
const PrintObject *first_object = print.objects().front();
@@ -3888,12 +3893,12 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
// ORCA-Belt: the PA line test draws directly on the build surface in
// logical bed coordinates — on a belt printer that surface is the
// belt plane, not the slicing plane.
BeltGCodeWriter* belt_writer = dynamic_cast<BeltGCodeWriter*>(m_writer.get());
if (belt_writer != nullptr)
belt_writer->set_world_coordinates(true);
const bool belt_world_coords = print.config().belt_printer.value;
if (belt_world_coords)
install_belt_kinematics(*m_writer, print.config(), /*world_coordinates=*/true);
gcode += pa_test.generate_test(params.start, params.step, std::llround(std::ceil((params.end - params.start) / params.step)) + 1);
if (belt_writer != nullptr)
belt_writer->set_world_coordinates(false);
if (belt_world_coords)
install_belt_kinematics(*m_writer, print.config(), /*world_coordinates=*/false);
file.write(gcode);
} else {
@@ -5231,6 +5236,10 @@ std::string GCode::generate_object_brim(const Print &print, const PrintObject &o
// geometry is already in plate coordinates.
m_config.apply(print.default_region_config());
m_config.apply(object.config(), true);
// m_layer is not switched to this object until after brim emission, so name
// the belt-floor owner explicitly or the classification borrows whichever
// object was visited last.
BeltFloorObjectGuard floor_owner{ m_belt_floor_object, &object };
const Point &offset = object.instances()[instance_id].shift;
this->set_origin(unscale(offset));
this->on_set_origin(&object, offset);
@@ -5319,7 +5328,9 @@ LayerResult GCode::process_belt_brim_layer(
break;
}
m_cur_layer_idx = m_belt_brim_layer_idx ++;
// Apron bands precede object layer 0 and have no layer id of their own; they take the
// filament and nozzle assignment in effect at the first object layer.
m_cur_layer_idx = 0;
// Publish the band's Z for _extrude()'s first-layer-plane probe, and make sure
// it cannot leak past this layer even if an extrusion throws.
@@ -5342,8 +5353,7 @@ LayerResult GCode::process_belt_brim_layer(
// skip these layers entirely.
{
char buf[64];
sprintf(buf, ";%s%g\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Layer_Change).c_str(), print_z);
gcode += buf;
gcode += ";" + GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Layer_Change) + "\n";
sprintf(buf, ";Z:%g\n", print_z);
gcode += buf;
const float band_height = float(height);
@@ -5374,7 +5384,12 @@ LayerResult GCode::process_belt_brim_layer(
print.config().layer_change_gcode.value, m_writer->filament()->id(), &config) + "\n";
}
gcode += this->emit_belt_brim_bands(print, layers, single_object_instance_idx);
// Objects sharing this apron Z may use different brim filaments; print each in its own tool.
for (const unsigned int brim_extruder : layer_tools.extruders) {
if (m_writer->filament() == nullptr || m_writer->filament()->id() != brim_extruder)
gcode += this->set_extruder(brim_extruder, print_z);
gcode += this->emit_belt_brim_bands(print, layers, single_object_instance_idx, brim_extruder);
}
result.gcode = std::move(gcode);
return result;
@@ -5388,7 +5403,8 @@ LayerResult GCode::process_belt_brim_layer(
// object layer, takes the ordinary path, and the band would be silently dropped.
std::string GCode::emit_belt_brim_bands(const Print &print,
const std::vector<LayerToPrint> &layers,
const size_t single_object_instance_idx)
const size_t single_object_instance_idx,
const unsigned int extruder_id)
{
std::string gcode;
for (const LayerToPrint &ltp : layers) {
@@ -5396,9 +5412,15 @@ std::string GCode::emit_belt_brim_bands(const Print &print,
if (band == nullptr || band->fills.empty() || ltp.original_object == nullptr)
continue;
const PrintObject &object = *ltp.original_object;
// belt_brim_filament() is 1-based.
if (! object.has_belt_brim() || static_cast<unsigned int>(object.belt_brim_filament() - 1) != extruder_id)
continue;
// Speeds, flow and retraction all read m_config.
m_config.apply(print.default_region_config());
m_config.apply(object.config(), true);
// Apron bands have no Layer at all (m_layer is null here), so the belt
// floor owner has to be named the same way the object brim names it.
BeltFloorObjectGuard floor_owner{ m_belt_floor_object, &object };
const size_t i_begin = single_object_instance_idx == size_t(-1) ? 0 : single_object_instance_idx;
const size_t i_end = single_object_instance_idx == size_t(-1) ? object.instances().size()
: single_object_instance_idx + 1;
@@ -6473,6 +6495,7 @@ LayerResult GCode::process_layer(
std::vector<GCode::ObjectByExtruder> &objects_by_extruder = objects_by_extruder_it->second;
std::vector<InstanceToPrint> &instances = filament_plan.first;
std::vector<IslandOrderNode> nodes;
std::vector<std::pair<size_t, bool>> layout; // Per instance, see IslandOrderCacheEntry
std::vector<size_t> node_instances;
auto quantize_to_mm = [](const Point &pt) -> Point {
const coord_t grid = coord_t(scale_(1.));
@@ -6497,6 +6520,7 @@ LayerResult GCode::process_layer(
const size_t instance_idx = instances.size();
instances.emplace_back(object_by_extruder, layer_id, *print_object, instance_id,
print_object->instances()[instance_id].model_instance->get_labeled_id());
layout.emplace_back(islands.size(), ! islands.empty() && ! islands.back().by_region.empty());
const Point &shift = print_object->instances()[instance_id].shift;
const size_t first_node = nodes.size();
if (islands_chainable)
@@ -6516,8 +6540,9 @@ LayerResult GCode::process_layer(
// Reuse the cached tour while this filament's island layout is unchanged.
auto &cache_entry = m_ordering_cache[filament_id];
if (!(cache_entry.first == nodes)) {
cache_entry.first = nodes;
if (! (cache_entry.nodes == nodes && cache_entry.layout == layout)) {
cache_entry.nodes = nodes;
cache_entry.layout = layout;
Points node_points;
node_points.reserve(nodes.size());
for (const IslandOrderNode &node : nodes)
@@ -6550,12 +6575,12 @@ LayerResult GCode::process_layer(
// A visit without explicit islands already prints everything.
continue;
std::vector<ObjectByExtruder::Island> &islands = instances[i].object_by_extruder.islands;
if (!islands.back().by_region.empty())
if (! islands.empty() && ! islands.back().by_region.empty())
last_visit.islands.emplace_back(islands.size() - 1);
}
cache_entry.second = std::move(visits);
cache_entry.visits = std::move(visits);
}
filament_plan.second = cache_entry.second;
filament_plan.second = cache_entry.visits;
}
}
@@ -6623,39 +6648,11 @@ LayerResult GCode::process_layer(
std::set<std::pair<size_t, size_t>> belt_brim_emitted;
// Emit every ORDINARY-layer apron band (belt_brim_prologue band coinciding with an
// object/support layer) whose brim filament is this pass's extruder. Mirrors
// emit_belt_brim_bands() per band, but filtered to one brim filament so each band
// object/support layer) whose brim filament is this pass's extruder, so each band
// prints in the correct tool's pass (Finding B). extruder_id is 0-based (the
// reindexed tool domain); belt_brim_filament() is 1-based, so subtract one.
// reindexed tool domain).
auto emit_belt_brim_for_extruder = [this, &print, &layers, single_object_instance_idx](unsigned int extruder_id) -> std::string {
std::string gc;
for (const LayerToPrint &ltp : layers) {
const BeltBrimBand *band = ltp.belt_brim_band;
if (band == nullptr || band->fills.empty() || ltp.original_object == nullptr)
continue;
const PrintObject &object = *ltp.original_object;
if (! object.has_belt_brim() || (unsigned int)(object.belt_brim_filament() - 1) != extruder_id)
continue;
// Speeds, flow and retraction all read m_config.
m_config.apply(print.default_region_config());
m_config.apply(object.config(), true);
const size_t i_begin = single_object_instance_idx == size_t(-1) ? 0 : single_object_instance_idx;
const size_t i_end = single_object_instance_idx == size_t(-1) ? object.instances().size()
: single_object_instance_idx + 1;
for (size_t i = i_begin; i < i_end && i < object.instances().size(); ++ i) {
// Band geometry is object-local, like the object's own extrusions.
const Point &offset = object.instances()[i].shift;
this->set_origin(unscale(offset));
this->on_set_origin(&object, offset);
m_avoid_crossing_perimeters.use_external_mp();
for (const ExtrusionEntity *ee : band->fills.entities)
if (ee != nullptr)
gc += this->extrude_entity(*ee, "brim", NOZZLE_CONFIG(support_speed));
m_avoid_crossing_perimeters.use_external_mp(false);
m_avoid_crossing_perimeters.disable_once();
}
}
return gc;
return this->emit_belt_brim_bands(print, layers, single_object_instance_idx, extruder_id);
};
for (unsigned int extruder_id : layer_tools.extruders)
@@ -6906,7 +6903,13 @@ LayerResult GCode::process_layer(
// in this instance's frame after set_origin() above). Empty islands are skipped;
// the trailing catch-all island has no centroid to chain by and always goes last.
std::vector<ObjectByExtruder::Island> &islands = instance_to_print.object_by_extruder.islands;
std::vector<size_t> island_order = visit.islands;
std::vector<size_t> island_order;
island_order.reserve(visit.islands.size());
for (size_t idx : visit.islands) // Never index past the islands (see IslandOrderCacheEntry)
if (idx < islands.size())
island_order.emplace_back(idx);
else
BOOST_LOG_TRIVIAL(error) << "island tour refers to island " << idx << " of " << islands.size() << ", skipped";
if (island_order.empty()) {
island_order.reserve(islands.size());
if (layer_to_print.object_layer != nullptr && islands.size() == layer_to_print.object_layer->lslices.size() + 1) {
@@ -7671,8 +7674,13 @@ std::string GCode::extrude_loop(const ExtrusionLoop& loop_ref,
loop.split_at(last_pos, false);
const auto seam_scarf_type = m_config.seam_slope_type.value;
// Belt printers never get a scarf joint. The scarf starts one layer height
// below the layer, which on a tilted belt is a step backwards along the belt
// axis into the previous layer's wall at the seam (0.28 mm at 45 degrees per
// 0.2 mm layer); with an aligned seam that ram repeats at the same spot on
// every layer and knocks the part loose.
bool enable_seam_slope = ((seam_scarf_type == SeamScarfType::External && !is_hole) || seam_scarf_type == SeamScarfType::All) &&
!m_config.spiral_mode &&
!m_config.spiral_mode && !m_config.belt_printer.value &&
(loop.role() == erExternalPerimeter || (loop.role() == erPerimeter && m_config.seam_slope_inner_walls)) &&
layer_id() > 0;
const auto nozzle_diameter = EXTRUDER_CONFIG(nozzle_diameter);
@@ -8398,20 +8406,6 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description,
auto _mm3_per_mm = path.mm3_per_mm * this->config().print_flow_ratio;
_mm3_per_mm *= filament_flow_ratio;
// Belt printer: compensate for any volume change introduced by the mesh
// forward transform. path.mm3_per_mm is derived from slicer-frame layer
// height × line width, but a slicer-frame slab of volume V maps under the
// back-transform to a machine-frame region of volume V / |det(T)|. The
// mesh transform is now rotation ∘ pre-remap, both orthogonal, so |det(T)|
// is always 1 and this is currently a no-op; it is retained as a guard in
// case a non-orthogonal mesh transform is ever reintroduced. (Machine-frame
// shear/scale acts on the g-code in BeltGCodeWriter, not here.)
if (m_config.belt_printer.value) {
double det = std::abs(BeltTransformPipeline::build_forward_transform(m_config).linear().determinant());
if (det > EPSILON)
_mm3_per_mm /= det;
}
if (path.role() == erTopSolidInfill) {
_mm3_per_mm *= m_config.top_solid_infill_flow_ratio;
} else if (path.role() == erBottomSurface) {
@@ -8515,6 +8509,20 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description,
// the speed fade tracks perpendicular distance from the plane on
// belt printers; otherwise this falls back to the slicing layer id.
const int _layer = this->effective_layer_index_for_point(path_point_mm);
// Belt printers: a tilted layer runs from the belt to the top of the part, so the
// "first layers" the fan stays off for are a band along the belt. Mark where the
// extrusion enters and leaves it, per segment, for the cooling buffer.
const bool belt_band_tags = m_enable_cooling_markers && m_config.belt_printer.value;
const int belt_band_layers = belt_band_tags ? m_config.close_fan_the_first_x_layers.get_at(m_writer->filament()->id()) : 0;
auto tag_belt_band = [this, &gcode, belt_band_tags, belt_band_layers, z = path_point_mm.z()](coord_t x, coord_t y) {
if (! belt_band_tags)
return;
const bool in_band = this->effective_layer_index_for_point(Vec3d(unscale<double>(x), unscale<double>(y), z)) < belt_band_layers;
if (in_band != m_belt_in_band) {
gcode += in_band ? ";_BELT_BAND_START\n" : ";_BELT_BAND_END\n";
m_belt_in_band = in_band;
}
};
if (path_on_first_layer || object_layer_over_raft()) {
//BBS: for solid infill of first layer, speed can be higher as long as
//wall lines have be attached
@@ -8999,6 +9007,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description,
tempDescription += Slic3r::format(" | Old Flow Value: %0.5f Length: %0.5f",oldE, line_length);
}
}
tag_belt_band((line.a.x() + line.b.x()) / 2, (line.a.y() + line.b.y()) / 2);
if (path.z_contoured) {
// ZAA: Z anti-aliased extrusion with variable Z per point
Vec2d dest2d = this->point_to_gcode(line.b.to_point());
@@ -9129,6 +9138,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, std::string description,
const ProcessedPoint &processed_point = new_points[i];
const ProcessedPoint &pre_processed_point = new_points[i-1];
Vec3d p = this->point_to_gcode_quantized(processed_point.p);
tag_belt_band((pre_processed_point.p.x() + processed_point.p.x()) / 2, (pre_processed_point.p.y() + processed_point.p.y()) / 2);
if (m_enable_cooling_markers) {
if (enable_overhang_bridge_fan) {
cur_fan_enabled = check_overhang_fan(processed_point.overlap, path.role());
@@ -9423,6 +9433,7 @@ std::string GCode::travel_to(const Point& point, ExtrusionRole role, std::string
// multi-hop travel path inside the configuration space
if (m_config.reduce_crossing_wall
&& !m_avoid_crossing_perimeters.disabled_once()
&& m_layer != nullptr // A brim apron layer has no Layer to avoid crossing
&& m_writer->is_current_position_clear())
//BBS: don't generate detour travel paths when current position is unclea
{
@@ -9447,7 +9458,8 @@ std::string GCode::travel_to(const Point& point, ExtrusionRole role, std::string
// When "Wipe while retracting" is enabled, then extruder moves to another position, and travel from this position can cross perimeters.
// Because of it, it is necessary to call avoid crossing perimeters again with new starting point after calling retraction()
// FIXME Lukas H.: Try to predict if this second calling of avoid crossing perimeters will be needed or not. It could save computations.
if (last_post_before_retract != this->last_pos() && m_config.reduce_crossing_wall) {
if (last_post_before_retract != this->last_pos() && m_config.reduce_crossing_wall
&& m_layer != nullptr) { // A brim apron layer has no Layer to avoid crossing
// If in the previous call of m_avoid_crossing_perimeters.travel_to was use_external_mp_once set to true restore this value for next call.
if (used_external_mp_once)
m_avoid_crossing_perimeters.use_external_mp_once();
@@ -10288,6 +10300,10 @@ std::string GCode::set_object_info(Print *print) {
for (PrintInstance& inst : object->instances()) {
inst.unique_id = unique_id++;
inst.id = inst_id++;
// Outlines are in plate coordinates. On a belt printer that is the frame after
// the slicing rotation has been undone and before the G-code axis remap and
// machine-frame shear: where the object stands on the belt, which is what an
// object picker shows. Klipper cancels by name, so nothing depends on more.
auto bbox = inst.get_bounding_box();
auto center = print->translate_to_print_space(Vec2d(bbox.center().x(), bbox.center().y()));
auto inst_name = get_instance_name(object, inst);
@@ -10310,6 +10326,48 @@ std::string GCode::set_object_info(Print *print) {
return gcode.str();
}
bool GCode::belt_height_above_floor(const Vec3d &point_slicing_mm, double &height_mm) const
{
// The owning object, which is what carries the belt description. During
// object-brim and coincident-apron emission m_layer still points at whichever
// object was visited last (or at nothing at all), so those paths publish the
// owner explicitly -- otherwise a brim's speed would depend on plate order.
const PrintObject *object = m_belt_floor_object != nullptr ? m_belt_floor_object
: (m_layer != nullptr ? m_layer->object() : nullptr);
if (object == nullptr)
return false;
// Respect an explicit first-layer-plane choice: only Auto and BeltAffine mean
// "use the belt". A user who selected XY, YZ or XZ has asked for the
// FirstLayerPlane evaluator and must keep it.
const FirstLayerPlaneMode mode = m_config.first_layer_plane.value;
if (mode != FirstLayerPlaneMode::Auto && mode != FirstLayerPlaneMode::BeltAffine)
return false;
// Likewise for a dialled-in plane offset. It is expressed as a machine-Z
// shift that FirstLayerPlane converts into a perpendicular distance in the
// slicing frame; this evaluator measures along slicing Z instead, so there is
// no faithful translation of it here. Honour the user's setting by deferring
// to the evaluator that implements it rather than silently dropping it.
if (std::abs(m_config.first_layer_plane_offset.value) > EPSILON)
return false;
const SlicingParameters &sp = object->slicing_parameters();
// Deliberately NOT BeltFloorContext: its init() folds in
// belt_support_floor_offset, a support-generator diagnostic. Letting that
// option move the model's first-layer speed band would be a surprising
// coupling -- a negative value would switch the slowdown off entirely.
// The belt surface itself is just shear * u + z_shift.
if (std::abs(sp.belt_floor_shear_factor) < EPSILON)
return false;
const double u = sp.belt_floor_from_axis == 0 ? point_slicing_mm.x() : point_slicing_mm.y();
const double floor_z = sp.belt_floor_shear_factor * u + sp.belt_floor_z_shift;
// Measured along the slicing Z, not perpendicular to the belt: layers are
// horizontal slabs in the sliced frame, so the slab holding the material that
// rests on the belt at this point is the one within one layer height of it.
// A perpendicular measure would shrink the band by 1/cos(tilt).
height_mm = point_slicing_mm.z() - floor_z;
return true;
}
// convert a model-space scaled point into G-code coordinates
Vec2d GCode::point_to_gcode(const Point &point) const
{
+81 -13
View File
@@ -2,9 +2,10 @@
#define slic3r_GCode_hpp_
#include "libslic3r.h"
#include <limits>
#include "ExPolygon.hpp"
#include "GCodeWriter.hpp"
#include "BeltGCodeWriter.hpp"
#include "GCode/BeltKinematics.hpp"
#include "FirstLayerPlane.hpp"
#include "Layer.hpp"
#include "Point.hpp"
@@ -228,7 +229,7 @@ public:
void do_export(Print* print, const char* path, GCodeProcessorResult* result = nullptr, ThumbnailsGeneratorCallback thumbnail_cb = nullptr);
void export_layer_filaments(GCodeProcessorResult* result);
//BBS: set offset for gcode writer
void set_gcode_offset(double x, double y) { m_writer->set_xy_offset(x, y); m_processor.set_xy_offset(x, y);}
void set_gcode_offset(double x, double y) { m_gcode_offset = Vec2d(x, y); m_writer->set_xy_offset(x, y); m_processor.set_xy_offset(x, y);}
// Exported for the helper classes (OozePrevention, Wipe) and for the Perl binding for unit tests.
const Vec2d& origin() const { return m_origin; }
@@ -376,10 +377,14 @@ protected:
// Virtual hooks for belt printer subclass (BeltGCode).
// No-ops in base GCode; overridden in BeltGCode.
virtual void init_belt_writer(Print &print, bool is_bbl_printers) {}
virtual void init_belt_writer(Print &print) {}
virtual void write_belt_header(GCodeOutputStream &file, const Print &print) {}
virtual void on_set_origin(const PrintObject *obj, const Point &inst_shift) {}
virtual bool should_disable_arc_fitting() const { return false; }
// Arc fitting is suppressed whenever the writer's machine mapping cannot
// represent a G2/G3 arc. Belt printers get this through BeltKinematics
// rather than through an override of their own.
virtual bool should_disable_arc_fitting() const
{ return ! m_writer->kinematics().supports_arc_moves(); }
void _do_export(Print &print, GCodeOutputStream &file, ThumbnailsGeneratorCallback thumbnail_cb);
@@ -418,13 +423,14 @@ protected:
const bool last_layer,
const size_t single_object_instance_idx);
// Emit the apron bands carried by these layers. Called from both the brim-only
// branch and the ordinary path, since a band's print_z can coincide with another
// object's layer on a multi-object belt.
// Emit the apron bands carried by these layers whose brim filament is extruder_id
// (0-based). Called from both the brim-only branch and the ordinary path, since a
// band's print_z can coincide with another object's layer on a multi-object belt.
std::string emit_belt_brim_bands(
const Print &print,
const std::vector<LayerToPrint> &layers,
const size_t single_object_instance_idx);
const size_t single_object_instance_idx,
const unsigned int extruder_id);
LayerResult process_layer(
const Print &print,
@@ -627,9 +633,21 @@ protected:
};
// Cache the per-filament island tour to avoid recomputing while the layer's island layout is
// unchanged. Key: filament_id. Value: {nodes the tour was computed from, resulting visits}.
std::map<unsigned int, std::pair<std::vector<IslandOrderNode>, std::vector<InstanceVisit>>>
m_ordering_cache;
// unchanged. Key: filament_id. Value: the nodes the tour was computed from, the per-instance
// island layout (count and whether the trailing catch-all island has anything to print), and
// the resulting visits.
// The layout is part of the key. Nodes only cover the chainable islands, so two
// layers with the same centroids but a different number of islands (thin walls, negative
// volumes come and go) matched the cache and the visit's catch-all index -- islands.size() - 1
// of the OLD layer -- ran past the new layer's islands (found by fuzzing: segfault in
// extrude_perimeters on multi-part objects).
struct IslandOrderCacheEntry
{
std::vector<IslandOrderNode> nodes;
std::vector<std::pair<size_t, bool>> layout;
std::vector<InstanceVisit> visits;
};
std::map<unsigned int, IslandOrderCacheEntry> m_ordering_cache;
ExtrusionQualityEstimator m_extrusion_quality_estimator;
@@ -767,6 +785,8 @@ protected:
// printers without a Z-axis shear; in that case all per-path plane
// checks short-circuit to the legacy Layer::id() == 0 path.
std::unique_ptr<FirstLayerPlane> m_first_layer_plane;
// Plate origin, kept so a writer replaced during export can be given it again.
Vec2d m_gcode_offset{ Vec2d::Zero() };
std::unique_ptr<PressureEqualizer> m_pressure_equalizer;
@@ -824,8 +844,20 @@ protected:
// _extrude() needs for the first-layer-plane probe is published here instead.
// Scoped by BeltBrimZGuard in process_belt_brim_layer(), never left set.
std::optional<coordf_t> m_belt_brim_z;
// Counter standing in for Layer::id() on apron layers, which precede layer 0.
size_t m_belt_brim_layer_idx{0};
// Belt brim only. Brim and coincident apron bands are emitted before m_layer
// is switched to their object, so belt_height_above_floor() would otherwise
// read the previously visited object's belt description -- making a brim's
// classification depend on plate visiting order. Those paths publish the
// owner here for the duration of the emission. Never left set.
const PrintObject *m_belt_floor_object{nullptr};
struct BeltFloorObjectGuard {
const PrintObject *&slot;
BeltFloorObjectGuard(const PrintObject *&s, const PrintObject *o) : slot(s) { slot = o; }
~BeltFloorObjectGuard() { slot = nullptr; }
};
// The last extrusion segment was inside the belt's first-layer fan band (see _extrude()).
bool m_belt_in_band{false};
std::set<unsigned int> m_initial_layer_extruders;
std::vector<std::vector<unsigned int>> m_sorted_layer_filaments;
@@ -849,6 +881,12 @@ protected:
// otherwise we delegate to the legacy per-layer test. This is the
// entry point used by per-path call sites in _extrude.
bool on_first_layer(const Vec3d &point_slicing_mm) const {
// Belt printers: measure height above the belt surface itself, in the
// slicing frame. See belt_height_above_floor() for why this does not go
// through FirstLayerPlane.
double h;
if (this->belt_height_above_floor(point_slicing_mm, h))
return h <= m_config.initial_layer_print_height.value + EPSILON;
if (m_first_layer_plane && m_first_layer_plane->is_active())
return m_first_layer_plane->is_first_layer(
point_slicing_mm, m_config.initial_layer_print_height.value);
@@ -859,10 +897,40 @@ protected:
// perpendicular distance to the plane in band_thickness_mm units;
// otherwise it returns the legacy slicing layer index.
int effective_layer_index_for_point(const Vec3d &point_slicing_mm) const {
double h;
if (this->belt_height_above_floor(point_slicing_mm, h)) {
const double lh = this->first_layer_band_mm();
return h <= 0. ? 0 : int(std::floor(h / lh));
}
if (m_first_layer_plane && m_first_layer_plane->is_active())
return m_first_layer_plane->effective_layer_index(point_slicing_mm);
return on_first_layer() ? 0 : layer_id();
}
// Band thickness for the *effective layer index* only. FirstLayerPlane keeps
// two separate thresholds and so must this path: is_first_layer() tests
// against initial_layer_print_height, while effective_layer_index() 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.
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;
return band > 0. ? band : 0.2;
}
// Height of a slicing-frame point above the belt surface, or false when this
// is not a belt print.
//
// The belt surface is known exactly in the slicing frame from the slicing
// parameters (belt_floor_shear_factor / _from_axis / _z_shift) -- the same
// description the support generator uses. FirstLayerPlane instead derives its
// plane by composing gcode_remap_* with the g-code back-transform, so its
// answer changes with the machine's *output* axis convention: on a printer
// with a non-identity remap it reported ~86mm of clearance for geometry
// sitting directly on the belt, and no extrusion was ever classified as
// first-layer. Measuring against the belt itself is independent of every
// remap and back-transform.
bool belt_height_above_floor(const Vec3d &point_slicing_mm, double &height_mm) const;
int layer_id() const {
if (m_layer == nullptr)
return -1;
+1 -1
View File
@@ -13,7 +13,7 @@ namespace Slic3r {
// machine's real coordinate space.
//
// Initialized once from PrintConfig, then applied per-point in
// GCodeWriter::to_machine_coords() before axis remapping.
// BeltKinematics::to_machine() before axis remapping.
//
// Active when gcode_back_transform is true AND at least one of:
// - a shear axis has global mode enabled, or
+40
View File
@@ -0,0 +1,40 @@
#include "BeltKinematics.hpp"
#include "../BeltTransform.hpp"
#include "../PrintConfig.hpp"
#include "../GCodeWriter.hpp"
namespace Slic3r {
BeltKinematics::BeltKinematics(const PrintConfig &config, bool world_coordinates)
: m_world_coordinates(world_coordinates)
{
m_back_active = m_back_transform.init_from_config(config);
m_machine_frame.init_from_config(config);
if (m_back_active)
// BeltBackTransform stores the inverse of this; keep the forward so
// to_logical() can reverse the whole chain.
m_back_forward = BeltTransformPipeline::build_forward_transform(config);
}
Vec3d BeltKinematics::to_machine(const Vec3d &p) const
{
const Vec3d after_back = m_world_coordinates ? p : m_back_transform.apply(p);
const Vec3d after_remap = this->apply_axis_remap(after_back);
return m_machine_frame.apply(after_remap);
}
Vec3d BeltKinematics::to_logical(const Vec3d &machine) const
{
const Vec3d before_frame = m_machine_frame.apply_inverse(machine);
const Vec3d before_remap = this->apply_axis_remap_inverse(before_frame);
if (m_world_coordinates || ! m_back_active)
return before_remap;
return m_back_forward * before_remap;
}
void install_belt_kinematics(GCodeWriter &writer, const PrintConfig &config, bool world_coordinates)
{
writer.set_kinematics(std::make_unique<BeltKinematics>(config, world_coordinates));
}
} // namespace Slic3r
+69
View File
@@ -0,0 +1,69 @@
#ifndef slic3r_BeltKinematics_hpp_
#define slic3r_BeltKinematics_hpp_
#include "MachineKinematics.hpp"
#include "BeltBackTransform.hpp"
#include "MachineFrameTransform.hpp"
namespace Slic3r {
class PrintConfig;
class GCodeWriter;
// Belt-printer machine frame.
//
// Forward order, as applied per emitted point:
// machine = MachineFrameTransform( axis_remap( BeltBackTransform( logical ) ) )
//
// i.e. the slicer->world back-transform runs FIRST and the machine-frame
// shear/scale LAST, so the latter acts as a global linear transform on the
// already-placed coordinates.
//
// world_coordinates mode (the PA line / PA pattern calibration generators)
// treats the incoming point as already relative to the belt surface -- X across,
// Y along the belt, Z above it -- and therefore skips the back-transform while
// keeping the remap and the machine frame. It is a different coordinate map, not
// a writer mode, which is why it is fixed at construction.
class BeltKinematics : public CartesianKinematics
{
public:
explicit BeltKinematics(const PrintConfig &config, bool world_coordinates = false);
Vec3d to_machine(const Vec3d &p) const override;
Vec3d to_logical(const Vec3d &machine) const override;
// Machine -> build-volume frame. Only the machine-frame shear/scale is undone,
// matching what GCodeProcessor's bounds validation wants. This is deliberately
// NOT to_logical().
Vec3d to_build_volume(const Vec3d &machine) const override
{ return m_machine_frame.apply_inverse(machine); }
// A belt writer has always emitted full XYZ on every move, whether or not any
// individual stage reports itself active. Making this conditional would change
// emitted G-code for an identity-transform belt configuration.
bool must_emit_all_axes() const override { return true; }
bool suppress_lift_at_unknown_position() const override { return true; }
// The machine frame shears and scales, so a circle is an ellipse in machine
// coordinates and G2/G3 cannot describe it.
bool supports_arc_moves() const override { return false; }
bool world_coordinates() const { return m_world_coordinates; }
private:
BeltBackTransform m_back_transform;
MachineFrameTransform m_machine_frame;
// Forward of what m_back_transform inverts, kept so to_logical() can undo it.
Transform3d m_back_forward { Transform3d::Identity() };
bool m_back_active { false };
bool m_world_coordinates { false };
};
// Install a belt machine frame on any GCodeWriter. Any axis remap and build
// volume already configured on the writer are carried over, so this may be
// called before or after those setters. Re-calling it with a different
// world_coordinates value swaps the map (used around the PA line generator).
void install_belt_kinematics(GCodeWriter &writer, const PrintConfig &config,
bool world_coordinates = false);
} // namespace Slic3r
#endif // slic3r_BeltKinematics_hpp_
+29 -225
View File
@@ -1,5 +1,4 @@
#include "../GCode.hpp"
#include "../FirstLayerPlane.hpp"
#include "CoolingBuffer.hpp"
#include <boost/algorithm/string/predicate.hpp>
#include <boost/algorithm/string/replace.hpp>
@@ -33,15 +32,11 @@ CoolingBuffer::CoolingBuffer(GCode &gcodegen) : m_config(gcodegen.config()), m_t
m_extruder_ids.emplace_back(ex.id());
}
// Borrow the first-layer plane from the GCode generator. When inactive
// (non-belt printers and belt printers without Z shear), per-line fan
// re-evaluation is skipped and behavior is bit-identical to the legacy
// per-layer path.
m_first_layer_plane = gcodegen.first_layer_plane();
}
void CoolingBuffer::reset(const Vec3d &position)
{
m_belt_band_active = false;
// BBS: add I and J axis to store center of arc
m_current_pos.assign(7, 0.f);
m_current_pos[0] = float(position.x());
@@ -81,6 +76,9 @@ struct CoolingLine
// ORCA: Add support for ironing fan speed control
TYPE_IRONING_FAN_START = 1 << 19,
TYPE_IRONING_FAN_END = 1 << 20,
// Belt printers: extrusions within the first-layer band above the belt.
TYPE_BELT_BAND_START = 1 << 21,
TYPE_BELT_BAND_END = 1 << 22,
};
CoolingLine(unsigned int type, size_t line_start, size_t line_end) :
@@ -338,13 +336,6 @@ std::string CoolingBuffer::process_layer(std::string &&gcode, size_t layer_id, b
std::vector<PerExtruderAdjustments> per_extruder_adjustments = this->parse_layer_gcode(m_gcode, m_current_pos);
float layer_time_stretched = this->calculate_layer_slowdown(per_extruder_adjustments);
out = this->apply_layer_cooldown(m_gcode, layer_id, layer_time_stretched, per_extruder_adjustments);
// First-layer plane: per-segment fan re-evaluation post-pass. Walks
// the cooled-down gcode and inserts inline M106 commands at band
// crossings (where the path's perpendicular distance to the plane
// crosses close_fan_the_first_x_layers thresholds). No-op when
// the evaluator is inactive.
if (m_first_layer_plane && m_first_layer_plane->is_active())
out = this->apply_first_layer_plane_fan_eval(std::move(out), layer_id, layer_time_stretched);
m_gcode.clear();
}
return out;
@@ -548,6 +539,10 @@ std::vector<PerExtruderAdjustments> CoolingBuffer::parse_layer_gcode(const std::
line.type = CoolingLine::TYPE_IRONING_FAN_START;
} else if (boost::starts_with(sline, ";_IRONING_FAN_END")) { // ORCA: Add support for ironing fan speed control
line.type = CoolingLine::TYPE_IRONING_FAN_END;
} else if (boost::starts_with(sline, ";_BELT_BAND_START")) {
line.type = CoolingLine::TYPE_BELT_BAND_START;
} else if (boost::starts_with(sline, ";_BELT_BAND_END")) {
line.type = CoolingLine::TYPE_BELT_BAND_END;
} else if (boost::starts_with(sline, "G4 ")) {
// Parse the wait time.
line.type = CoolingLine::TYPE_G4;
@@ -889,7 +884,9 @@ std::string CoolingBuffer::apply_layer_cooldown(
{CoolingLine::TYPE_SUPPORT_INTERFACE_FAN_START, false},
{CoolingLine::TYPE_IRONING_FAN_START, false}, // ORCA: Add support for ironing fan speed control
{CoolingLine::TYPE_FORCE_RESUME_FAN, false}};
bool need_set_fan = false;
// Belt printers: a band still open from the previous layer has to take the fan back from
// the layer-level speed issued just above.
bool need_set_fan = m_belt_band_active;
for (const CoolingLine *line : lines) {
const char *line_start = gcode.c_str() + line->line_start;
@@ -903,6 +900,8 @@ std::string CoolingBuffer::apply_layer_cooldown(
if (new_extruder != m_current_extruder) {
m_current_extruder = new_extruder;
change_extruder_set_fan(true);
if (m_belt_band_active)
need_set_fan = true;
}
}
new_gcode.append(line_start, line_end - line_start);
@@ -955,6 +954,13 @@ std::string CoolingBuffer::apply_layer_cooldown(
if (m_additional_fan_speed != -1 && m_config.auxiliary_fan.value)
new_gcode += GCodeWriter::set_additional_fan(m_additional_fan_speed);
}
else if (line->type & CoolingLine::TYPE_BELT_BAND_START) {
m_belt_band_active = true;
need_set_fan = true;
} else if (line->type & CoolingLine::TYPE_BELT_BAND_END) {
m_belt_band_active = false;
need_set_fan = true;
}
else if (line->type & CoolingLine::TYPE_EXTRUDE_END) {
// Just remove this comment.
} else if (line->type & (CoolingLine::TYPE_ADJUSTABLE | CoolingLine::TYPE_EXTERNAL_PERIMETER | CoolingLine::TYPE_WIPE | CoolingLine::TYPE_HAS_F)) {
@@ -1047,7 +1053,15 @@ std::string CoolingBuffer::apply_layer_cooldown(
m_current_fan_speed = speed;
}
};
if (fan_speed_change_requests[CoolingLine::TYPE_OVERHANG_FAN_START]){
if (m_belt_band_active) {
// Belt printers: a tilted layer runs from the belt to the top of the part, so
// "the first layers" are a band along the belt rather than the first slicing
// layers. Extrusions GCode::_extrude() marks as inside that band print with the
// fan off, whatever overhang, bridge or resume request is pending, as the first
// layers of a flat bed do. Leaving the band falls through to the branches below.
set_fan(0);
fan_speed_change_requests[CoolingLine::TYPE_FORCE_RESUME_FAN] = false;
} else if (fan_speed_change_requests[CoolingLine::TYPE_OVERHANG_FAN_START]){
set_fan(overhang_fan_speed);
} else if (fan_speed_change_requests[CoolingLine::TYPE_INTERNAL_BRIDGE_FAN_START]){ // ORCA: Add support for separate internal bridge fan speed control
set_fan(internal_bridge_fan_speed);
@@ -1076,214 +1090,4 @@ std::string CoolingBuffer::apply_layer_cooldown(
return new_gcode;
}
// Pure helper: compute the main fan speed for a given effective layer index.
// Mirrors the inline logic in change_extruder_set_fan but is callable from
// per-line code in apply_first_layer_plane_fan_eval.
int CoolingBuffer::compute_main_fan_speed(int effective_layer_id, float layer_time,
unsigned int extruder_id) const
{
#define EXTRUDER_CFG(opt) m_config.opt.get_at(extruder_id)
float fan_min_speed = EXTRUDER_CFG(fan_min_speed);
float fan_max_speed = EXTRUDER_CFG(fan_max_speed);
bool reduce_fan_stop_start_freq = EXTRUDER_CFG(reduce_fan_stop_start_freq);
int close_fan_the_first_x_layers = EXTRUDER_CFG(close_fan_the_first_x_layers);
int full_fan_speed_layer = EXTRUDER_CFG(full_fan_speed_layer);
float slow_down_layer_time = float(EXTRUDER_CFG(slow_down_layer_time));
float fan_cooling_layer_time = float(EXTRUDER_CFG(fan_cooling_layer_time));
#undef EXTRUDER_CFG
if (close_fan_the_first_x_layers <= 0 && full_fan_speed_layer > 0)
close_fan_the_first_x_layers = 1;
float fan_speed_new = reduce_fan_stop_start_freq ? fan_min_speed : 0.f;
if (effective_layer_id >= close_fan_the_first_x_layers) {
if (layer_time < slow_down_layer_time) {
fan_speed_new = fan_max_speed;
} else if (layer_time < fan_cooling_layer_time) {
double t = (layer_time - slow_down_layer_time) /
(fan_cooling_layer_time - slow_down_layer_time);
fan_speed_new = float(int(floor(t * fan_min_speed +
(1. - t) * fan_max_speed) + 0.5));
}
if (effective_layer_id + 1 < full_fan_speed_layer) {
float factor = float(effective_layer_id + 1 - close_fan_the_first_x_layers)
/ float(full_fan_speed_layer - close_fan_the_first_x_layers);
fan_speed_new = float(std::clamp(int(fan_speed_new * factor + 0.5f), 0, 255));
}
} else {
fan_speed_new = 0.f;
}
return int(fan_speed_new);
}
// Post-pass: walk the cooled-down gcode line by line, track XYZ position,
// and insert M106 commands at first-layer-plane band crossings so the fan
// follows perpendicular distance to the plane rather than the slicing-layer
// index. Only invoked when the FirstLayerPlane evaluator is active.
//
// This implementation is intentionally minimal: it overrides only the MAIN
// fan (the one set by GCodeWriter::set_fan); overhang/internal-bridge/etc
// special fans remain at their layer-level values from apply_layer_cooldown.
// That keeps the per-line logic small while still giving the user precise
// fan control near the belt surface, which is the main quality concern.
std::string CoolingBuffer::apply_first_layer_plane_fan_eval(
std::string &&gcode_in, size_t /*layer_id*/, float layer_time)
{
if (!m_first_layer_plane || !m_first_layer_plane->is_active())
return std::move(gcode_in);
const std::string &gcode = gcode_in;
std::string out;
out.reserve(gcode.size() + 256);
// Match the PWM floor applied at every other set_fan call in this file so
// band-crossing M106 emissions start the fan reliably at low speeds.
const unsigned int part_cooling_fan_min_pwm = static_cast<unsigned int>(std::max(0, m_config.part_cooling_fan_min_pwm.value));
// Track position in slicing-frame mm. Seed from m_current_pos which the
// CoolingBuffer keeps up-to-date across layers.
Vec3d cur_pos_mm(m_current_pos[0], m_current_pos[1], m_current_pos[2]);
// Track current main fan speed by parsing M106 commands as we walk so
// we can restore it after a band exit.
int current_main_fan = m_fan_speed;
int pre_band_main_fan = current_main_fan;
// Implicit initial state: assume the layer started "out of the band"
// (i.e., the layer-level fan setting from apply_layer_cooldown is in
// effect). The first movement we encounter will reconcile this.
bool in_first_layer_band = false;
unsigned int active_extruder = m_current_extruder;
auto parse_xyz_into = [](const std::string_view &line_sv, Vec3d &p) {
if (line_sv.size() < 3) return false;
if (line_sv[0] != 'G') return false;
if (line_sv[1] != '0' && line_sv[1] != '1') return false;
if (line_sv[2] != ' ' && line_sv[2] != '\t') return false;
const char *c = line_sv.data() + 3;
const char *end = line_sv.data() + line_sv.size();
bool any = false;
while (c < end && *c != ';') {
while (c < end && (*c == ' ' || *c == '\t')) ++c;
if (c >= end || *c == ';' || *c == '\n' || *c == '\r') break;
char axis = *c;
++c;
if (axis == 'X' || axis == 'Y' || axis == 'Z') {
char *next;
double v = std::strtod(c, &next);
if (next != c) {
if (axis == 'X') p.x() = v;
else if (axis == 'Y') p.y() = v;
else p.z() = v;
c = next;
any = true;
continue;
}
}
// Skip unrecognized word.
while (c < end && *c != ' ' && *c != '\t' && *c != ';' && *c != '\n')
++c;
}
return any;
};
auto parse_m106 = [](const std::string_view &line_sv) -> int {
// Returns -1 if not an M106, otherwise the S value (0..255).
if (line_sv.size() < 4 || line_sv[0] != 'M') return -1;
if (!(line_sv[1] == '1' && line_sv[2] == '0' && line_sv[3] == '6'))
return -1;
// Find S<value>
size_t s_pos = line_sv.find('S');
if (s_pos == std::string_view::npos) return -1;
const char *c = line_sv.data() + s_pos + 1;
char *next;
long v = std::strtol(c, &next, 10);
if (next == c) return -1;
return int(std::clamp<long>(v, 0, 255));
};
auto parse_m107 = [](const std::string_view &line_sv) -> bool {
return line_sv.size() >= 4 && line_sv[0] == 'M' &&
line_sv[1] == '1' && line_sv[2] == '0' && line_sv[3] == '7';
};
auto parse_tool_change = [this](const std::string_view &line_sv) -> int {
// Returns the new extruder id, or -1 if not a toolchange.
if (line_sv.size() < m_toolchange_prefix.size() + 1) return -1;
if (line_sv.compare(0, m_toolchange_prefix.size(), m_toolchange_prefix) != 0)
return -1;
const char *c = line_sv.data() + m_toolchange_prefix.size();
char *next;
long v = std::strtol(c, &next, 10);
if (next == c) return -1;
return int(v);
};
const char *p = gcode.c_str();
const char *end = gcode.c_str() + gcode.size();
while (p < end) {
const char *line_end = p;
while (line_end < end && *line_end != '\n') ++line_end;
const char *next_line = line_end;
if (next_line < end) ++next_line; // include the '\n'
std::string_view line_sv(p, line_end - p);
// Track tool changes so the per-line fan eval uses the right extruder.
int new_tool = parse_tool_change(line_sv);
if (new_tool >= 0)
active_extruder = unsigned(new_tool);
// Track existing fan commands so we can restore the right value when
// exiting a band.
int m106_speed = parse_m106(line_sv);
if (m106_speed >= 0) {
current_main_fan = m106_speed;
if (!in_first_layer_band)
pre_band_main_fan = m106_speed;
} else if (parse_m107(line_sv)) {
current_main_fan = 0;
if (!in_first_layer_band)
pre_band_main_fan = 0;
}
// Movement line: parse XYZ, evaluate plane, possibly emit a fan
// change BEFORE this line.
bool moved = parse_xyz_into(line_sv, cur_pos_mm);
if (moved) {
const int eff_idx = m_first_layer_plane->effective_layer_index(cur_pos_mm);
const int close_n = m_config.close_fan_the_first_x_layers.get_at(active_extruder);
const bool now_in_band = eff_idx < std::max(close_n, 1);
if (now_in_band != in_first_layer_band) {
// Band crossing: emit a M106 with the appropriate speed.
int target_fan;
if (now_in_band) {
// Entering the first-layer band: fan off.
pre_band_main_fan = current_main_fan;
target_fan = compute_main_fan_speed(eff_idx, layer_time, active_extruder);
} else {
// Exiting the band: restore the layer's normal fan speed.
// Use compute_main_fan_speed with the effective index so
// the linear ramp factor (close_fan→full_fan_speed_layer)
// also follows distance from the plane.
target_fan = compute_main_fan_speed(eff_idx, layer_time, active_extruder);
if (target_fan == 0)
target_fan = pre_band_main_fan;
}
if (target_fan != current_main_fan) {
out += GCodeWriter::set_fan(m_config.gcode_flavor, target_fan, part_cooling_fan_min_pwm);
current_main_fan = target_fan;
m_fan_speed = target_fan;
m_current_fan_speed = target_fan;
}
in_first_layer_band = now_in_band;
}
}
out.append(p, next_line - p);
p = next_line;
}
return out;
}
} // namespace Slic3r
+3 -19
View File
@@ -10,7 +10,6 @@ namespace Slic3r {
class GCode;
class Layer;
class FirstLayerPlane;
struct PerExtruderAdjustments;
// A standalone G-code filter, to control cooling of the print.
@@ -37,21 +36,6 @@ private:
// Returns the adjusted G-code.
std::string apply_layer_cooldown(const std::string &gcode, size_t layer_id, float layer_time, std::vector<PerExtruderAdjustments> &per_extruder_adjustments);
// First-layer plane: per-line fan re-evaluation post-pass. Walks the
// post-cooldown gcode, tracks XYZ position, and inserts M106 commands at
// band-crossing transitions in slicing-frame coordinates. Only runs
// when m_first_layer_plane is active.
std::string apply_first_layer_plane_fan_eval(std::string &&gcode_in,
size_t layer_id,
float layer_time);
// Pure helper: compute the main fan speed for a given effective layer
// index (layer-id units, mapped through the plane evaluator) and the
// current extruder. Mirrors the inline logic in the change_extruder_set_fan
// lambda but is callable from per-line code.
int compute_main_fan_speed(int effective_layer_id, float layer_time,
unsigned int extruder_id) const;
// G-code snippet cached for the support layers preceding an object layer.
std::string m_gcode;
// Internal data.
@@ -74,9 +58,9 @@ private:
unsigned int m_current_nozzle;
//BBS: current fan speed
int m_current_fan_speed;
// First-layer plane evaluator, borrowed from GCode. Null = inactive
// (legacy per-layer fan control).
const FirstLayerPlane *m_first_layer_plane = nullptr;
// Belt printers: the extrusion being processed lies in the first-layer band above the
// belt (between a ";_BELT_BAND_START" and a ";_BELT_BAND_END"). Kept across layers.
bool m_belt_band_active = false;
};
}
+30 -12
View File
@@ -2776,10 +2776,10 @@ bool GCodeProcessor::check_multi_extruder_gcode_valid(const int
};
// Belt-printer post-gcode shear/scale/post_remap is applied as the final
// step of BeltGCodeWriter::to_machine_coords, so MoveVertex.position is
// in the printer's machine frame. Undo it here so XY area and Z height
// checks operate in the build-volume frame that printable_area /
// printable_height are defined in. For non-belt printers
// step of BeltKinematics::to_machine, so MoveVertex.position is
// in the printer's machine frame. Undo it here so the XY area check
// operates in the build-volume frame that printable_area is defined in
// (the height checks below are skipped on belt printers). For non-belt printers
// (is_active() == false) apply_inverse is identity and behaviour is
// unchanged from before.
const bool machine_frame_active = m_machine_frame_transform.is_active();
@@ -2860,7 +2860,12 @@ bool GCodeProcessor::check_multi_extruder_gcode_valid(const int
valid = false;
}
}
if ( iter->second.max_print_z > plate_printable_height ) { //over height
// Belt printers: the Z recorded here grows with belt travel (machine Z with the
// frame transform, the slicing-frame Z without it), while printable_height is the
// clearance above the belt; the two are not comparable, so the over-height check
// is skipped, as the preview's ToolHeightOutside warning already is.
// Print::validate() checks the object's height against the clearance.
if ( !m_belt_printer && iter->second.max_print_z > plate_printable_height ) { //over height
m_result.gcode_check_result.error_code |= (1 << 3);
std::pair<int, int> filament_to_object_id;
filament_to_object_id.first = iter->first;
@@ -2901,7 +2906,7 @@ bool GCodeProcessor::check_multi_extruder_gcode_valid(const int
}
// check printable height
if ((extruder_id < printable_heights.size()) && (iter->second.max_print_z > printable_heights[extruder_id])) {
if (!m_belt_printer && (extruder_id < printable_heights.size()) && (iter->second.max_print_z > printable_heights[extruder_id])) {
m_result.gcode_check_result.error_code |= (1 << 1);
std::pair<int, int> filament_to_object_id;
filament_to_object_id.first = iter->first;
@@ -3072,6 +3077,7 @@ void GCodeProcessor::apply_config(const PrintConfig& config)
// bounds rather than machine-frame positions.
m_machine_frame_transform.init_from_config(config);
m_result.machine_frame_transform_active = m_machine_frame_transform.is_active();
m_belt_printer = config.belt_printer.value;
auto filament_maps = config.option<ConfigOptionInts>("filament_map");
if (filament_maps != nullptr) {
@@ -3586,6 +3592,7 @@ void GCodeProcessor::reset()
m_zero_layer_height = 0.0f;
m_first_layer_height = 0.0f;
m_processing_start_custom_gcode = false;
m_in_config_block = false;
m_g1_line_id = 0;
m_layer_id = 0;
m_cp_color.reset();
@@ -4191,9 +4198,20 @@ void GCodeProcessor::process_tags(const std::string_view comment, bool producers
return;
}
if (boost::starts_with(comment, " CONFIG_BLOCK_START")) {
m_in_config_block = true;
return;
}
if (boost::starts_with(comment, " CONFIG_BLOCK_END")) {
m_in_config_block = false;
return;
}
// Belt printer: derive the physical tilt magnitude from the slicing-rotation
// angle header comment (used to enable the preview's belt view).
if (boost::starts_with(comment, " belt_slice_rotation_angle = ")) {
// angle header comment (used to enable the preview's belt view). Only the belt
// header carries it outside the config block; the config block lists the key
// for every printer, belt or not.
if (!m_in_config_block && boost::starts_with(comment, " belt_slice_rotation_angle = ")) {
try {
m_result.belt_tilt_angle = std::abs(std::stof(std::string(comment.substr(29))));
} catch (...) {}
@@ -4220,13 +4238,13 @@ void GCodeProcessor::process_tags(const std::string_view comment, bool producers
return RemapAxis::PosX;
};
if (boost::starts_with(comment, " preslice_remap_x = ")) {
m_result.preslice_remap_x = parse_remap_axis(trim(std::string(comment.substr(25)))); return;
m_result.preslice_remap_x = parse_remap_axis(trim(std::string(comment.substr(20)))); return;
}
if (boost::starts_with(comment, " preslice_remap_y = ")) {
m_result.preslice_remap_y = parse_remap_axis(trim(std::string(comment.substr(25)))); return;
m_result.preslice_remap_y = parse_remap_axis(trim(std::string(comment.substr(20)))); return;
}
if (boost::starts_with(comment, " preslice_remap_z = ")) {
m_result.preslice_remap_z = parse_remap_axis(trim(std::string(comment.substr(25)))); return;
m_result.preslice_remap_z = parse_remap_axis(trim(std::string(comment.substr(20)))); return;
}
}
// wipe start tag
@@ -7113,7 +7131,7 @@ void GCodeProcessor::store_move_vertex(EMoveType type, EMovePathType path_type,
// During the start G-code "prepare" stage the toolhead Z is not yet a real
// print height on a normal printer, so it is pinned to the first-layer height
// to keep the preview tidy. Belt printers are the exception: there the Z is
// written explicitly by BeltGCodeWriter and the designed-view back-transform
// written explicitly by the belt kinematics and the designed-view back-transform
// couples machine Z into the rendered model Y (the belt tilt mixes the height
// and belt-feed axes). Overriding Z therefore back-transforms the last
// prepare-stage move (the unretract before the first extrusion) to model
+2
View File
@@ -1164,6 +1164,7 @@ class Print;
// transform on move positions so bounds checks operate in the
// pre-machine-frame (build-volume) frame.
MachineFrameTransform m_machine_frame_transform;
bool m_belt_printer{ false };
unsigned int m_line_id;
unsigned int m_last_line_id;
@@ -1194,6 +1195,7 @@ class Print;
float m_first_layer_height; // mm
float m_zero_layer_height; // mm
bool m_processing_start_custom_gcode;
bool m_in_config_block;
unsigned int m_g1_line_id;
unsigned int m_layer_id;
CpColor m_cp_color;
@@ -9,7 +9,7 @@ namespace Slic3r {
// Post-stage machine-frame transform for belt printers.
//
// Applied in BeltGCodeWriter::to_machine_coords AFTER the back-transform and
// Applied in BeltKinematics::to_machine AFTER the back-transform and
// the gcode_remap_* axis remap. Maps Cartesian (axis-permuted) G-code
// coordinates into the printer's physical machine frame.
//
+47
View File
@@ -0,0 +1,47 @@
#include "MachineKinematics.hpp"
namespace Slic3r {
// Moved verbatim from GCodeWriter::apply_axis_remap().
Vec3d CartesianKinematics::apply_axis_remap(const Vec3d &pos) const
{
if (!has_axis_remap())
return pos;
auto remap = [this, &pos](int r) -> double {
int axis = r % 3;
if (r < 3) return pos[axis];
if (r < 6) return -pos[axis];
return m_build_vol_max[axis] - pos[axis];
};
return { remap(m_remap_x), remap(m_remap_y), remap(m_remap_z) };
}
// Inverse of the above. Output axis i is fed by source axis (r_i % 3); walking
// the three outputs therefore fills every source component exactly once, so long
// as the remap is a permutation (which set_axis_remap callers guarantee).
Vec3d CartesianKinematics::apply_axis_remap_inverse(const Vec3d &machine) const
{
if (!has_axis_remap())
return machine;
Vec3d out = Vec3d::Zero();
const int r[3] = { m_remap_x, m_remap_y, m_remap_z };
for (int i = 0; i < 3; ++i) {
const int axis = r[i] % 3;
if (r[i] < 3) out[axis] = machine[i];
else if (r[i] < 6) out[axis] = -machine[i];
else out[axis] = m_build_vol_max[axis] - machine[i];
}
return out;
}
Vec3d CartesianKinematics::to_machine(const Vec3d &p) const
{
return this->apply_axis_remap(p);
}
Vec3d CartesianKinematics::to_logical(const Vec3d &machine) const
{
return this->apply_axis_remap_inverse(machine);
}
} // namespace Slic3r
+106
View File
@@ -0,0 +1,106 @@
#ifndef slic3r_MachineKinematics_hpp_
#define slic3r_MachineKinematics_hpp_
#include "../Point.hpp"
namespace Slic3r {
// The frame contract for emitted movement.
//
// GCodeWriter produces points in the *logical placed* frame: plate offsets have
// already been subtracted, but no machine-specific mapping has been applied.
// A MachineKinematics turns that into the coordinates actually written to
// G-code, and answers the two structural questions the writer needs in order to
// decide which axis words it may omit.
//
// This is a seam for writer-generated movement only. Start/end/custom G-code,
// classic wipe-tower output and GCodeWriter::extrude_arc_to_xy() do NOT pass
// through it: they write machine coordinates directly.
class MachineKinematics
{
public:
virtual ~MachineKinematics() = default;
// Logical placed point -> emitted machine point.
virtual Vec3d to_machine(const Vec3d &p) const = 0;
// Inverse of to_machine(), back to the logical placed frame. Intended for
// consumers that must reconstruct model coordinates from emitted G-code
// (the G-code viewer's upright preview).
virtual Vec3d to_logical(const Vec3d &machine) const = 0;
// Machine point -> build-volume frame, for bounds validation only. This is
// deliberately NOT to_logical(): the build-volume check wants the physical
// frame the printable area is expressed in, not the model frame. Keeping
// them separate stops the two contracts from being confused.
virtual Vec3d to_build_volume(const Vec3d &machine) const = 0;
// True when a move must emit X, Y and Z because omitting a word would be
// wrong under this mapping. Deliberately not called "couples_axes": a pure
// axis permutation forces full emission without physically coupling axes.
virtual bool must_emit_all_axes() const = 0;
// True when a lift must be suppressed while the current position is unknown,
// because _travel_to_z() re-emits the logical X/Y through this mapping and an
// uninitialised position would map to a bogus machine point -- for a reverse
// mapping, the far corner of the bed.
virtual bool suppress_lift_at_unknown_position() const = 0;
// True when a G2/G3 arc in the logical XY plane is still the same arc in the
// machine frame. Arc moves emit only X, Y, I and J, so this asks a narrower
// question than must_emit_all_axes(): whether logical X and Y reach the
// machine unchanged. A mapping that only negates or reverses Z keeps its
// arcs; one that permutes X or Y moves the arc out of the plane that I/J
// describes, and a shear turns the circle into an ellipse G2/G3 cannot
// express at all.
virtual bool supports_arc_moves() const = 0;
// Configuration. GCodeWriter forwards its setters here so that the state
// lives with the strategy and a strategy installed before the setters run
// still receives it.
virtual void set_axis_remap(int rx, int ry, int rz) = 0;
virtual void set_build_volume_max(const Vec3d &max) = 0;
};
// Axis remap only -- the historical GCodeWriter behaviour, moved verbatim.
//
// The remap encodes, per output axis, which source axis feeds it and how:
// r < 3 : source axis r, unchanged
// r < 6 : source axis r-3, negated
// else : source axis r-6, reversed within the build volume
class CartesianKinematics : public MachineKinematics
{
public:
Vec3d to_machine(const Vec3d &p) const override;
Vec3d to_logical(const Vec3d &machine) const override;
Vec3d to_build_volume(const Vec3d &machine) const override { return machine; }
bool must_emit_all_axes() const override { return this->has_axis_remap(); }
bool suppress_lift_at_unknown_position() const override { return this->has_axis_remap(); }
// X and Y must reach the machine untouched. Because the remap is a
// permutation, pinning those two also pins Z to Z, so a mapping that only
// negates or reverses Z still supports arcs -- every word a G2/G3 emits is
// unchanged by it.
bool supports_arc_moves() const override { return m_remap_x == 0 && m_remap_y == 1; }
void set_axis_remap(int rx, int ry, int rz) override
{ m_remap_x = rx; m_remap_y = ry; m_remap_z = rz; }
void set_build_volume_max(const Vec3d &max) override { m_build_vol_max = max; }
bool has_axis_remap() const
{ return m_remap_x != 0 || m_remap_y != 1 || m_remap_z != 2; }
protected:
Vec3d apply_axis_remap(const Vec3d &pos) const;
Vec3d apply_axis_remap_inverse(const Vec3d &pos) const;
int m_remap_x { 0 };
int m_remap_y { 1 };
int m_remap_z { 2 };
Vec3d m_build_vol_max { Vec3d::Zero() };
};
} // namespace Slic3r
#endif // slic3r_MachineKinematics_hpp_
+2 -2
View File
@@ -627,7 +627,7 @@ void compute_global_occlusion(GlobalModelInfo &result, const PrintObject *po,
SeamPosition seam_position = spAligned) {
BOOST_LOG_TRIVIAL(debug)
<< "SeamPlacer: gather occlusion meshes: start";
auto obj_transform = po->trafo_centered();
auto obj_transform = po->trafo_sliced();
indexed_triangle_set triangle_set;
indexed_triangle_set negative_volumes_set;
//add all parts
@@ -712,7 +712,7 @@ void gather_enforcers_blockers(GlobalModelInfo &result, const PrintObject *po) {
BOOST_LOG_TRIVIAL(debug)
<< "SeamPlacer: build AABB trees for raycasting enforcers/blockers: start";
auto obj_transform = po->trafo_centered();
auto obj_transform = po->trafo_sliced();
for (const ModelVolume *mv : po->model_object()->volumes) {
if (mv->is_seam_painted()) {
+2 -2
View File
@@ -1066,8 +1066,8 @@ void ToolOrdering::fill_wipe_tower_partitions(const PrintConfig &config, coordf_
// The `print_z < object_bottom_z` clause reads "below the object" as "raft
// gap". On a belt printer that is wrong: the brim apron legitimately prints
// below the object's first layer, and treating those layers as raft would put a
// wipe tower at negative Z. Belt brim and the prime tower are mutually
// exclusive (rejected in Print::validate()), so simply drop the clause there.
// wipe tower at negative Z. A belt printer never prints the classic
// prime tower (Print::has_wipe_tower()), so simply drop the clause there.
//
// Gate on config.belt_printer, NOT on has_belt_brim: every layer below the
// object bottom on a belt printer is legitimately a sub-object stream - brim
+6 -2
View File
@@ -74,13 +74,17 @@ public:
void set_layer_tools_ptr(const LayerTools* lt) { m_layer_tools = lt; }
private:
// Returns true if entity is not printed with its usual extruder for a given copy.
// Returns true if entity is not printed with its usual extruder for a given
// copy -- i.e. it was claimed as a wiping/purge extrusion. Public because the
// belt purge prism uses it to tell which of its fills actually carry purge
// from the ones that are unclaimed waste (Print::_plan_belt_purge()).
bool is_entity_overridden(const ExtrusionEntity* entity, const PrintObject *object, size_t copy_id) const {
auto it = entity_map.find(std::make_tuple(entity, object));
return it != entity_map.end() && copy_id < it->second.size() && it->second[copy_id] != -1;
}
private:
int first_nonsoluble_extruder_on_layer(const PrintConfig& print_config) const;
int last_nonsoluble_extruder_on_layer(const PrintConfig& print_config) const;
+117 -20
View File
@@ -1,4 +1,5 @@
#include "GCodeWriter.hpp"
#include "FirstLayerPlane.hpp"
#include "CustomGCode.hpp"
#include "Geometry.hpp"
#include "I18N.hpp"
@@ -24,34 +25,55 @@ namespace Slic3r {
bool GCodeWriter::full_gcode_comment = true;
// A lift emitted through _travel_to_z() re-emits the stored logical X/Y under a
// mapping that must emit every axis. While the position is unknown that X/Y is
// the uninitialised origin, which maps to a real but wrong machine point, so the
// lift has to be skipped rather than commanded.
bool GCodeWriter::must_skip_lift_now() const
{
return m_kinematics->suppress_lift_at_unknown_position() && ! this->is_current_position_clear();
}
bool GCodeWriter::point_on_first_layer(const Vec3d &point_logical) const
{
if (m_first_layer_plane && m_first_layer_plane->is_active())
return m_first_layer_plane->is_first_layer(point_logical, m_first_layer_thickness_mm);
return m_is_first_layer;
}
void GCodeWriter::set_axis_remap(int rx, int ry, int rz)
{
m_remap_x = rx;
m_remap_y = ry;
m_remap_z = rz;
m_kinematics->set_axis_remap(rx, ry, rz);
}
void GCodeWriter::set_build_volume_max(const Vec3d &max)
{
m_build_vol_max = max;
m_kinematics->set_build_volume_max(max);
}
void GCodeWriter::set_kinematics(std::unique_ptr<MachineKinematics> kinematics)
{
assert(kinematics);
m_kinematics = std::move(kinematics);
// Replay whatever was configured on the previous strategy so callers may
// install the kinematics before or after set_axis_remap/set_build_volume_max.
m_kinematics->set_axis_remap(m_remap_x, m_remap_y, m_remap_z);
m_kinematics->set_build_volume_max(m_build_vol_max);
}
// Kept as the writer-facing name for "this move must emit every axis word".
bool GCodeWriter::has_axis_remap() const
{
return m_remap_x != 0 || m_remap_y != 1 || m_remap_z != 2;
return m_kinematics->must_emit_all_axes();
}
Vec3d GCodeWriter::apply_axis_remap(const Vec3d &pos) const
{
if (!has_axis_remap())
return pos;
auto remap = [this, &pos](int r) -> double {
int axis = r % 3;
if (r < 3) return pos[axis];
if (r < 6) return -pos[axis];
return m_build_vol_max[axis] - pos[axis];
};
return { remap(m_remap_x), remap(m_remap_y), remap(m_remap_z) };
return m_kinematics->to_machine(pos);
}
bool GCodeWriter::supports_separate_travel_acceleration(GCodeFlavor flavor)
@@ -795,7 +817,7 @@ std::string GCodeWriter::travel_to_xy(const Vec2d &point, const std::string &com
} else {
w.emit_xy(point_on_plate);
}
auto speed = m_is_first_layer
auto speed = this->point_on_first_layer(Vec3d(point_on_plate.x(), point_on_plate.y(), m_pos.z()))
? this->config.get_abs_value_at("initial_layer_travel_speed", m_cached_extruder_idx) : this->config.travel_speed.get_at(m_cached_extruder_idx);
w.emit_f(speed * 60.0);
//BBS
@@ -808,6 +830,8 @@ it will not perform subsequent lifts, even if Z was raised manually
(i.e. with travel_to_z()) and thus _lifted was reduced. */
std::string GCodeWriter::lazy_lift(LiftType lift_type, bool spiral_vase)
{
if (m_force_normal_lift)
lift_type = LiftType::NormalLift;
// check whether the above/below conditions are met
double target_lift = 0;
{
@@ -822,6 +846,10 @@ std::string GCodeWriter::lazy_lift(LiftType lift_type, bool spiral_vase)
// BBS
if (m_lifted == 0 && m_to_lift == 0 && target_lift > 0) {
if (spiral_vase) {
if (this->must_skip_lift_now())
// Record no lift, so a later unlift() does not descend from a
// height that was never commanded.
return "";
m_lifted = target_lift;
return this->_travel_to_z(m_pos(2) + target_lift, "lift Z");
}
@@ -836,7 +864,7 @@ std::string GCodeWriter::lazy_lift(LiftType lift_type, bool spiral_vase)
// BBS: immediately execute an undelayed lift move with a spiral lift pattern
// designed specifically for subsequent gcode injection (e.g. timelapse)
std::string GCodeWriter::eager_lift(const LiftType type) {
const LiftType effective_type = type;
const LiftType effective_type = m_force_normal_lift ? LiftType::NormalLift : type;
std::string lift_move;
double target_lift = 0;
{
@@ -867,7 +895,12 @@ std::string GCodeWriter::eager_lift(const LiftType type) {
}
//BBS: if position is unknown use normal lift
else if (target_lift > 0) {
lift_move = _travel_to_z(m_pos(2) + target_lift, "normal lift Z");
if (this->must_skip_lift_now())
// Skipped, not deferred: leave m_lifted at zero below so unlift()
// does not descend from a height that was never commanded.
target_lift = 0.;
else
lift_move = _travel_to_z(m_pos(2) + target_lift, "normal lift Z");
}
m_lifted = target_lift;
m_to_lift = 0;
@@ -888,7 +921,12 @@ std::string GCodeWriter::travel_to_xyz(const Vec3d &point, const std::string &co
// BBS
Vec3d dest_point = point;
auto travel_speed =
m_is_first_layer ? this->config.get_abs_value_at("initial_layer_travel_speed", m_cached_extruder_idx) : this->config.travel_speed.get_at(m_cached_extruder_idx);
this->point_on_first_layer(Vec3d(point.x() - m_x_offset, point.y() - m_y_offset, point.z())) ? this->config.get_abs_value_at("initial_layer_travel_speed", m_cached_extruder_idx) : this->config.travel_speed.get_at(m_cached_extruder_idx);
// See uses_pointwise_travel_speed(): the historical path deliberately emits the
// raw configured speed in the final branch below, ignoring travel_speed.
const double final_travel_speed = this->uses_pointwise_travel_speed()
? travel_speed
: this->config.travel_speed.get_at(m_cached_extruder_idx);
//BBS: a z_hop need to be handle when travel
if (std::abs(m_to_lift) > EPSILON) {
assert(std::abs(m_lifted) < EPSILON);
@@ -946,7 +984,14 @@ std::string GCodeWriter::travel_to_xyz(const Vec3d &point, const std::string &co
w0.emit_comment(GCodeWriter::full_gcode_comment, comment);
slop_move = w0.string();
}
else if (m_to_lift_type == LiftType::NormalLift) {
else if (m_to_lift_type == LiftType::NormalLift && ! this->must_skip_lift_now()) {
// Only lift in place when the current position is known, for a mapping
// that makes _travel_to_z re-emit logical X/Y: at print start (and after
// custom gcode) m_pos.xy is still the uninitialised origin, which would
// map to a bogus machine point. The xy_z_move below then travels straight
// to the destination with full XYZ and establishes the correct position.
// Mappings that do not need this (the historical Cartesian behaviour)
// report false and keep lifting unconditionally.
slop_move = _travel_to_z(target.z(), "normal lift Z");
}
}
@@ -1002,20 +1047,20 @@ std::string GCodeWriter::travel_to_xyz(const Vec3d &point, const std::string &co
if (has_axis_remap()) {
// Remap may couple XY with Z; emit full XYZ in machine coordinates.
w.emit_xyz(apply_axis_remap(point_on_plate));
w.emit_f(this->config.travel_speed.get_at(m_cached_extruder_idx) * 60.0);
w.emit_f(final_travel_speed * 60.0);
w.emit_comment(GCodeWriter::full_gcode_comment, comment);
out_string = w.string();
} else if (!this->is_current_position_clear())
{
//force to move xy first then z after filament change
w.emit_xy(Vec2d(point_on_plate.x(), point_on_plate.y()));
w.emit_f(this->config.travel_speed.get_at(m_cached_extruder_idx) * 60.0);
w.emit_f(final_travel_speed * 60.0);
w.emit_comment(GCodeWriter::full_gcode_comment, comment);
out_string = w.string() + _travel_to_z(point_on_plate.z(), comment);
} else {
GCodeG1Formatter w;
w.emit_xyz(point_on_plate);
w.emit_f(this->config.travel_speed.get_at(m_cached_extruder_idx) * 60.0);
w.emit_f(final_travel_speed * 60.0);
w.emit_comment(GCodeWriter::full_gcode_comment, comment);
out_string = w.string();
}
@@ -1050,8 +1095,9 @@ std::string GCodeWriter::_travel_to_z(double z, const std::string &comment)
double speed = this->config.travel_speed_z.get_at(m_cached_extruder_idx);
if (speed == 0.) {
speed = m_is_first_layer ? this->config.get_abs_value_at("initial_layer_travel_speed", m_cached_extruder_idx)
: this->config.travel_speed.get_at(m_cached_extruder_idx);
speed = this->point_on_first_layer(Vec3d(m_pos.x() - m_x_offset, m_pos.y() - m_y_offset, z))
? this->config.get_abs_value_at("initial_layer_travel_speed", m_cached_extruder_idx)
: this->config.travel_speed.get_at(m_cached_extruder_idx);
}
GCodeG1Formatter w;
@@ -1190,11 +1236,62 @@ std::string GCodeWriter::extrude_to_xy(const Vec2d &point, double dE, const std:
return w.string();
}
// Approximate an arc with linear extrusions, for machine mappings that cannot
// express a G2/G3 (see extrude_arc_to_xy). center_offset is I/J: the centre
// relative to the CURRENT position, which is why this must run before m_pos is
// updated.
std::string GCodeWriter::extrude_arc_as_polyline(const Vec2d &point, const Vec2d &center_offset,
double dE, const bool is_ccw,
const std::string &comment, bool force_no_extrusion)
{
const Vec2d start = Vec2d(m_pos.x(), m_pos.y());
const Vec2d centre = start + center_offset;
const double r = (start - centre).norm();
if (r < EPSILON)
// Degenerate: no arc to speak of, so a single move is exact.
return this->extrude_to_xy(point, dE, comment, force_no_extrusion);
double a0 = std::atan2(start.y() - centre.y(), start.x() - centre.x());
double a1 = std::atan2(point.y() - centre.y(), point.x() - centre.x());
double sweep = a1 - a0;
if (is_ccw) { while (sweep <= 0.) sweep += 2. * PI; }
else { while (sweep >= 0.) sweep -= 2. * PI; }
// Segment count from a chord-deviation bound: r*(1-cos(dtheta/2)) <= tol.
const double tol = 0.005; // mm
const double dmax = (tol >= r) ? PI : 2. * std::acos(1. - tol / r);
const int n = std::max(2, int(std::ceil(std::abs(sweep) / std::max(dmax, EPSILON))));
std::string out;
for (int i = 1; i <= n; ++ i) {
const double a = a0 + sweep * (double(i) / double(n));
const Vec2d p = (i == n) ? point
: Vec2d(centre.x() + r * std::cos(a), centre.y() + r * std::sin(a));
out += this->extrude_to_xy(p, dE / double(n), i == n ? comment : std::string(), force_no_extrusion);
}
return out;
}
//BBS: generate G2 or G3 extrude which moves by arc
//point is end point which means X and Y axis
//center_offset is I and J axis
std::string GCodeWriter::extrude_arc_to_xy(const Vec2d& point, const Vec2d& center_offset, double dE, const bool is_ccw, const std::string& comment, bool force_no_extrusion)
{
// Arcs emit only X/Y/I/J, so a mapping that moves logical X or Y cannot be
// expressed as a G2/G3. GCode::should_disable_arc_fitting() normally stops
// arcs being generated at all for such a mapping, but this is public API, so
// define the behaviour rather than asserting.
//
// This check MUST precede every state mutation below: falling through to
// extrude_to_xy() after filament()->extrude(dE) would advance E twice.
//
// A single chord is not a safe substitute either -- a semicircle would become
// its diameter and a full circle a stationary blob -- so approximate the arc
// with linear segments bounded by a chord tolerance, splitting dE between
// them in proportion to arc length.
if (! m_kinematics->supports_arc_moves())
return this->extrude_arc_as_polyline(point, center_offset, dE, is_ccw, comment, force_no_extrusion);
m_pos(0) = point(0);
m_pos(1) = point(1);
if (!force_no_extrusion)
+71 -12
View File
@@ -9,8 +9,12 @@
#include "Polygon.hpp"
#include "PrintConfig.hpp"
#include "GCode/CoolingBuffer.hpp"
#include "GCode/MachineKinematics.hpp"
#include <memory>
namespace Slic3r {
class FirstLayerPlane;
class GCodeWriter {
public:
virtual ~GCodeWriter() = default;
@@ -18,17 +22,19 @@ public:
bool multiple_extruders;
GCodeWriter() :
multiple_extruders(false), m_curr_filament_extruder(MAXIMUM_EXTRUDER_NUMBER, nullptr),
m_curr_extruder_id (-1),
m_cached_extruder_idx(0),
m_single_extruder_multi_material(false),
m_last_acceleration(0), m_max_acceleration(0),m_last_travel_acceleration(0), m_max_travel_acceleration(0),
m_last_jerk(0), m_max_jerk_x(0), m_max_jerk_y(0),
m_last_bed_temperature(0), m_last_bed_temperature_reached(true),
multiple_extruders(false),
m_lifted(0),
m_to_lift(0),
m_to_lift_type(LiftType::NormalLift),
m_current_speed(3600), m_is_first_layer(true)
m_is_first_layer(true), m_current_speed(3600),
m_kinematics(std::make_unique<CartesianKinematics>()),
m_cached_extruder_idx(0),
m_curr_filament_extruder(MAXIMUM_EXTRUDER_NUMBER, nullptr),
m_curr_extruder_id (-1),
m_single_extruder_multi_material(false),
m_last_acceleration(0), m_max_acceleration(0),m_last_travel_acceleration(0), m_max_travel_acceleration(0),
m_last_jerk(0), m_max_jerk_x(0), m_max_jerk_y(0),
m_last_bed_temperature(0), m_last_bed_temperature_reached(true)
{}
Extruder* filament(size_t extruder_id) { assert(extruder_id < m_curr_filament_extruder.size()); return m_curr_filament_extruder[extruder_id]; }
const Extruder* filament(size_t extruder_id) const { assert(extruder_id < m_curr_filament_extruder.size()); return m_curr_filament_extruder[extruder_id]; }
@@ -85,6 +91,10 @@ public:
virtual std::string extrude_to_xy(const Vec2d &point, double dE, const std::string &comment = std::string(), bool force_no_extrusion = false);
//BBS: generate G2 or G3 extrude which moves by arc
std::string extrude_arc_to_xy(const Vec2d &point, const Vec2d &center_offset, double dE, const bool is_ccw, const std::string &comment = std::string(), bool force_no_extrusion = false);
// Linear approximation of an arc, used when the machine mapping cannot
// express a G2/G3. Must be called before m_pos is updated: center_offset is
// relative to the current position.
std::string extrude_arc_as_polyline(const Vec2d &point, const Vec2d &center_offset, double dE, const bool is_ccw, const std::string &comment = std::string(), bool force_no_extrusion = false);
virtual std::string extrude_to_xyz(const Vec3d &point, double dE, const std::string &comment = std::string(), bool force_no_extrusion = false);
std::string retract(bool before_wipe = false, double retract_length = 0);
std::string retract_for_toolchange(bool before_wipe = false, double retract_length = 0);
@@ -142,10 +152,28 @@ public:
void set_build_volume_max(const Vec3d &max);
bool has_axis_remap() const;
// Install the machine frame mapping. Any axis remap / build volume already
// configured is carried over, so install order does not matter.
void set_kinematics(std::unique_ptr<MachineKinematics> kinematics);
const MachineKinematics& kinematics() const { return *m_kinematics; }
// First-layer plane evaluator. When set to an active plane, travel speed
// selection consults the plane per destination point instead of the
// layer-coarse m_is_first_layer flag. Borrowed pointer; lifetime is owned
// by GCode, which constructs the plane after the writer exists -- so this is
// deliberately a setter and not a constructor argument.
void set_first_layer_plane(const FirstLayerPlane *plane, double first_layer_height_mm)
{ m_first_layer_plane = plane; m_first_layer_thickness_mm = first_layer_height_mm; }
// Force every lift to a plain vertical lift. Spiral and slope lifts compute
// their slope in the logical frame and do not account for a machine mapping
// that couples axes.
void set_force_normal_lift(bool force) { m_force_normal_lift = force; }
// Returns whether this flavor supports separate print and travel acceleration.
static bool supports_separate_travel_acceleration(GCodeFlavor flavor);
protected:
// Position/lift/offset state — accessible to subclasses (e.g. BeltGCodeWriter)
// Position/lift/offset state.
Vec3d m_pos = Vec3d::Zero();
double m_x_offset{ 0 };
double m_y_offset{ 0 };
@@ -158,17 +186,48 @@ protected:
virtual std::string _travel_to_z(double z, const std::string &comment);
// Axis remap state — accessible to subclasses.
// Whether a destination gets first-layer treatment. With an active plane
// evaluator, distance from the plane decides; otherwise the layer-coarse
// m_is_first_layer flag does.
bool point_on_first_layer(const Vec3d &point_logical) const;
// True when a lift must be skipped because this mapping would emit the
// stored logical X/Y and that position is not yet known.
bool must_skip_lift_now() const;
// True when travel speed is selected per destination point rather than per
// layer. Set for writers that install a first-layer plane. The historical
// path emits the raw configured travel speed in the final branch of
// travel_to_xyz(), ignoring the first-layer selection computed at the top of
// that function; a plane-driven writer uses the first-layer-aware value
// throughout. Both are preserved exactly -- unifying them would change
// emitted feedrates and belongs in its own commit.
bool uses_pointwise_travel_speed() const { return m_first_layer_plane != nullptr; }
// Borrowed; null = inactive.
const FirstLayerPlane *m_first_layer_plane = nullptr;
double m_first_layer_thickness_mm = 0.;
bool m_force_normal_lift = false;
// The machine frame mapping. Owns the axis-remap state that used to live
// here as m_remap_* / m_build_vol_max; the setters above forward to it.
// Never null: a CartesianKinematics at the identity remap reproduces the
// historical behaviour exactly.
std::unique_ptr<MachineKinematics> m_kinematics;
// Last configured remap / build volume, replayed onto a newly installed
// kinematics so set_kinematics() and the setters are order-independent.
int m_remap_x = 0; // RemapAxis: 0=+X, 1=+Y, 2=+Z, 3=-X, etc.
int m_remap_y = 1;
int m_remap_z = 2;
Vec3d m_build_vol_max = Vec3d::Zero();
// Apply axis remap to a point. Returns pos unchanged if remap is identity.
// Apply the machine frame mapping to a point. Returns pos unchanged when the
// mapping is the identity.
Vec3d apply_axis_remap(const Vec3d &pos) const;
// Motion uses the global/base process variant until a filament becomes active.
// Protected so BeltGCodeWriter indexes the per-extruder speed options (travel_speed,
// Protected so subclasses index the per-extruder speed options (travel_speed,
// travel_speed_z, initial_layer_travel_speed) exactly as the base writer does.
size_t m_cached_extruder_idx;
+16 -9
View File
@@ -1215,7 +1215,7 @@ static inline std::vector<std::vector<ExPolygons>> segmentation_top_and_bottom_l
// project downards pointing painted triangles over bottom surfaces.
std::vector<std::vector<Polygons>> top_raw(num_facets_states), bottom_raw(num_facets_states);
std::vector<float> zs = zs_from_layers(layers);
Transform3d object_trafo = print_object.trafo_centered();
Transform3d object_trafo = print_object.trafo_sliced();
#ifdef MM_SEGMENTATION_DEBUG_TOP_BOTTOM
static int iRun = 0;
@@ -1244,10 +1244,16 @@ static inline std::vector<std::vector<ExPolygons>> segmentation_top_and_bottom_l
slicing_params.trafo = volume_trafo;
Polygons bottom_slice = slice_mesh(painted, zs[0], slicing_params);
top.erase(top.begin());
bottom.erase(bottom.begin());
bottom[0] = union_(bottom[0], bottom_slice);
// Only the requested projections exist: with
// top_shell_layers = 0 `top` is empty and erasing its begin() was
// undefined (found by fuzzing: a sunk, painted object crashed here).
if (! top.empty())
top.erase(top.begin());
if (! bottom.empty()) {
bottom.erase(bottom.begin());
if (! bottom.empty())
bottom[0] = union_(bottom[0], bottom_slice);
}
} else
slice_mesh_slabs(painted, zs, volume_trafo, max_top_layers > 0 ? &top : nullptr, max_bottom_layers > 0 ? &bottom : nullptr, nullptr, throw_on_cancel_callback);
auto merge = [](std::vector<Polygons> &&src, std::vector<Polygons> &dst) {
@@ -2039,17 +2045,19 @@ std::vector<std::vector<ExPolygons>> segmentation_by_painting(const PrintObject
}
BOOST_LOG_TRIVIAL(debug) << "Print object segmentation - Projection of painted triangles - Begin";
// The layers were sliced in this frame (belt rotation, remap and Z lift included), and it already centers the object.
const Transform3d object_trafo = print_object.trafo_sliced();
for (const ModelVolume *mv : print_object.model_object()->volumes) {
const ModelVolumeFacetsInfo facets_info = extract_facets_info(*mv);
tbb::parallel_for(tbb::blocked_range<size_t>(1, num_facets_states), [&mv, &print_object, &facets_info, &layers, &edge_grids, &painted_lines, &painted_lines_mutex, &input_expolygons, &throw_on_cancel_callback](const tbb::blocked_range<size_t> &range) {
tbb::parallel_for(tbb::blocked_range<size_t>(1, num_facets_states), [&mv, &object_trafo, &facets_info, &layers, &edge_grids, &painted_lines, &painted_lines_mutex, &input_expolygons, &throw_on_cancel_callback](const tbb::blocked_range<size_t> &range) {
for (size_t extruder_idx = range.begin(); extruder_idx < range.end(); ++extruder_idx) {
throw_on_cancel_callback();
const indexed_triangle_set custom_facets = facets_info.facets_annotation.get_facets(*mv, EnforcerBlockerType(extruder_idx));
if (!mv->is_model_part() || custom_facets.indices.empty())
continue;
const Transform3f tr = print_object.trafo().cast<float>() * mv->get_matrix().cast<float>();
tbb::parallel_for(tbb::blocked_range<size_t>(0, custom_facets.indices.size()), [&tr, &custom_facets, &print_object, &layers, &edge_grids, &input_expolygons, &painted_lines, &painted_lines_mutex, &extruder_idx](const tbb::blocked_range<size_t> &range) {
const Transform3f tr = (object_trafo * mv->get_matrix()).cast<float>();
tbb::parallel_for(tbb::blocked_range<size_t>(0, custom_facets.indices.size()), [&tr, &custom_facets, &layers, &edge_grids, &input_expolygons, &painted_lines, &painted_lines_mutex, &extruder_idx](const tbb::blocked_range<size_t> &range) {
for (size_t facet_idx = range.begin(); facet_idx < range.end(); ++facet_idx) {
float min_z = std::numeric_limits<float>::max();
float max_z = std::numeric_limits<float>::lowest();
@@ -2102,7 +2110,6 @@ std::vector<std::vector<ExPolygons>> segmentation_by_painting(const PrintObject
Line line_to_test(Point(scale_(line_start_f.x()), scale_(line_start_f.y())),
Point(scale_(line_end_f.x()), scale_(line_end_f.y())));
line_to_test.translate(-print_object.center_offset());
// BoundingBoxes for EdgeGrids are computed from printable regions. It is possible that the painted line (line_to_test) could
// be outside EdgeGrid's BoundingBox, for example, when the negative volume is used on the painted area (GH #7618).
+38 -14
View File
@@ -20,7 +20,6 @@
#include "GCode.hpp"
#include "BeltGCode.hpp"
#include "BeltTransform.hpp"
#include "GCode/MachineFrameTransform.hpp"
#include "GCode/WipeTower.hpp"
#include "GCode/WipeTower2.hpp"
#include "GCode/WipeTowerEstimate.hpp"
@@ -121,6 +120,10 @@ 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", "first_layer_plane_offset", "first_layer_plane_thickness",
// Only inflates the GUI bed volume, like printable_area.
"belt_printer_infinite_y",
//BBS
"additional_cooling_fan_speed",
"reduce_crossing_wall",
@@ -1417,11 +1420,10 @@ StringObjectException Print::validate(std::vector<StringObjectException> *warnin
return { L("Draft shield is not compatible with belt printer mode.") };
// Belt brim spans many layers and owns the layers below the object, which
// neither the prime tower nor spiral vase can share.
// spiral vase cannot share. The prime tower setting is no obstacle: belt
// printers never print the classic tower, and the belt purge prism is an
// ordinary object that never takes a brim.
if (this->has_belt_brim()) {
if (m_config.enable_prime_tower.value)
return { L("Brim is not compatible with the prime tower on a belt printer. "
"Disable one of them.") };
if (m_config.spiral_mode.value)
return { L("Brim is not compatible with spiral vase mode on a belt printer. "
"Disable one of them.") };
@@ -1551,6 +1553,21 @@ StringObjectException Print::validate(std::vector<StringObjectException> *warnin
add_warning(warningtemp);
}
// The purge tower is a model object the GUI creates and sizes; libslic3r only purges
// into one that exists. A project sliced without it (the CLI on a project saved before
// the tower was generated) changes filament with nowhere to purge.
if (m_config.belt_printer.value && m_config.enable_belt_purge_tower.value
&& m_config.print_sequence != PrintSequence::ByObject
&& ! m_config.spiral_mode.value && this->object_extruders().size() > 1 && ! this->has_belt_purge_tower()) {
StringObjectException warningtemp;
warningtemp.string = L("The belt purge tower is enabled but the project has no purge tower object; "
"filament changes will not be purged. Open the project in the application "
"to generate the tower.");
warningtemp.opt_key = "enable_belt_purge_tower";
warningtemp.is_warning = true;
add_warning(warningtemp);
}
if (m_config.belt_printer.value && m_config.enable_belt_purge_tower.value) {
const size_t prism_count = std::count_if(m_objects.begin(), m_objects.end(), [](const PrintObject *object) {
return object->config().belt_purge_tower_object.value;
@@ -1617,17 +1634,11 @@ StringObjectException Print::validate(std::vector<StringObjectException> *warnin
// is not comparable to printable_height (which is gantry clearance in the
// build-volume frame). Compare against the model's pre-shear Z instead,
// mirroring the bbox computed in PrintObject::update_slicing_parameters.
// When the post-gcode MachineFrameTransform is active the printer's
// physical Z mapping is non-trivial — skip the check entirely.
// The machine-frame transform only changes how that height is written to
// G-code, not how much room there is under the gantry.
const bool belt_printer = this->config().belt_printer.value;
bool skip_max_height_check = false;
if (belt_printer) {
MachineFrameTransform machine_frame;
machine_frame.init_from_config(this->config());
skip_max_height_check = machine_frame.is_active();
}
const double shrinkage_compensation_z = this->shrinkage_compensation().z();
for (size_t print_object_idx = 0; !skip_max_height_check && print_object_idx < m_objects.size(); ++ print_object_idx) {
for (size_t print_object_idx = 0; print_object_idx < m_objects.size(); ++ print_object_idx) {
const PrintObject &print_object = *m_objects[print_object_idx];
double effective_max_z = 0;
@@ -2515,6 +2526,19 @@ void Print::process(long long *time_cost_with_cache, bool use_cache)
if (m_objects.empty())
return;
// Belt purge prism: _plan_belt_purge() (psWipeTower) truncates the prism's
// layers and drops its unclaimed fills, stashing both so a replan can undo
// them. The object steps below regenerate per-layer content over m_layers
// ONLY, so if any of them is about to rerun the stashes must go back first;
// otherwise truncated layers keep stale perimeters/fills and dropped fills
// are re-inserted next to freshly generated ones. Every object-step
// invalidation also invalidates psWipeTower, so "psWipeTower not done" is
// exactly "some object step may rerun" -- and when it IS done nothing below
// regenerates, and the plan's edits have to stay.
if (!this->is_step_done(psWipeTower))
for (PrintObject *obj : m_objects)
obj->belt_undo_purge_plan();
for (PrintObject *obj : m_objects)
obj->clear_shared_object();
+35
View File
@@ -359,6 +359,9 @@ public:
// Trafo with the center_offset() applied after the transformation, to center the object in XY before slicing.
Transform3d trafo_centered() const
{ Transform3d t = this->trafo(); t.pretranslate(Vec3d(- unscale<double>(m_center_offset.x()), - unscale<double>(m_center_offset.y()), 0)); return t; }
// trafo_centered() with the belt pre-slice transforms applied: the frame the layers were sliced in (Layer::slice_z).
// Equal to trafo_centered() unless a belt rotation or pre-slice remap is active.
Transform3d trafo_sliced() const;
const PrintInstances& instances() const { return m_instances; }
PrintInstances &instances() { return m_instances; }
@@ -582,6 +585,28 @@ private:
// Wipe-tower-only invalidations do not necessarily reslice the object, so
// truncation must be reversible when later toolchanges move upward.
void belt_restore_truncated_layers();
// Belt purge prism, plastic saving: drop the fills on one layer that no
// toolchange claimed. `claimed` reports whether an entity was overridden as
// purge; everything else on that layer would otherwise print as solid infill
// in the prism's own filament for nothing. Perimeters are never touched, so
// the bar keeps a continuous wall along the belt.
//
// Entities are STASHED, not deleted, with their original positions -- the
// same reversibility contract belt_truncate_layers_above() has, and the
// reason the original version of this had to be removed: psWipeTower can
// rerun without regenerating infill, and a later tool ordering may claim what
// this one did not. Returns the number of entities dropped.
size_t belt_drop_unclaimed_fills(Layer *layer, const std::function<bool(const ExtrusionEntity*)> &claimed);
// Put every stashed fill back at its original index. Must run before a replan.
void belt_restore_dropped_fills();
// Undo every edit _plan_belt_purge() made to this object's layers, leaving
// m_layers exactly as the object steps produced it. Fills first: they point
// into layers that are still live, and truncated layers were stashed whole
// with their own fills untouched, so the two stashes never share an entity.
// Print::process() calls this before any object step may rerun (those steps
// regenerate per-layer content over m_layers only, so a stale stash would
// otherwise be restored on top of fresh content); the plan calls it too.
void belt_undo_purge_plan() { belt_restore_dropped_fills(); belt_restore_truncated_layers(); }
//BBS
ExPolygons _shrink_contour_holes(double contour_delta, double hole_delta, const ExPolygons& polys) const;
// BBS
@@ -625,6 +650,16 @@ private:
SlicingParameters m_slicing_params;
LayerPtrs m_layers;
LayerPtrs m_belt_truncated_layers;
// Fills removed by belt_drop_unclaimed_fills(), owned by this vector until
// restored or until clear_layers() deletes them. An entity is in exactly one
// of the live collection or this stash, never both.
struct BeltDroppedFill {
Layer *layer { nullptr };
size_t region_idx { 0 };
size_t index { 0 }; // position in the original fills.entities
ExtrusionEntity *entity { nullptr };
};
std::vector<BeltDroppedFill> m_belt_dropped_fills;
SupportLayerPtrs m_support_layers;
// Belt brim, generated in posSupportMaterial by BeltBrim.cpp. Object-local
// slicing frame, one entry per object layer plus a prologue of brim-only
+11
View File
@@ -1241,6 +1241,17 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", i=%1%, key=%2%")%i %changed_keys[i];
}
}
// On belt printers the support tilt follows the slicing rotation. The GUI keeps the two in
// sync, but a CLI or 3MF edit of the rotation alone would otherwise leave supports on a stale tilt.
if (const auto *belt_opt = new_full_config.option<ConfigOptionBool>("belt_printer"); belt_opt && belt_opt->value) {
const auto *axis_opt = new_full_config.option<ConfigOptionEnum<BeltRotationAxis>>("belt_slice_rotation");
const auto *angle_opt = new_full_config.option<ConfigOptionFloat>("belt_slice_rotation_angle");
if (axis_opt && angle_opt) {
const auto tilt = BeltTransformPipeline::physical_tilt(axis_opt->value, angle_opt->value);
new_full_config.set_key_value("build_plate_tilt_x", new ConfigOptionFloat(tilt.tilt_x_deg));
new_full_config.set_key_value("build_plate_tilt_y", new ConfigOptionFloat(tilt.tilt_y_deg));
}
}
const ConfigOption* enable_support_option = new_full_config.option("enable_support");
if (enable_support_option && enable_support_option->getBool())
m_support_used = true;
+14 -13
View File
@@ -373,8 +373,6 @@ 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) },
{ "clip_only", int(BeltSupportFloorMode::ClipOnly) },
{ "both", int(BeltSupportFloorMode::Both) },
};
CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(BeltSupportFloorMode)
@@ -7163,8 +7161,8 @@ void PrintConfigDef::init_fff_params()
"A negative value tilts the -X side higher. Set to 0 for no X-axis tilt. "
"In belt printer mode, this is automatically synced to the belt angle.");
def->sidetext = u8"\u00B0";
def->min = -90;
def->max = 90;
def->min = -89;
def->max = 89;
def->mode = comExpert;
def->set_default_value(new ConfigOptionFloat(0.));
@@ -7175,8 +7173,8 @@ void PrintConfigDef::init_fff_params()
"A positive value tilts the plate so the +Y side is higher, shifting gravity toward -Y and increasing overhangs on the +Y side. "
"A negative value tilts the -Y side higher. Set to 0 for no Y-axis tilt.");
def->sidetext = u8"\u00B0";
def->min = -90;
def->max = 90;
def->min = -89;
def->max = 89;
def->mode = comExpert;
def->set_default_value(new ConfigOptionFloat(0.));
@@ -7281,19 +7279,19 @@ void PrintConfigDef::init_fff_params()
"your belt printer's physical bed plane. For a printer whose bed is in the XZ plane, "
"set Y to +Z and Z to +Y (or -Y) to swap the vertical and belt-travel axes. "
"Default +X: no change.",
RemapAxis::PosX, comExpert);
RemapAxis::PosX, comDevelop);
add_belt_remap("preslice_remap_y", "Y",
"Before slicing, which model-space axis becomes the slicer's Y axis. "
"The slicer treats Y as one of the two horizontal bed axes. If your physical "
"belt surface runs along the Z axis, map Y to +Z here so the slicer slices "
"along the correct plane. Default +Y: no change.",
RemapAxis::PosY, comExpert);
RemapAxis::PosY, comDevelop);
add_belt_remap("preslice_remap_z", "Z",
"Before slicing, which model-space axis becomes the slicer's Z axis (layer stacking direction). "
"The slicer builds layers upward along this axis. If your printer's layer-stacking "
"direction is the physical Y axis, map Z to +Y (or -Y for inverted direction). "
"Rev mode mirrors relative to the build volume maximum. Default +Z: no change.",
RemapAxis::PosZ, comExpert);
RemapAxis::PosZ, comDevelop);
def = this->add("preslice_remap_global", coBool);
def->label = L("Global");
@@ -7302,12 +7300,12 @@ void PrintConfigDef::init_fff_params()
"Without this, the remap is applied locally around each object's center, so "
"objects at different positions don't get a position-dependent contribution. "
"Mirrors the 'Global' option on the belt slicing rotation, but for the remap.");
def->mode = comExpert;
def->mode = comDevelop;
def->set_default_value(new ConfigOptionBool(false));
add_belt_remap("gcode_remap_x", "X", "Which slicing axis maps to machine X in G-code output. Applied AFTER slicing, during G-code generation.", RemapAxis::PosX, comExpert);
add_belt_remap("gcode_remap_y", "Y", "Which slicing axis maps to machine Y in G-code output. Applied AFTER slicing, during G-code generation.", RemapAxis::PosY, comExpert);
add_belt_remap("gcode_remap_z", "Z", "Which slicing axis maps to machine Z in G-code output. Applied AFTER slicing, during G-code generation.", RemapAxis::PosZ, comExpert);
add_belt_remap("gcode_remap_x", "X", "Which slicing axis maps to machine X in G-code output. Applied AFTER slicing, during G-code generation.", RemapAxis::PosX, comDevelop);
add_belt_remap("gcode_remap_y", "Y", "Which slicing axis maps to machine Y in G-code output. Applied AFTER slicing, during G-code generation.", RemapAxis::PosY, comDevelop);
add_belt_remap("gcode_remap_z", "Z", "Which slicing axis maps to machine Z in G-code output. Applied AFTER slicing, during G-code generation.", RemapAxis::PosZ, comDevelop);
// The machine-frame G-code transform (shear + scale) is no longer configured
// by per-axis keys: it is derived from the belt tilt (belt_slice_rotation axis
@@ -9354,6 +9352,9 @@ 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") {
-2
View File
@@ -275,8 +275,6 @@ enum class BeltSupportFloorMode
{
None, // No belt floor awareness
GeneratorOnly, // Only in tree support drop_nodes/contact_points
ClipOnly, // Only post-processing clipping
Both, // Both generator and clipping
};
enum class BeltSupportZOffsetMode
+22 -77
View File
@@ -4,7 +4,6 @@
#include "Print.hpp"
#include "BeltTransform.hpp"
#include <thread>
#include "BoundingBox.hpp"
#include "ClipperUtils.hpp"
#include "Clipper2Utils.hpp"
@@ -460,15 +459,11 @@ std::vector<std::set<int>> PrintObject::detect_extruder_geometric_unprintables()
// 3) Generates perimeters, gap fills and fill regions (fill regions of type stInternal).
void PrintObject::make_perimeters()
{
BOOST_LOG_TRIVIAL(trace) << "[BELTRACE] make_perimeters request tid=" << std::this_thread::get_id() << " obj=" << this;
// prerequisites
this->slice();
if (! this->set_started(posPerimeters)) {
BOOST_LOG_TRIVIAL(trace) << "[BELTRACE] make_perimeters SKIP tid=" << std::this_thread::get_id() << " obj=" << this << " (already started/done)";
if (! this->set_started(posPerimeters))
return;
}
BOOST_LOG_TRIVIAL(trace) << "[BELTRACE] make_perimeters ENTER tid=" << std::this_thread::get_id() << " obj=" << this;
m_print->set_status(15, L("Generating walls"));
BOOST_LOG_TRIVIAL(info) << "Generating walls..." << log_memory_info();
@@ -566,7 +561,6 @@ void PrintObject::make_perimeters()
m_print->throw_if_canceled();
BOOST_LOG_TRIVIAL(debug) << "Generating perimeters in parallel - end";
BOOST_LOG_TRIVIAL(trace) << "[BELTRACE] make_perimeters EXIT tid=" << std::this_thread::get_id() << " obj=" << this;
this->set_done(posPerimeters);
}
@@ -955,9 +949,7 @@ void PrintObject::detect_overhangs_for_lift()
void PrintObject::generate_support_material()
{
BOOST_LOG_TRIVIAL(trace) << "[BELTRACE] generate_support_material request tid=" << std::this_thread::get_id() << " obj=" << this;
if (this->set_started(posSupportMaterial)) {
BOOST_LOG_TRIVIAL(trace) << "[BELTRACE] generate_support_material ENTER tid=" << std::this_thread::get_id() << " obj=" << this;
this->clear_support_layers();
if(!has_support() && !m_print->get_no_check_flag()) {
@@ -1005,10 +997,7 @@ void PrintObject::generate_support_material()
// posSupportMaterial, so this needs no extra invalidation edges.
make_belt_brim(*this);
m_print->throw_if_canceled();
BOOST_LOG_TRIVIAL(trace) << "[BELTRACE] generate_support_material EXIT tid=" << std::this_thread::get_id() << " obj=" << this;
this->set_done(posSupportMaterial);
} else {
BOOST_LOG_TRIVIAL(trace) << "[BELTRACE] generate_support_material SKIP tid=" << std::this_thread::get_id() << " obj=" << this << " (already started/done)";
}
}
@@ -1101,7 +1090,10 @@ std::pair<FillAdaptive::OctreePtr, FillAdaptive::OctreePtr> PrintObject::prepare
indexed_triangle_set mesh = this->model_object()->raw_indexed_triangle_set();
// Rotate mesh and build octree on it with axis-aligned (standart base) cubes.
auto to_octree = transform_to_octree().toRotationMatrix();
its_transform(mesh, to_octree * this->trafo_centered(), true);
// Overhangs below are placed at Layer::bottom_z(), which includes the belt global Z offset.
Transform3d object_trafo = this->trafo_sliced();
object_trafo.translation().z() += m_belt_global_z_offset;
its_transform(mesh, to_octree * object_trafo, true);
// Triangulate internal bridging surfaces.
std::vector<std::vector<Vec3d>> overhangs(std::max(surfaces_w_bottom_z.size(), size_t(1)));
@@ -1148,6 +1140,13 @@ void PrintObject::clear_layers()
for (Layer *l : m_belt_truncated_layers)
delete l;
m_belt_truncated_layers.clear();
// Fills dropped for plastic saving are owned by the stash while they sit
// outside their layer's collection, so they are freed here too. Order
// matters only in that these point at layers deleted just above, and we
// never dereference the layer -- just the entity.
for (const BeltDroppedFill &d : m_belt_dropped_fills)
delete d.entity;
m_belt_dropped_fills.clear();
}
}
@@ -1199,6 +1198,10 @@ bool PrintObject::has_belt_brim() const
{
if (! m_print->has_tilted_belt())
return false;
// The purge prism is sacrificial and sits at the plate's edge; its generator sets no_brim, and
// 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)
@@ -1246,7 +1249,9 @@ bool PrintObject::belt_brim_instances_compatible() const
// matters for configurations that do not.
if (m_instances.size() <= 1)
return true;
const int axis = m_slicing_params.belt_floor_from_axis;
// 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();
@@ -4684,67 +4689,6 @@ void PrintObject::combine_infill()
}
}
// Belt printer: clip an ExtrusionEntityCollection to a region defined by clip_expoly.
// Handles ExtrusionPath, ExtrusionMultiPath, ExtrusionLoop, and nested ExtrusionEntityCollection.
static void clip_support_fills(ExtrusionEntityCollection &fills, const ExPolygons &clip_region)
{
ExtrusionEntitiesPtr new_entities;
for (ExtrusionEntity *entity : fills.entities) {
if (auto *path = dynamic_cast<ExtrusionPath *>(entity)) {
ExtrusionEntityCollection clipped;
path->intersect_expolygons(clip_region, &clipped);
if (!clipped.empty()) {
for (ExtrusionEntity *e : clipped.entities)
new_entities.push_back(e->clone());
}
delete entity;
} else if (auto *multipath = dynamic_cast<ExtrusionMultiPath *>(entity)) {
ExtrusionPaths new_paths;
for (const ExtrusionPath &p : multipath->paths) {
ExtrusionEntityCollection clipped;
p.intersect_expolygons(clip_region, &clipped);
for (ExtrusionEntity *e : clipped.entities)
if (auto *cp = dynamic_cast<ExtrusionPath *>(e))
new_paths.push_back(std::move(*cp));
}
if (!new_paths.empty()) {
multipath->paths = std::move(new_paths);
new_entities.push_back(multipath);
} else {
delete entity;
}
} else if (auto *loop = dynamic_cast<ExtrusionLoop *>(entity)) {
ExtrusionPaths new_paths;
for (const ExtrusionPath &p : loop->paths) {
ExtrusionEntityCollection clipped;
p.intersect_expolygons(clip_region, &clipped);
for (ExtrusionEntity *e : clipped.entities)
if (auto *cp = dynamic_cast<ExtrusionPath *>(e))
new_paths.push_back(std::move(*cp));
}
if (!new_paths.empty()) {
// Loop is no longer a closed loop after clipping; emit as individual paths.
for (auto &p : new_paths)
new_entities.push_back(new ExtrusionPath(std::move(p)));
delete entity;
} else {
delete entity;
}
} else if (auto *coll = dynamic_cast<ExtrusionEntityCollection *>(entity)) {
clip_support_fills(*coll, clip_region);
if (!coll->empty()) {
new_entities.push_back(coll);
} else {
delete entity;
}
} else {
// Unknown entity type — keep as-is.
new_entities.push_back(entity);
}
}
fills.entities = std::move(new_entities);
}
void PrintObject::_generate_support_material()
{
if (is_tree(m_config.support_type.value)) {
@@ -5124,6 +5068,7 @@ static void project_triangles_to_slabs(ConstLayerPtrsAdaptor layers, const index
void PrintObject::project_and_append_custom_facets(
bool seam, EnforcerBlockerType type, std::vector<Polygons>& out, std::vector<std::pair<Vec3f, Vec3f>>* vertical_points) const
{
const Transform3d object_trafo = this->trafo_sliced();
for (const ModelVolume* mv : this->model_object()->volumes)
if (mv->is_model_part()) {
const indexed_triangle_set custom_facets = seam
@@ -5132,12 +5077,12 @@ void PrintObject::project_and_append_custom_facets(
if (! custom_facets.indices.empty()) {
if (seam)
project_triangles_to_slabs(this->layers(), custom_facets,
(this->trafo_centered() * mv->get_matrix()).cast<float>(),
(object_trafo * mv->get_matrix()).cast<float>(),
seam, out);
else {
std::vector<Polygons> projected;
// Support blockers or enforcers. Project downward facing painted areas upwards to their respective slicing plane.
slice_mesh_slabs(custom_facets, zs_from_layers(this->layers()), this->trafo_centered() * mv->get_matrix(), nullptr, &projected, vertical_points, [](){});
slice_mesh_slabs(custom_facets, zs_from_layers(this->layers()), object_trafo * mv->get_matrix(), nullptr, &projected, vertical_points, [](){});
// Merge these projections with the output, layer by layer.
assert(! projected.empty());
assert(out.empty() || out.size() == projected.size());
+9 -66
View File
@@ -1,6 +1,5 @@
#include <boost/log/trivial.hpp>
#include <limits>
#include <thread>
#include <tbb/parallel_for.h>
@@ -860,12 +859,8 @@ void groupingVolumesForBrim(PrintObject* object, LayerPtrs& layers, int firstLay
// Resulting expolygons of layer regions are marked as Internal.
void PrintObject::slice()
{
BOOST_LOG_TRIVIAL(trace) << "[BELTRACE] slice request tid=" << std::this_thread::get_id() << " obj=" << this;
if (! this->set_started(posSlice)) {
BOOST_LOG_TRIVIAL(trace) << "[BELTRACE] slice SKIP tid=" << std::this_thread::get_id() << " obj=" << this << " (already started/done)";
if (! this->set_started(posSlice))
return;
}
BOOST_LOG_TRIVIAL(trace) << "[BELTRACE] slice ENTER tid=" << std::this_thread::get_id() << " obj=" << this;
//BBS: add flag to reload scene for shell rendering
m_print->set_status(5, L("Slicing mesh"), PrintBase::SlicingStatus::RELOAD_SCENE);
std::vector<coordf_t> layer_height_profile;
@@ -1000,41 +995,6 @@ void PrintObject::slice()
0.);
double centering_z_corr = (T_fwd.linear() * c_off).z();
global_z_offset += centering_z_corr;
BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] centering correction"
<< " obj=" << this->model_object()->name
<< " m_center_offset_mm=(" << c_off.x() << "," << c_off.y() << ")"
<< " centering_z_corr=" << centering_z_corr
<< " (added to global_z_offset)";
}
// [BELT-DEBUG] Per-object summary so Case A vs Case B can be compared
// side-by-side. Lays out every value that feeds into the final layer
// print_z adjustment.
{
BoundingBoxf3 raw_bb = this->model_object()->raw_bounding_box();
BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] slice() per-object summary"
<< " obj=" << this->model_object()->name
<< " n_volumes=" << this->model_object()->volumes.size()
<< " raw_bbox.min=(" << raw_bb.min.x() << "," << raw_bb.min.y() << "," << raw_bb.min.z() << ")"
<< " raw_bbox.max=(" << raw_bb.max.x() << "," << raw_bb.max.y() << "," << raw_bb.max.z() << ")"
<< " raw_bbox.center=(" << raw_bb.center().x() << "," << raw_bb.center().y() << ")"
<< " m_center_offset=(" << unscale<double>(m_center_offset.x()) << "," << unscale<double>(m_center_offset.y()) << ")"
<< " inst_shift=(" << unscale<double>(inst_shift.x()) << "," << unscale<double>(inst_shift.y()) << ")"
<< " m_belt_min_z=" << m_belt_min_z
<< " belt_surface_z=" << belt_surface_z
<< " belt_z_shift=" << belt_z_shift;
// Per-volume bbox + get_matrix translation so order/composition is visible.
int vi = 0;
for (const ModelVolume *mv : this->model_object()->volumes) {
if (!mv->is_model_part()) { ++vi; continue; }
BoundingBoxf3 vol_bb = mv->mesh().transformed_bounding_box(mv->get_matrix());
BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] vol[" << vi
<< "] id=" << mv->id().id << " name='" << mv->name << "'"
<< " get_matrix.translation=(" << mv->get_matrix().translation().x() << "," << mv->get_matrix().translation().y() << "," << mv->get_matrix().translation().z() << ")"
<< " object_bbox.min=(" << vol_bb.min.x() << "," << vol_bb.min.y() << "," << vol_bb.min.z() << ")"
<< " object_bbox.max=(" << vol_bb.max.x() << "," << vol_bb.max.y() << "," << vol_bb.max.z() << ")";
++vi;
}
}
if (pcfg.belt_preslice_global.value) {
@@ -1044,9 +1004,6 @@ void PrintObject::slice()
Vec3d d(unscale<double>(inst_shift.x()), unscale<double>(inst_shift.y()), 0.);
Vec3d c = T.linear() * d - d;
global_z_offset += c.z();
BOOST_LOG_TRIVIAL(trace) << "[BELTRACE] write m_belt_global_xy_correction tid=" << std::this_thread::get_id()
<< " obj=" << this << " old=(" << m_belt_global_xy_correction.x() << "," << m_belt_global_xy_correction.y()
<< ") new=(" << c.x() << "," << c.y() << ")";
m_belt_global_xy_correction = Vec2d(c.x(), c.y());
BOOST_LOG_TRIVIAL(trace) << "Belt preslice_global: correction=("
@@ -1081,18 +1038,7 @@ void PrintObject::slice()
BOOST_LOG_TRIVIAL(trace) << "Belt global: z_offset=" << global_z_offset
<< " (relative to min across " << this->print()->objects().size() << " objects)";
BOOST_LOG_TRIVIAL(trace) << "[BELTRACE] write m_belt_global_z_offset tid=" << std::this_thread::get_id()
<< " obj=" << this << " old=" << m_belt_global_z_offset << " new=" << global_z_offset;
m_belt_global_z_offset = global_z_offset;
// [BELT-DEBUG] Final breakdown of all contributions to layer.print_z
// and where the first / last layer end up post-adjustment.
BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] global_z_offset breakdown"
<< " obj=" << this->model_object()->name
<< " belt_z_shift=" << belt_z_shift
<< " total_global_z_offset=" << global_z_offset
<< " xy_correction=(" << m_belt_global_xy_correction.x() << "," << m_belt_global_xy_correction.y() << ")"
<< " belt_floor_z_shift_before=" << (m_slicing_params.belt_floor_z_shift)
<< " n_layers=" << m_layers.size();
if (std::abs(global_z_offset) > EPSILON) {
for (Layer *layer : m_layers)
layer->print_z += global_z_offset;
@@ -1101,12 +1047,6 @@ void PrintObject::slice()
// layer print_z, so belt_floor_z_shift must match.
m_slicing_params.belt_floor_z_shift += global_z_offset;
}
if (!m_layers.empty()) {
BOOST_LOG_TRIVIAL(trace) << "[BELT-DEBUG] post-adjustment"
<< " first_layer.print_z=" << m_layers.front()->print_z
<< " last_layer.print_z=" << m_layers.back()->print_z
<< " belt_floor_z_shift_after=" << m_slicing_params.belt_floor_z_shift;
}
if (!m_layers.empty()) {
BOOST_LOG_TRIVIAL(trace) << "Belt global: first_layer_z=" << m_layers.front()->print_z
<< " last_layer_z=" << m_layers.back()->print_z
@@ -1125,10 +1065,6 @@ void PrintObject::slice()
}
// BBS
BOOST_LOG_TRIVIAL(trace) << "[BELTRACE] slice EXIT tid=" << std::this_thread::get_id() << " obj=" << this
<< " layers=" << m_layers.size() << " belt_min_z=" << m_belt_min_z
<< " belt_global_z_offset=" << m_belt_global_z_offset
<< " belt_xy=(" << m_belt_global_xy_correction.x() << "," << m_belt_global_xy_correction.y() << ")";
this->set_done(posSlice);
}
@@ -1806,6 +1742,13 @@ ExPolygons PrintObject::_shrink_contour_holes(double contour_delta, double hole_
return union_ex(new_ex_polys);
}
Transform3d PrintObject::trafo_sliced() const
{
Transform3d trafo = this->trafo_centered();
BeltSliceStrategy::apply_preslice_transforms(trafo, this->print()->config(), this->model_object()->volumes);
return trafo;
}
std::vector<Polygons> PrintObject::slice_support_volumes(const ModelVolumeType model_volume_type) const
{
auto it_volume = this->model_object()->volumes.begin();
@@ -1820,7 +1763,7 @@ std::vector<Polygons> PrintObject::slice_support_volumes(const ModelVolumeType m
const Print *print = this->print();
auto throw_on_cancel_callback = std::function<void()>([print](){ print->throw_if_canceled(); });
MeshSlicingParamsEx params;
params.trafo = this->trafo_centered();
params.trafo = this->trafo_sliced();
for (; it_volume != it_volume_end; ++ it_volume)
if ((*it_volume)->type() == model_volume_type) {
std::vector<ExPolygons> slices2 = slice_volume(*(*it_volume), zs, params, throw_on_cancel_callback);
+6
View File
@@ -2069,4 +2069,10 @@ sub clip_with_shape {
}
*/
Vec2d build_plate_tilt_slope(const PrintConfig &print_config)
{
auto slope = [](double tilt_deg) { return std::tan(Geometry::deg2rad(std::clamp(tilt_deg, -89., 89.))); };
return { slope(print_config.build_plate_tilt_y.value), slope(print_config.build_plate_tilt_x.value) };
}
} // namespace Slic3r
+4
View File
@@ -150,6 +150,10 @@ Polygons belt_floor_surface_polygon(
const SlicingParameters &slicing_params, const PrintConfig &print_config,
const PrintObject &object, coordf_t print_z);
// Build plate tilt: XY drift of gravity per unit of layer height, zero on a level plate.
// The tilt is capped below 90 degrees to keep the drift finite.
Vec2d build_plate_tilt_slope(const PrintConfig &print_config);
} // namespace Slic3r
#endif /* slic3r_SupportCommon_hpp_ */
+3 -7
View File
@@ -1438,9 +1438,8 @@ static inline ExPolygons detect_overhangs(
const bool bridge_no_support = object_config.bridge_no_support.value;
const coordf_t xy_expansion = scale_(object_config.support_expansion.value);
// Build plate tilt: compute per-layer XY shift for tilted gravity direction
const double tilt_x_rad = Geometry::deg2rad(print_config.build_plate_tilt_x.value);
const double tilt_y_rad = Geometry::deg2rad(print_config.build_plate_tilt_y.value);
const bool has_tilt = std::abs(tilt_x_rad) > EPSILON || std::abs(tilt_y_rad) > EPSILON;
const Vec2d tilt_slope = build_plate_tilt_slope(print_config);
const bool has_tilt = tilt_slope.cwiseAbs().maxCoeff() > EPSILON;
float lower_layer_offset = 0;
if (layer_id == 0)
@@ -1480,10 +1479,7 @@ static inline ExPolygons detect_overhangs(
Polygons tilted_lower;
if (has_tilt) {
tilted_lower = lower_layer_polygons;
const double lh = lower_layer.height;
Point tilt_shift(coord_t(scale_(lh * tan(tilt_y_rad))),
coord_t(scale_(lh * tan(tilt_x_rad))));
translate(tilted_lower, tilt_shift);
translate(tilted_lower, Point::new_scale(tilt_slope * lower_layer.height));
effective_lower = &tilted_lower;
}
+32 -23
View File
@@ -95,29 +95,6 @@ TreeModelVolumes::TreeModelVolumes(
#else
{
m_anti_overhang = print_object.slice_support_blockers();
// Belt floor: add belt surface polygons to anti_overhang so support
// is never generated inside the belt. Only in global shear mode —
// in local mode the belt floor clipping handles everything and
// anti_overhang at the bottom layers would block all support.
{
const auto &sp = print_object.slicing_parameters();
const auto &pcfg = print_object.print()->config();
BeltFloorContext ctx;
ctx.init_local(sp, pcfg, print_object.belt_global_z_offset());
if (ctx.is_active()
&& std::abs(print_object.belt_global_z_offset()) > EPSILON
&& pcfg.belt_support_floor_mode.value == BeltSupportFloorMode::GeneratorOnly) {
size_t num_layers_needed = print_object.layer_count();
// Ensure m_anti_overhang is large enough.
if (m_anti_overhang.size() < num_layers_needed)
m_anti_overhang.resize(num_layers_needed, Polygons{});
for (size_t layer_idx = 0; layer_idx < num_layers_needed; ++layer_idx) {
double print_z = print_object.get_layer(layer_idx)->print_z
- print_object.belt_global_z_offset();
append(m_anti_overhang[layer_idx], ctx.surface_polygon(print_z));
}
}
}
TreeSupportMeshGroupSettings mesh_settings(print_object);
const TreeSupportSettings config{ mesh_settings, print_object.slicing_parameters() };
m_current_min_xy_dist = config.xy_min_distance;
@@ -147,6 +124,38 @@ TreeModelVolumes::TreeModelVolumes(
}
}
}
// Belt floor: add belt surface polygons to anti_overhang so support is
// never generated inside the belt.
//
// This MUST run after m_raft_layers is final. m_anti_overhang is consumed
// in the same index space as m_layer_outlines -- object layer i lives at
// index num_raft_layers + i -- but slice_support_blockers() returns it in
// object-layer space. Without the shift below, every entry lands
// num_raft_layers too low: with the belt raft that is tens of layers, so
// the belt suppression is applied to the wrong layers entirely and the
// topmost object layers get none at all.
{
const size_t num_raft = m_raft_layers.size();
const size_t num_obj = print_object.layer_count();
if (num_raft > 0 && ! m_anti_overhang.empty())
// Shift the support blockers into the same space.
m_anti_overhang.insert(m_anti_overhang.begin(), num_raft, Polygons{});
const auto &sp = print_object.slicing_parameters();
const auto &pcfg = print_object.print()->config();
BeltFloorContext ctx;
ctx.init_local(sp, pcfg, print_object.belt_global_z_offset());
if (ctx.is_active()
&& std::abs(print_object.belt_global_z_offset()) > EPSILON
&& pcfg.belt_support_floor_mode.value == BeltSupportFloorMode::GeneratorOnly) {
if (m_anti_overhang.size() < num_raft + num_obj)
m_anti_overhang.resize(num_raft + num_obj, Polygons{});
for (size_t i = 0; i < num_obj; ++i) {
const double print_z = print_object.get_layer(i)->print_z
- print_object.belt_global_z_offset();
append(m_anti_overhang[num_raft + i], ctx.surface_polygon(print_z));
}
}
}
m_current_outline_idx = 0;
m_layer_outlines.emplace_back(mesh_settings, std::vector<Polygons>{});
+71 -11
View File
@@ -709,9 +709,19 @@ void TreeSupport::detect_overhangs(bool check_support_necessity/* = false*/)
const double threshold_rad = Geometry::deg2rad(thresh_angle);
// Build plate tilt: compute per-layer XY shift for tilted gravity direction
const PrintConfig& print_cfg = m_object->print()->config();
const double tilt_x_rad = Geometry::deg2rad(print_cfg.build_plate_tilt_x.value);
const double tilt_y_rad = Geometry::deg2rad(print_cfg.build_plate_tilt_y.value);
const bool has_tilt = std::abs(tilt_x_rad) > EPSILON || std::abs(tilt_y_rad) > EPSILON;
const Vec2d tilt_slope = build_plate_tilt_slope(print_cfg);
const bool has_tilt = tilt_slope.cwiseAbs().maxCoeff() > EPSILON;
// Belt printers: the object is pre-rotated by the belt angle before slicing, so a wall
// that is vertical in the world advances by one layer height per layer in the sliced
// frame. The build-plate tilt shift above compensates for that, but its direction has to
// follow the belt shear -- the sign and axis are already known exactly from the slicing
// parameters, so take them from there rather than from tan(build_plate_tilt), which
// carries a magnitude but no direction. Non-belt tilted beds keep the existing behaviour.
BeltFloorContext ovh_belt_ctx;
const bool belt_ovh_active = ovh_belt_ctx.init(m_slicing_params, print_cfg);
const double belt_shear = ovh_belt_ctx.shear_factor();
const int belt_axis = ovh_belt_ctx.from_axis();
// FIXME this is a fudge constant!
double support_tree_tip_diameter = 0.8;
auto enforcer_overhang_offset = scaled<double>(support_tree_tip_diameter);
@@ -855,15 +865,60 @@ void TreeSupport::detect_overhangs(bool check_support_necessity/* = false*/)
ExPolygons& lower_polys = lower_layer->lslices_extrudable;
// Apply build plate tilt: shift lower layer polygons to simulate tilted gravity
//
// On a belt the object's very first slice can come out empty (the bottom
// vertex is a sub-extrudable sliver), leaving the layer above it with an
// empty predecessor even though it rests on the belt. That case needs no
// special handling here: the belt surface is unioned into effective_lower
// below and sampled at the bottom of the layer, so a contacting island is
// covered and a genuinely floating one still reports its overhang. Doing it
// that way keeps the decision per-island -- an earlier whole-layer skip,
// conditioned on the nearest point of the *union* of the cross-section,
// let one contacting island silence a separate floating one.
ExPolygons shifted_lower;
if (has_tilt) {
if (belt_ovh_active || has_tilt) {
shifted_lower = lower_polys; // copy
const double lh = lower_layer->height;
Point tilt_shift(coord_t(scale_(lh * tan(tilt_y_rad))),
coord_t(scale_(lh * tan(tilt_x_rad))));
Point tilt_shift(0, 0);
if (belt_ovh_active) {
// Advance the lower layer along the belt by exactly the amount a
// world-vertical wall moves per layer, so such a wall stops reading
// as an overhang. Sign comes from the shear, not from a tilt angle.
const coord_t d = coord_t(-scale_(lh * belt_shear));
if (belt_axis == 0) tilt_shift.x() = d; else tilt_shift.y() = d;
} else {
tilt_shift = Point::new_scale(tilt_slope * lh);
}
translate(shifted_lower, tilt_shift);
}
const ExPolygons &effective_lower = has_tilt ? shifted_lower : lower_polys;
ExPolygons effective_lower = (belt_ovh_active || has_tilt) ? shifted_lower : lower_polys;
// Belt printers: material resting on the belt is held up by the belt, not by
// the layer below it, so the belt surface counts as support from underneath.
// Without this the object's belt-contact face reads as a fresh overhang on
// every layer -- the leading strip that produced the spurious support nub.
if (belt_ovh_active) {
// surface_polygon() is a +/-1000mm half-plane. Unioning that raw with
// 20mm-scale geometry and then offsetting it puts a huge dynamic range
// through Clipper, which left intermittent artefacts every few layers.
// Clip it to the layer's own bounding box first.
// Evaluate the belt surface at the BOTTOM of the layer, not its top:
// a layer meets the belt across its whole thickness, and print_z is the
// top. On the object's first layer -- which is thicker, and whose lower
// layer is empty -- using print_z left the leading 0.37mm uncovered and
// produced the one remaining spurious overhang.
Polygons belt_surface = ovh_belt_ctx.surface_polygon(layer->print_z - layer->height);
if (! belt_surface.empty()) {
BoundingBox clip_bb = get_extents(curr_polys);
clip_bb.merge(get_extents(lower_polys));
clip_bb.offset(scale_(10.));
belt_surface = intersection(belt_surface, Polygons{ clip_bb.polygon() });
if (! belt_surface.empty()) {
append(effective_lower, union_ex(belt_surface));
effective_lower = union_ex(effective_lower);
}
}
}
// normal overhang
ExPolygons lower_layer_offseted = offset_ex(effective_lower, support_offset_scaled, SUPPORT_SURFACES_OFFSET_PARAMETERS);
@@ -882,8 +937,13 @@ void TreeSupport::detect_overhangs(bool check_support_necessity/* = false*/)
for (const ExPolygon& expoly : curr_polys) {
bool is_sharp_tail = false;
// 1. nothing below
// this is a sharp tail region if it's floating and non-ignorable
if (!overlaps(offset_ex(expoly, 0.1 * extrusion_width_scaled), lower_polys)) {
// this is a sharp tail region if it's floating and non-ignorable.
// On a belt, "below" has to include the belt itself and the
// shear-advanced lower layer, or every belt-contact island reads as
// a sharp tail -- which is what the empty-predecessor skip above was
// really masking. effective_lower is exactly that notion of below.
const ExPolygons &tail_lower = belt_ovh_active ? effective_lower : lower_polys;
if (!overlaps(offset_ex(expoly, 0.1 * extrusion_width_scaled), tail_lower)) {
is_sharp_tail = !offset_ex(expoly, -0.1 * extrusion_width_scaled).empty();
}
@@ -1853,8 +1913,6 @@ void TreeSupport::generate()
if (first_layer != nullptr) {
ExPolygons floating = diff_ex(first_layer->lslices_extrudable,
ctx.surface_polygon(first_layer->bottom_z() - first_layer->height));
BOOST_LOG_TRIVIAL(debug) << "[BELT-CALIB] wedge seed: obj=" << m_object->model_object()->name
<< " bottom_z=" << first_layer->bottom_z() << " floating=" << floating.size();
if (!floating.empty()) {
source_areas = std::move(floating);
first_z = first_layer->bottom_z();
@@ -1917,6 +1975,8 @@ 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());
for (size_t i = 0; i < sl_vec.size(); ++i)
sl_vec[i]->set_id(i);
}
}
}
+4 -8
View File
@@ -211,9 +211,8 @@ static std::vector<std::pair<TreeSupportSettings, std::vector<size_t>>> group_me
// +1 makes the threshold inclusive
double tan_threshold = support_threshold_auto ? 0. : tan(M_PI * double(support_threshold + 1) / 180.);
// Build plate tilt: compute per-layer XY shift for tilted gravity direction
const double tilt_x_rad = Geometry::deg2rad(print_config.build_plate_tilt_x.value);
const double tilt_y_rad = Geometry::deg2rad(print_config.build_plate_tilt_y.value);
const bool has_tilt = std::abs(tilt_x_rad) > EPSILON || std::abs(tilt_y_rad) > EPSILON;
const Vec2d tilt_slope = build_plate_tilt_slope(print_config);
const bool has_tilt = tilt_slope.cwiseAbs().maxCoeff() > EPSILON;
//FIXME this is a fudge constant!
auto enforcer_overhang_offset = scaled<double>(config.tree_support_tip_diameter.value);
const coordf_t radius_sample_resolution = g_config_tree_support_collision_resolution;
@@ -235,7 +234,7 @@ static std::vector<std::pair<TreeSupportSettings, std::vector<size_t>>> group_me
size_t num_overhang_layers = support_auto ? num_object_layers : std::min(num_object_layers, std::max(size_t(support_enforce_layers), enforcers_layers.size()));
tbb::parallel_for(tbb::blocked_range<LayerIndex>(1, num_overhang_layers),
[&print_object, &config, &print_config, &enforcers_layers, &blockers_layers,
support_auto, support_enforce_layers, support_threshold_auto, tan_threshold, enforcer_overhang_offset, num_raft_layers, radius_sample_resolution, has_tilt, tilt_x_rad, tilt_y_rad, &throw_on_cancel, &out]
support_auto, support_enforce_layers, support_threshold_auto, tan_threshold, enforcer_overhang_offset, num_raft_layers, radius_sample_resolution, has_tilt, tilt_slope, &throw_on_cancel, &out]
(const tbb::blocked_range<LayerIndex> &range) {
for (LayerIndex layer_id = range.begin(); layer_id < range.end(); ++ layer_id) {
const Layer &current_layer = *print_object.get_layer(layer_id);
@@ -263,10 +262,7 @@ static std::vector<std::pair<TreeSupportSettings, std::vector<size_t>>> group_me
Polygons lower_layer_offseted;
if (has_tilt) {
Polygons lower_src = to_polygons(lower_layer.lslices_extrudable);
const double lh = lower_layer.height;
Point tilt_shift(coord_t(scale_(lh * tan(tilt_y_rad))),
coord_t(scale_(lh * tan(tilt_x_rad))));
translate(lower_src, tilt_shift);
translate(lower_src, Point::new_scale(tilt_slope * lower_layer.height));
lower_layer_offseted = offset(lower_src, lower_layer_offset);
} else {
lower_layer_offseted = offset(lower_layer.lslices_extrudable, lower_layer_offset);
+16 -1
View File
@@ -651,7 +651,22 @@ inline SupportGeneratorLayer& layer_initialize(
const size_t layer_idx)
{
layer_new.print_z = layer_z(slicing_params, config, layer_idx);
layer_new.bottom_z = layer_idx > 0 ? layer_z(slicing_params, config, layer_idx - 1) : 0;
// Layer 0 has no layer below it, so its bottom is the build plate at z = 0 --
// true for a flat bed, false for a belt, whose virtual support layers extend
// below zero. Taking 0 there made the bottom-most belt layer's height come out
// as its own (negative) print_z, which reached Flow::with_height() and threw
// FlowErrorNegativeFlow, so tree support could not slice any belt model whose
// branches reached down that far.
//
// Only the negative case is corrected. An earlier version used
// min(0, print_z - layer_height), which also fires whenever the initial layer
// is THINNER than the regular layer height -- e.g. 0.2 over 0.3, both
// independently configurable -- and silently changed flat-bed support layer
// heights. Keying on the sign leaves every non-negative print_z on exactly
// the previous value of 0.
layer_new.bottom_z = layer_idx > 0 ? layer_z(slicing_params, config, layer_idx - 1) : 0.;
if (layer_idx == 0 && layer_new.print_z < 0.)
layer_new.bottom_z = layer_new.print_z - slicing_params.layer_height;
layer_new.height = layer_new.print_z - layer_new.bottom_z;
return layer_new;
}
+1 -1
View File
@@ -367,7 +367,7 @@ bool TriangleSelector::is_facet_clipped(int facet_idx, const ClippingPlane &clp)
void TriangleSelector::seed_fill_select_triangles(const Vec3f &hit, int facet_start, const Transform3d& trafo_no_translate,
const ClippingPlane &clp, float seed_fill_angle, float highlight_by_angle_deg,
bool force_reselection, const Vec3f &up_direction)
const Vec3f &up_direction, bool force_reselection)
{
assert(facet_start < m_orig_size_indices);
+2 -2
View File
@@ -336,8 +336,8 @@ public:
const ClippingPlane &clp, // Clipping plane to limit painting to not clipped facets only
float seed_fill_angle, // the maximal angle between two facets to be painted by the same color
float highlight_by_angle_deg = 0.f, // The maximal angle of overhang. If it is set to a non-zero value, it is possible to paint only the triangles of overhang defined by this angle in degrees.
bool force_reselection = false, // force reselection of the triangle mesh even in cases that mouse is pointing on the selected triangle
const Vec3f &up_direction = Vec3f::UnitZ()); // Up direction for overhang detection (accounts for build plate tilt)
const Vec3f &up_direction = Vec3f::UnitZ(), // Up direction for overhang detection (accounts for build plate tilt)
bool force_reselection = false); // force reselection of the triangle mesh even in cases that mouse is pointing on the selected triangle
void bucket_fill_select_triangles(const Vec3f &hit, // point where to start
int facet_start, // facet of the original mesh (unsplit) that the hit point belongs to
+6 -6
View File
@@ -1,5 +1,5 @@
#include "calib.hpp"
#include "BeltGCodeWriter.hpp"
#include "GCode/BeltKinematics.hpp"
#include "BoundingBox.hpp"
#include "Config.hpp"
#include "Model.hpp"
@@ -855,10 +855,8 @@ void CalibPressureAdvancePattern::_refresh_writer(bool is_bbl_machine, const Mod
// needs the machine kinematics (axis remap + frame shear/scale) with the
// coordinates interpreted as world points (see set_world_coordinates).
if (print_config.belt_printer.value) {
auto belt_writer = std::make_shared<BeltGCodeWriter>();
belt_writer->set_belt_back_transform(print_config);
belt_writer->set_machine_frame_transform(print_config);
belt_writer->set_world_coordinates(true);
auto belt_writer = std::make_shared<GCodeWriter>();
install_belt_kinematics(*belt_writer, print_config, /*world_coordinates=*/true);
const int rx = int(print_config.gcode_remap_x.value);
const int ry = int(print_config.gcode_remap_y.value);
const int rz = int(print_config.gcode_remap_z.value);
@@ -869,7 +867,9 @@ void CalibPressureAdvancePattern::_refresh_writer(bool is_bbl_machine, const Mod
print_config.printable_height.value));
}
m_writer = std::move(belt_writer);
} else if (dynamic_cast<BeltGCodeWriter*>(m_writer.get()) != nullptr) {
} else if (m_writer && dynamic_cast<const BeltKinematics *>(&m_writer->kinematics()) != nullptr) {
// Previously configured for a belt printer; drop back to a plain writer,
// exactly as the old dynamic_cast<BeltGCodeWriter*> check did.
m_writer = std::make_shared<GCodeWriter>();
}
+1 -1
View File
@@ -363,7 +363,7 @@ private:
const Calib_Params &m_params;
// Polymorphic so belt printers get a BeltGCodeWriter in world-coordinates
// Polymorphic so belt printers get belt kinematics in world-coordinates
// mode (_refresh_writer); shared_ptr keeps the class copyable — the writer
// is rebuilt by refresh_setup() before every use anyway.
std::shared_ptr<GCodeWriter> m_writer{std::make_shared<GCodeWriter>()};
+4 -137
View File
@@ -387,9 +387,6 @@ void Bed3D::render_internal(GLCanvas3D& canvas, const Transform3d& view_matrix,
m_model.set_color(m_is_dark ? DEFAULT_MODEL_COLOR_DARK : DEFAULT_MODEL_COLOR);
// Belt printer: bed rotation is applied inside render_model() and render_default()
// using m_is_belt_printer and m_belt_angle members.
switch (m_type)
{
case Type::System: { render_system(canvas, view_matrix, projection_matrix, bottom); break; }
@@ -398,8 +395,6 @@ void Bed3D::render_internal(GLCanvas3D& canvas, const Transform3d& view_matrix,
}
render_gravity_arrow(view_matrix, projection_matrix);
render_slicing_arrow(view_matrix, projection_matrix);
render_slicing_plane(view_matrix, projection_matrix);
glsafe(::glDisable(GL_DEPTH_TEST));
}
@@ -700,12 +695,6 @@ void Bed3D::render_model(const Transform3d& view_matrix, const Transform3d& proj
shader->start_using();
shader->set_uniform("emission_factor", 0.0f);
Transform3d model_matrix = Geometry::assemble_transform(m_model_offset);
// Belt printer: rotate the bed model about the tilt axis so the belt tilt
// is visible. Negative angle: belt surface tilts downward away from the nozzle.
if (m_is_belt_printer && m_belt_angle > 0.f) {
double angle_rad = Geometry::deg2rad(static_cast<double>(m_belt_angle));
model_matrix = Eigen::AngleAxisd(-angle_rad, belt_tilt_unit_axis()) * model_matrix;
}
shader->set_uniform("volume_world_matrix", model_matrix);
shader->set_uniform("view_model_matrix", view_matrix * model_matrix);
shader->set_uniform("projection_matrix", projection_matrix);
@@ -746,20 +735,14 @@ void Bed3D::render_custom(GLCanvas3D& canvas, const Transform3d& view_matrix, co
void Bed3D::render_gravity_arrow(const Transform3d& view_matrix, const Transform3d& projection_matrix)
{
const DynamicPrintConfig& cfg = wxGetApp().preset_bundle->printers.get_edited_preset().config;
// build_plate_tilt_{x,y} are kept in sync with the belt tilt (see TabPrinter), so
// reading them here covers both belt and non-belt tilted printers.
double tilt_x_deg = cfg.opt_float("build_plate_tilt_x");
double tilt_y_deg = cfg.opt_float("build_plate_tilt_y");
if (tilt_x_deg == 0. && tilt_y_deg == 0.) {
const Vec3d up_dir = build_plate_tilt_up_direction();
if (up_dir == Vec3d::UnitZ()) {
m_gravity_arrow.reset();
return;
}
// Gravity direction (matching the slicer's tilt convention)
double tilt_x_rad = Geometry::deg2rad(tilt_x_deg);
double tilt_y_rad = Geometry::deg2rad(tilt_y_deg);
Vec3d gravity_dir = Vec3d(-tan(tilt_y_rad), -tan(tilt_x_rad), -1.0).normalized();
const Vec3d gravity_dir = -up_dir;
// Build the arrow model (same dimensions as the axis arrows)
if (!m_gravity_arrow.is_initialized()) {
@@ -803,114 +786,6 @@ void Bed3D::render_gravity_arrow(const Transform3d& view_matrix, const Transform
shader->stop_using();
}
void Bed3D::render_slicing_arrow(const Transform3d& view_matrix, const Transform3d& projection_matrix)
{
if (!m_is_belt_printer || m_belt_angle <= 0.f)
return;
// Build the arrow model: shorter and wider than the gravity arrow.
if (!m_slicing_arrow.is_initialized()) {
const float stem_length = 15.0f; // shorter than gravity arrow (25)
const float stem_radius = 1.0f; // wider than gravity arrow (~0.33)
const float tip_radius = 3.0f; // wider tip
const float tip_length = 5.0f;
m_slicing_arrow.init_from(stilized_arrow(16, tip_radius, tip_length, stem_radius, stem_length));
}
// The slicing direction: layers stack along the gantry normal, i.e. the image of
// +Z under the mesh rotation about the tilt axis. Use the same AngleAxis as the
// slicing pipeline so the arrow matches whichever tilt axis is configured.
double angle_rad = Geometry::deg2rad(static_cast<double>(m_belt_angle));
Vec3d slice_dir = (Eigen::AngleAxisd(angle_rad, belt_tilt_unit_axis()).toRotationMatrix()
* Vec3d::UnitZ()).normalized();
// Compute rotation to align +Z (arrow default) with slice_dir.
Vec3d from = Vec3d::UnitZ();
double dot = from.dot(slice_dir);
Transform3d rot = Transform3d::Identity();
if (dot < -0.9999) {
rot = Eigen::AngleAxisd(M_PI, Vec3d::UnitX()) * rot;
} else if (dot < 0.9999) {
Vec3d axis = from.cross(slice_dir).normalized();
double angle = std::acos(std::clamp(dot, -1.0, 1.0));
rot = Eigen::AngleAxisd(angle, axis) * rot;
}
GLShaderProgram* shader = wxGetApp().get_shader("flat");
if (shader == nullptr)
return;
// Disable depth test so the arrow is always visible (not occluded by the tilted bed).
glsafe(::glDisable(GL_DEPTH_TEST));
shader->start_using();
const Camera& camera = wxGetApp().plater()->get_camera();
Transform3d model_matrix = rot;
shader->set_uniform("view_model_matrix", camera.get_view_matrix() * model_matrix);
shader->set_uniform("projection_matrix", camera.get_projection_matrix());
m_slicing_arrow.set_color({ 1.0f, 0.2f, 0.6f, 1.0f }); // pink
m_slicing_arrow.render();
shader->stop_using();
glsafe(::glEnable(GL_DEPTH_TEST));
}
void Bed3D::render_slicing_plane(const Transform3d& view_matrix, const Transform3d& projection_matrix)
{
if (!m_is_belt_printer || m_belt_angle <= 0.f)
return;
// Build a quad in the XZ plane (world frame) representing the belt slicing plane.
// The plane is tilted at belt_angle from horizontal, with normal (0, -sin(a), cos(a)).
// We render it as a semi-transparent quad centered on the build plate.
if (!m_slicing_plane.is_initialized()) {
const float half_size = 120.f; // mm, large enough to be visible
GLModel::Geometry init_data;
init_data.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P3N3 };
init_data.reserve_vertices(4);
init_data.reserve_indices(2); // 2 triangles
// Quad corners in local frame (XY plane, will be rotated to match slicing plane)
Vec3f n = Vec3f::UnitZ();
init_data.add_vertex(Vec3f(-half_size, -half_size, 0.f), n);
init_data.add_vertex(Vec3f( half_size, -half_size, 0.f), n);
init_data.add_vertex(Vec3f( half_size, half_size, 0.f), n);
init_data.add_vertex(Vec3f(-half_size, half_size, 0.f), n);
init_data.add_triangle(0, 1, 2);
init_data.add_triangle(0, 2, 3);
m_slicing_plane.init_from(std::move(init_data));
}
GLShaderProgram* shader = wxGetApp().get_shader("flat");
if (shader == nullptr)
return;
glsafe(::glEnable(GL_DEPTH_TEST));
glsafe(::glEnable(GL_BLEND));
glsafe(::glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA));
shader->start_using();
// Show a tilted plane representing the slicing direction.
// The slicing plane is rotated by belt_angle about the tilt axis from horizontal.
// Raise it slightly so it's visible above the bed surface.
double angle_rad = Geometry::deg2rad(static_cast<double>(m_belt_angle));
Transform3d model_matrix = Transform3d::Identity();
model_matrix.translate(Vec3d(0., 0., 30.));
model_matrix.rotate(Eigen::AngleAxisd(angle_rad, belt_tilt_unit_axis()));
shader->set_uniform("view_model_matrix", view_matrix * model_matrix);
shader->set_uniform("projection_matrix", projection_matrix);
m_slicing_plane.set_color({ 0.2f, 0.6f, 1.0f, 0.3f }); // semi-transparent blue
m_slicing_plane.render();
glsafe(::glDisable(GL_BLEND));
shader->stop_using();
}
void Bed3D::render_default(bool bottom, const Transform3d& view_matrix, const Transform3d& projection_matrix)
{
// m_texture.reset();
@@ -921,15 +796,7 @@ void Bed3D::render_default(bool bottom, const Transform3d& view_matrix, const Tr
if (shader != nullptr) {
shader->start_using();
// Belt printer: rotate the default bed about X so the belt tilt is visible.
Transform3d view_model_matrix = view_matrix;
if (m_is_belt_printer && m_belt_angle > 0.f) {
double angle_rad = Geometry::deg2rad(static_cast<double>(m_belt_angle));
Transform3d belt_rotation = Transform3d::Identity();
belt_rotation.rotate(Eigen::AngleAxisd(-angle_rad, Vec3d::UnitX()));
view_model_matrix = view_matrix * belt_rotation;
}
shader->set_uniform("view_model_matrix", view_model_matrix);
shader->set_uniform("view_model_matrix", view_matrix);
shader->set_uniform("projection_matrix", projection_matrix);
glsafe(::glEnable(GL_DEPTH_TEST));
-18
View File
@@ -111,8 +111,6 @@ private:
GLModel m_model;
Vec3d m_model_offset{ Vec3d::Zero() };
GLModel m_gravity_arrow;
GLModel m_slicing_arrow; // Pink arrow showing the effective slicing direction
GLModel m_slicing_plane; // Debug: shows the intended slicing plane direction
Axes m_axes;
float m_scale_factor{ 1.0f };
@@ -122,11 +120,6 @@ private:
std::vector<std::vector<Vec2d>> m_extruder_shapes;
std::vector<double> m_extruder_heights;
bool m_is_dark = false;
// Belt printer state for rendering.
bool m_is_belt_printer = false;
float m_belt_angle = 0.f;
// Tilt axis: 0 = X (belt travels along Y, the common case), 1 = Y.
int m_belt_tilt_axis = 0;
public:
Bed3D() = default;
@@ -148,15 +141,6 @@ public:
const BuildVolume& build_volume() const { return m_build_volume; }
BuildVolume& build_volume() { return m_build_volume; }
// Belt printer bed settings. tilt_axis: 0 = X (belt along Y), 1 = Y.
void set_belt_printer(bool enabled, float angle_deg, int tilt_axis = 0) {
m_is_belt_printer = enabled; m_belt_angle = angle_deg; m_belt_tilt_axis = tilt_axis;
}
bool is_belt_printer() const { return m_is_belt_printer; }
float belt_angle() const { return m_belt_angle; }
// Unit vector of the tilt axis in bed space.
Vec3d belt_tilt_unit_axis() const { return m_belt_tilt_axis == 1 ? Vec3d::UnitY() : Vec3d::UnitX(); }
// Was the model provided, or was it generated procedurally?
Type get_type() const { return m_type; }
// Was the model generated procedurally?
@@ -196,8 +180,6 @@ private:
void render_custom(GLCanvas3D& canvas, const Transform3d& view_matrix, const Transform3d& projection_matrix, bool bottom);
void render_default(bool bottom, const Transform3d& view_matrix, const Transform3d& projection_matrix);
void render_gravity_arrow(const Transform3d& view_matrix, const Transform3d& projection_matrix);
void render_slicing_arrow(const Transform3d& view_matrix, const Transform3d& projection_matrix);
void render_slicing_plane(const Transform3d& view_matrix, const Transform3d& projection_matrix);
// BBS: remove the bed picking logic
// void register_raycasters_for_picking(const GLModel::Geometry& geometry, const Transform3d& trafo);
+1 -11
View File
@@ -1155,17 +1155,7 @@ void GLVolumeCollection::render(GLVolumeCollection::ERenderType type,
// Compute up direction accounting for build plate tilt. This is frame-invariant
// (config cannot change mid-render), so compute it once before the volume loop.
Vec3f up_direction = Vec3f::UnitZ();
{
const DynamicPrintConfig& prt_cfg = GUI::wxGetApp().preset_bundle->printers.get_edited_preset().config;
double tilt_x_deg = prt_cfg.opt_float("build_plate_tilt_x");
double tilt_y_deg = prt_cfg.opt_float("build_plate_tilt_y");
if (tilt_x_deg != 0. || tilt_y_deg != 0.) {
double tilt_x_rad = Geometry::deg2rad(tilt_x_deg);
double tilt_y_rad = Geometry::deg2rad(tilt_y_deg);
up_direction = Vec3f(float(tan(tilt_y_rad)), float(tan(tilt_x_rad)), 1.f).normalized();
}
}
const Vec3f up_direction = GUI::build_plate_tilt_up_direction().cast<float>();
for (GLVolumeWithIdAndZ& volume : to_render) {
#if ENABLE_MODIFIERS_ALWAYS_TRANSPARENT
+313 -232
View File
@@ -9,6 +9,7 @@
#include "libslic3r/Preset.hpp"
#include "libslic3r/PresetBundle.hpp"
#include "libslic3r/PrintConfig.hpp"
#include "libslic3r/FilamentMixer.hpp"
#include "libslic3r/TriangleMesh.hpp"
#include "libslic3r/Geometry.hpp"
#include "libslic3r/BoundingBox.hpp"
@@ -32,28 +33,45 @@ namespace GUI {
// is sized so each tilted slicing plane's cross-section through the prism can
// absorb the worst-case purge volume of one layer; length follows the printed
// objects along the belt (plus ramp/height compensation at both tilted ends).
bool ensure_belt_purge_tower(Model &model, PartPlateList &partplate_list, ObjectList *obj_list, BeltPurgeSignature &sig)
bool ensure_belt_purge_tower(Model &model, PartPlateList &partplate_list, ObjectList *obj_list, std::vector<BeltPurgeSignature> &sigs)
{
auto is_prism = [](const ModelObject *mo) {
const ConfigOption *opt = mo->config.option("belt_purge_tower_object");
return opt != nullptr && opt->getBool();
};
std::vector<int> prism_idxs;
// Every plate gets its own prism. A prism belongs to the plate it lies on; one
// that lies on no plate is stale.
const int plate_count = partplate_list.get_plate_count();
sigs.resize(size_t(plate_count));
std::vector<std::vector<int>> prisms_by_plate(static_cast<size_t>(plate_count));
std::vector<int> stale_prisms;
for (int i = 0; i < (int) model.objects.size(); ++i)
if (is_prism(model.objects[i]))
prism_idxs.push_back(i);
if (is_prism(model.objects[i])) {
const int plate_idx = model.objects[i]->instances.empty() ? -1 : partplate_list.find_instance(i, 0);
if (plate_idx >= 0 && plate_idx < plate_count)
prisms_by_plate[size_t(plate_idx)].push_back(i);
else
stale_prisms.push_back(i);
}
// Deletes prism objects, keeping the sidebar and part plates in sync
// (same primitives as Plater::priv::remove(), minus scene update — the
// caller refreshes the scene).
auto remove_prisms = [&](const std::vector<int> &idxs) {
// caller refreshes the scene). Highest index first, so the others stay valid.
auto remove_prisms = [&](std::vector<int> idxs) {
std::sort(idxs.begin(), idxs.end());
for (auto it = idxs.rbegin(); it != idxs.rend(); ++it) {
model.delete_object(size_t(*it));
partplate_list.notify_instance_removed(*it, -1);
obj_list->delete_object_from_list(size_t(*it));
}
};
auto all_prisms = [&]() {
std::vector<int> all = stale_prisms;
for (const auto &v : prisms_by_plate)
all.insert(all.end(), v.begin(), v.end());
return all;
};
// Cheap early-out for non-belt printers: only belt printers ever get a belt
// purge tower, and this runs on every background-process tick — so avoid the
@@ -62,10 +80,11 @@ bool ensure_belt_purge_tower(Model &model, PartPlateList &partplate_list, Object
{
const auto *belt_pre = wxGetApp().preset_bundle->printers.get_edited_preset().config.option<ConfigOptionBool>("belt_printer");
if (belt_pre == nullptr || !belt_pre->value) {
sig = BeltPurgeSignature{};
if (prism_idxs.empty())
std::fill(sigs.begin(), sigs.end(), BeltPurgeSignature{});
const std::vector<int> all = all_prisms();
if (all.empty())
return false;
remove_prisms(prism_idxs);
remove_prisms(all);
return true;
}
}
@@ -92,65 +111,15 @@ bool ensure_belt_purge_tower(Model &model, PartPlateList &partplate_list, Object
const auto *seq_opt = print_config.option<ConfigOptionEnum<PrintSequence>>("print_sequence");
const bool by_object = seq_opt != nullptr && seq_opt->value == PrintSequence::ByObject;
// Filaments used and bounding extent of the non-prism objects on the
// current plate (1-based filament ids; volume extruder 0 = object default).
PartPlate *plate = partplate_list.get_curr_plate();
std::set<int> filaments;
double x_min = std::numeric_limits<double>::max();
double x_max = -std::numeric_limits<double>::max();
double y_min = std::numeric_limits<double>::max();
double y_max = -std::numeric_limits<double>::max();
double z_max = 0.;
bool have_objects = false;
if (belt && plate != nullptr) {
for (int obj_idx = 0; obj_idx < (int) model.objects.size(); ++obj_idx) {
const ModelObject *mo = model.objects[obj_idx];
if (is_prism(mo))
continue;
int obj_extruder = 1;
if (const ConfigOption *opt = mo->config.option("extruder"); opt != nullptr && opt->getInt() > 0)
obj_extruder = opt->getInt();
bool any_instance_on_plate = false;
for (int inst_idx = 0; inst_idx < (int) mo->instances.size(); ++inst_idx) {
if (!plate->contain_instance_totally(obj_idx, inst_idx))
continue;
any_instance_on_plate = true;
const BoundingBoxf3 bb = mo->instance_bounding_box(inst_idx);
x_min = std::min(x_min, bb.min.x());
x_max = std::max(x_max, bb.max.x());
y_min = std::min(y_min, bb.min.y());
y_max = std::max(y_max, bb.max.y());
z_max = std::max(z_max, bb.max.z());
}
if (!any_instance_on_plate)
continue;
have_objects = true;
for (const ModelVolume *mv : mo->volumes)
for (int e : mv->get_extruders())
filaments.insert(e > 0 ? e : obj_extruder);
}
}
// Mixed filament slots are expanded to their physical components below.
std::vector<unsigned char> is_mixed;
std::vector<std::string> comp_strs;
if (const auto *o = full_cfg.option<ConfigOptionBools>("filament_is_mixed"))
is_mixed = o->values;
if (const auto *o = full_cfg.option<ConfigOptionStrings>("filament_mixed_components"))
comp_strs = o->values;
const bool wanted = belt && prime_tower_enabled && !by_object && have_objects && filaments.size() > 1;
if (!wanted) {
sig = BeltPurgeSignature{};
if (prism_idxs.empty())
return false;
remove_prisms(prism_idxs);
BOOST_LOG_TRIVIAL(warning) << "[BELT-DEBUG] belt purge tower removed (conditions not met)";
return true;
}
// --- Sizing -----------------------------------------------------------
const int n_islands = std::max(1, (int) filaments.size() - 1);
const double gap = 1.0;
// Every disconnected island needs at least 1 mm of printable width. Honor
// the configured total width whenever possible, but never let the island
// layout silently grow past the footprint used for placement.
const double min_width = n_islands + (n_islands - 1) * gap;
const double width = std::max(min_width,
print_config.has("belt_purge_tower_width") ? print_config.opt_float("belt_purge_tower_width") : 35.);
const double printable_width = width - (n_islands - 1) * gap;
const double gap = 1.0;
const double layer_h = print_config.has("layer_height") ? print_config.opt_float("layer_height") : 0.2;
// Belt geometry. The rotation axis is the gantry tilt axis; the belt
@@ -168,15 +137,6 @@ bool ensure_belt_purge_tower(Model &model, PartPlateList &partplate_list, Object
const double sin_t = std::sin(theta);
const double cot_t = std::cos(theta) / sin_t;
// Parts' extent along the belt-travel axis and the lateral (across-belt) axis.
const double belt_min = belt_is_y ? y_min : x_min;
const double belt_max = belt_is_y ? y_max : x_max;
const double lat_min = belt_is_y ? x_min : y_min;
const double lat_max = belt_is_y ? x_max : y_max;
// Worst-case purge volume of one layer: up to (filament count - 1)
// toolchanges, each needing the worst flush matrix entry (mirrors the
// volume selection in Print::_plan_belt_purge()).
// NOTE: both purge_in_prime_tower and single_extruder_multi_material are
// PRINTER options (Preset.cpp s_Preset_printer_options) — read them from the
// printer preset. Reading purge_in_prime_tower from the print preset returns
@@ -185,102 +145,236 @@ bool ensure_belt_purge_tower(Model &model, PartPlateList &partplate_list, Object
// the backend Print::_plan_belt_purge() which reads both from the merged config.
const bool use_matrix = (printer_config.has("purge_in_prime_tower") && printer_config.opt_bool("purge_in_prime_tower"))
&& (printer_config.has("single_extruder_multi_material") && printer_config.opt_bool("single_extruder_multi_material"));
double max_flush = print_config.has("prime_volume") ? print_config.opt_float("prime_volume") : 45.;
if (use_matrix) {
const size_t extruder_nums = wxGetApp().preset_bundle->get_printer_extruder_count();
const std::vector<double> matrix = get_flush_volumes_matrix(
project_config.option<ConfigOptionFloats>("flush_volumes_matrix")->values, 0, extruder_nums);
const auto * multi_opt = project_config.option<ConfigOptionFloats>("flush_multiplier");
const double multiplier = multi_opt != nullptr && !multi_opt->values.empty() ? multi_opt->get_at(0) : 1.;
const int n_total = (int) (std::sqrt(double(matrix.size())) + 0.5);
double m = 0.;
for (int i : filaments)
for (int j : filaments)
if (i != j && i <= n_total && j <= n_total)
m = std::max(m, matrix[size_t(i - 1) * n_total + size_t(j - 1)]);
if (m > 0.)
max_flush = m * multiplier;
}
const double v_layer = double(filaments.size() - 1) * max_flush;
// Height from the per-layer purge demand. A tilted slicing plane cuts a
// printable_width x (height/sin) rectangle out of the bars, so one layer
// slab absorbs printable_width * (height/sin) * layer_height of purge. Solve for the height that
// holds the worst-case per-layer purge, with eta (infill/perimeter packing)
// and a safety margin for the tilt ramps / grid-alignment slop, plus a
// minimum so the tower is a real printable body rather than a sliver.
const double eta = 0.85;
const double safety = 1.6;
const double prime_volume = print_config.has("prime_volume") ? print_config.opt_float("prime_volume") : 45.;
const double printable_height = printer_config.has("printable_height") ? printer_config.opt_float("printable_height") : 250.;
double height = safety * v_layer * sin_t / (printable_width * layer_h * eta);
height = std::clamp(height, 8.0, std::max(8.0, printable_height));
// --- Idempotence (input-keyed) ----------------------------------------
auto q = [](double v) { return std::lround(v * 10.0); }; // 0.1 mm quantization
BeltPurgeSignature new_sig;
new_sig.valid = true;
new_sig.filament_count = (int) filaments.size();
new_sig.key[0] = q(width);
new_sig.key[1] = q(layer_h);
new_sig.key[2] = q(height);
new_sig.key[3] = q(belt_min);
new_sig.key[4] = q(belt_max);
new_sig.key[5] = q(lat_min);
new_sig.key[6] = q(z_max);
new_sig.key[7] = static_cast<long>(rot);
new_sig.key[8] = std::lround(theta * 10000.0);
new_sig.key[9] = q(lat_max);
const Vec3d plate_origin = plate->get_origin();
new_sig.key[10] = q(plate_origin.x());
new_sig.key[11] = q(plate_origin.y());
if (prism_idxs.size() == 1 && model.objects[size_t(prism_idxs.front())]->instances.size() == 1 && new_sig == sig)
return false; // already up to date — do not touch the model
// --- Position ----------------------------------------------------------
// Belt-travel axis. With the mesh rotated by theta before slicing, a machine
// point (y,z) maps to slicing-Z = y*sin(theta) + z*cos(theta). The parts
// occupy slicing-Z in [y_min*sin, y_max*sin + z_max*cos], and the bar's
// FULL-cross-section region (the part not in a triangular end ramp) spans
// slicing-Z [belt_start*sin + H*cos, belt_end*sin]. Covering the parts'
// whole band needs belt_start <= y_min - H*cot (bar's own leading ramp) and
// belt_end >= y_max + z_max*cot (parts' top features print further up the
// belt). The trailing z_max*cot term dominates the bar's own ramp.
const double margin = 5.;
const double ramp_compensation = height / sin_t;
const double belt_origin = plate_origin[belt_is_y ? 1 : 0];
const double belt_start = std::max(belt_origin, belt_min - ramp_compensation); // leading ramp, toward belt origin
const double belt_end = belt_max + margin + ramp_compensation + z_max * cot_t; // + parts' top-feature belt reach
const double length = std::max(belt_end - belt_start, 10.);
const double belt_center = 0.5 * (belt_start + belt_end);
// Across-belt: flush against the bed's maximum edge, inset by half the bar
// width so the bar's far edge sits on the boundary and the whole bar stays
// on the bed. The bed (printable_area) is plate-local but model instances
// live in the plate's world frame, so add the plate origin's lateral
// component. lat_min/lat_max come from instance_bounding_box (world frame).
const double lat_origin = plate_origin[belt_is_y ? 0 : 1];
const double inset = 1.;
double lat_center = lat_max + 5. + 0.5 * width; // fallback: just past the parts
BoundingBoxf bed_ext_dbg;
// The bed (printable_area) is plate-local but model instances live in the
// plate's world frame, so the plate origin is added to every bed coordinate.
const double inset = 1.;
BoundingBoxf bed_ext;
if (const auto *bed_opt = printer_config.option<ConfigOptionPoints>("printable_area");
bed_opt != nullptr && !bed_opt->values.empty()) {
const BoundingBoxf bed_ext = get_extents(bed_opt->values);
bed_ext_dbg = bed_ext;
const double bed_lat_max = belt_is_y ? bed_ext.max.x() : bed_ext.max.y();
lat_center = lat_origin + bed_lat_max - inset - 0.5 * width;
bed_opt != nullptr && !bed_opt->values.empty())
bed_ext = get_extents(bed_opt->values);
// What each plate needs, decided before anything is deleted or created.
struct Plan
{
bool wanted = false;
BeltPurgeSignature sig;
int n_islands = 1;
double w_sub = 0., length = 0., height = 0.;
Vec3d center = Vec3d::Zero();
};
std::vector<Plan> plans(static_cast<size_t>(plate_count));
for (int plate_idx = 0; plate_idx < plate_count; ++plate_idx) {
Plan &plan = plans[size_t(plate_idx)];
PartPlate *plate = partplate_list.get_plate(plate_idx);
// Filaments used and bounding extent of the non-prism objects on this
// plate (1-based filament ids; volume extruder 0 = object default).
std::set<int> filaments;
double x_min = std::numeric_limits<double>::max();
double x_max = -std::numeric_limits<double>::max();
double y_min = std::numeric_limits<double>::max();
double y_max = -std::numeric_limits<double>::max();
double z_max = 0.;
bool have_objects = false;
if (belt && plate != nullptr) {
for (int obj_idx = 0; obj_idx < (int) model.objects.size(); ++obj_idx) {
const ModelObject *mo = model.objects[obj_idx];
if (is_prism(mo))
continue;
int obj_extruder = 1;
if (const ConfigOption *opt = mo->config.option("extruder"); opt != nullptr && opt->getInt() > 0)
obj_extruder = opt->getInt();
bool any_instance_on_plate = false;
for (int inst_idx = 0; inst_idx < (int) mo->instances.size(); ++inst_idx) {
if (!plate->contain_instance_totally(obj_idx, inst_idx))
continue;
any_instance_on_plate = true;
const BoundingBoxf3 bb = mo->instance_bounding_box(inst_idx);
x_min = std::min(x_min, bb.min.x());
x_max = std::max(x_max, bb.max.x());
y_min = std::min(y_min, bb.min.y());
y_max = std::max(y_max, bb.max.y());
z_max = std::max(z_max, bb.max.z());
}
if (!any_instance_on_plate)
continue;
have_objects = true;
for (const ModelVolume *mv : mo->volumes)
for (int e : mv->get_extruders())
filaments.insert(e > 0 ? e : obj_extruder);
}
}
// A mixed filament slot is VIRTUAL: it never reaches a nozzle. At slice time
// ToolOrdering::resolve_mixed_filaments() replaces it with its physical
// components, so the toolchanges the prism has to absorb are between those
// components, not to the mixed slot itself. Counting the slot as a filament
// of its own therefore over-provisions the prism by one island per mixed slot
// -- the "extra purge tower" -- and, when every component is already used by
// another object, by an island that can never be reached at all.
//
// Expand here with the same helper the backend uses (Print.cpp's sequential
// path), so the GUI sizes the prism against the same filament set the slicer
// will actually produce. No-op when no filament is mixed.
if (has_any_mixed_filament(is_mixed)) {
std::vector<unsigned int> zero_based;
zero_based.reserve(filaments.size());
for (int f : filaments)
if (f > 0)
zero_based.push_back((unsigned int) (f - 1));
zero_based = expand_mixed_filaments(zero_based, is_mixed, comp_strs);
filaments.clear();
for (unsigned int f : zero_based)
filaments.insert((int) f + 1);
}
plan.wanted = belt && prime_tower_enabled && !by_object && have_objects && filaments.size() > 1;
if (!plan.wanted)
continue;
// --- Sizing -----------------------------------------------------------
const int n_islands = std::max(1, (int) filaments.size() - 1);
// Every disconnected island needs at least 1 mm of printable width. Honor
// the configured total width whenever possible, but never let the island
// layout silently grow past the footprint used for placement.
const double min_width = n_islands + (n_islands - 1) * gap;
const double width = std::max(min_width,
print_config.has("belt_purge_tower_width") ? print_config.opt_float("belt_purge_tower_width") : 35.);
const double printable_width = width - (n_islands - 1) * gap;
// Parts' extent along the belt-travel axis and the lateral (across-belt) axis.
const double belt_min = belt_is_y ? y_min : x_min;
const double belt_max = belt_is_y ? y_max : x_max;
const double lat_min = belt_is_y ? x_min : y_min;
const double lat_max = belt_is_y ? x_max : y_max;
// Worst-case purge volume of one layer: up to (filament count - 1)
// toolchanges, each needing the worst flush matrix entry (mirrors the
// volume selection in Print::_plan_belt_purge()).
double max_flush = prime_volume;
if (use_matrix) {
const size_t extruder_nums = wxGetApp().preset_bundle->get_printer_extruder_count();
const std::vector<double> matrix = get_flush_volumes_matrix(
project_config.option<ConfigOptionFloats>("flush_volumes_matrix")->values, 0, extruder_nums);
const auto * multi_opt = project_config.option<ConfigOptionFloats>("flush_multiplier");
const double multiplier = multi_opt != nullptr && !multi_opt->values.empty() ? multi_opt->get_at(0) : 1.;
const int n_total = (int) (std::sqrt(double(matrix.size())) + 0.5);
double m = 0.;
for (int i : filaments)
for (int j : filaments)
if (i != j && i <= n_total && j <= n_total)
m = std::max(m, matrix[size_t(i - 1) * n_total + size_t(j - 1)]);
if (m > 0.)
max_flush = m * multiplier;
}
const double v_layer = double(filaments.size() - 1) * max_flush;
// Height from the per-layer purge demand. A tilted slicing plane cuts a
// printable_width x (height/sin) rectangle out of the bars, so one layer
// slab absorbs printable_width * (height/sin) * layer_height of purge. Solve for the height that
// holds the worst-case per-layer purge, with eta (infill/perimeter packing)
// and a safety margin for the tilt ramps / grid-alignment slop, plus a
// minimum so the tower is a real printable body rather than a sliver.
const double eta = 0.85;
const double safety = 1.6;
double height = safety * v_layer * sin_t / (printable_width * layer_h * eta);
height = std::clamp(height, 8.0, std::max(8.0, printable_height));
// --- Idempotence (input-keyed) ----------------------------------------
auto q = [](double v) { return std::lround(v * 10.0); }; // 0.1 mm quantization
BeltPurgeSignature &new_sig = plan.sig;
new_sig.valid = true;
new_sig.filament_count = (int) filaments.size();
new_sig.key[0] = q(width);
new_sig.key[1] = q(layer_h);
new_sig.key[2] = q(height);
new_sig.key[3] = q(belt_min);
new_sig.key[4] = q(belt_max);
new_sig.key[5] = q(lat_min);
new_sig.key[6] = q(z_max);
new_sig.key[7] = static_cast<long>(rot);
new_sig.key[8] = std::lround(theta * 10000.0);
new_sig.key[9] = q(lat_max);
const Vec3d plate_origin = plate->get_origin();
new_sig.key[10] = q(plate_origin.x());
new_sig.key[11] = q(plate_origin.y());
if (bed_ext.defined) {
new_sig.key[12] = q(bed_ext.max.x());
new_sig.key[13] = q(bed_ext.max.y());
}
// --- Position ----------------------------------------------------------
// Belt-travel axis. With the mesh rotated by theta before slicing, a machine
// point (y,z) maps to slicing-Z = y*sin(theta) + z*cos(theta). The parts
// occupy slicing-Z in [y_min*sin, y_max*sin + z_max*cos], and the bar's
// FULL-cross-section region (the part not in a triangular end ramp) spans
// slicing-Z [belt_start*sin + H*cos, belt_end*sin]. Covering the parts'
// whole band needs belt_start <= y_min - H*cot (bar's own leading ramp) and
// belt_end >= y_max + z_max*cot (parts' top features print further up the
// belt). The trailing z_max*cot term dominates the bar's own ramp.
//
// Along the belt the bar stops at the end of the plate: a longer bar cannot be
// printed, and the cross-sections it loses there are reported by the purge
// planner when the parts' last layers then purge more than the bar holds.
const double margin = 5.;
const double ramp_compensation = height / sin_t;
const double belt_origin = plate_origin[belt_is_y ? 1 : 0];
double belt_end = belt_max + margin + ramp_compensation + z_max * cot_t; // + parts' top-feature belt reach
if (bed_ext.defined)
belt_end = std::min(belt_end, belt_origin + (belt_is_y ? bed_ext.max.y() : bed_ext.max.x()) - inset);
const double belt_start = std::max(belt_origin, std::min(belt_min - ramp_compensation, belt_end - 10.)); // leading ramp, toward belt origin
const double length = std::max(belt_end - belt_start, 10.);
belt_end = belt_start + length;
const double belt_center = 0.5 * (belt_start + belt_end);
// Across-belt: flush against the bed's maximum edge, inset by half the bar
// width so the bar's far edge sits on the boundary and the whole bar stays
// on the bed. lat_min/lat_max come from instance_bounding_box (world frame).
const double lat_origin = plate_origin[belt_is_y ? 0 : 1];
double lat_center = lat_max + 5. + 0.5 * width; // fallback: just past the parts
if (bed_ext.defined) {
const double bed_lat_max = belt_is_y ? bed_ext.max.x() : bed_ext.max.y();
lat_center = lat_origin + bed_lat_max - inset - 0.5 * width;
}
plan.n_islands = n_islands;
plan.w_sub = (width - (n_islands - 1) * gap) / n_islands;
plan.length = length;
plan.height = height;
plan.center = Vec3d(belt_is_y ? lat_center : belt_center,
belt_is_y ? belt_center : lat_center,
0.5 * height);
}
BOOST_LOG_TRIVIAL(warning) << "[BELT-DEBUG] purge place"
<< " plate_origin=(" << plate_origin.x() << "," << plate_origin.y() << ")"
<< " parts_x=[" << x_min << "," << x_max << "] parts_y=[" << y_min << "," << y_max << "] z_max=" << z_max
<< " bed_ext=[" << bed_ext_dbg.min.x() << "," << bed_ext_dbg.min.y()
<< " -> " << bed_ext_dbg.max.x() << "," << bed_ext_dbg.max.y() << "]"
<< " lat_center=" << lat_center << " belt=[" << belt_start << "," << belt_end << "]";
// --- Decide ---------------------------------------------------------------
// A plate whose prism exists and matches its recorded inputs is left alone, so
// subsequent ticks are no-ops until the parts/config actually change.
std::vector<int> to_delete = stale_prisms;
std::vector<int> to_create;
for (int plate_idx = 0; plate_idx < plate_count; ++plate_idx) {
const Plan &plan = plans[size_t(plate_idx)];
const std::vector<int> &existing = prisms_by_plate[size_t(plate_idx)];
BeltPurgeSignature &sig = sigs[size_t(plate_idx)];
if (!plan.wanted) {
sig = BeltPurgeSignature{};
to_delete.insert(to_delete.end(), existing.begin(), existing.end());
} else if (existing.size() == 1 && model.objects[size_t(existing.front())]->instances.size() == 1 && plan.sig == sig) {
continue;
} else {
to_delete.insert(to_delete.end(), existing.begin(), existing.end());
to_create.push_back(plate_idx);
}
}
if (to_delete.empty() && to_create.empty())
return false;
const Vec3d desired_center(belt_is_y ? lat_center : belt_center,
belt_is_y ? belt_center : lat_center,
0.5 * height);
remove_prisms(to_delete);
// --- (Re)create -----------------------------------------------------------
// Build the prism as N DISCONNECTED sub-bars side by side across the belt,
// N = (filaments - 1) = the worst-case number of toolchanges on one layer.
// Why: mark_wiping_extrusions overrides whole extrusion-entity COLLECTIONS,
@@ -299,77 +393,64 @@ bool ensure_belt_purge_tower(Model &model, PartPlateList &partplate_list, Object
// reintroduce the multi-swap-per-layer absorption bug; a hair over ~2 line
// widths also keeps gap-fill from bridging them. 1 mm is about as close as
// they can butt up while staying individually purgeable.
const double w_sub = (width - (n_islands - 1) * gap) / n_islands;
for (int plate_idx : to_create) {
const Plan &plan = plans[size_t(plate_idx)];
// --- (Re)create ---------------------------------------------------------
if (!prism_idxs.empty())
remove_prisms(prism_idxs);
TriangleMesh prism_mesh;
for (int i = 0; i < plan.n_islands; ++i) {
const double lat_off = i * (plan.w_sub + gap);
// Box dims: lateral = w_sub, along-belt = length, vertical = height.
TriangleMesh box = belt_is_y ? make_cube(plan.w_sub, plan.length, plan.height) // X = lateral, Y = belt
: make_cube(plan.length, plan.w_sub, plan.height); // X = belt, Y = lateral
box.translate(belt_is_y ? Vec3f((float) lat_off, 0.f, 0.f) : Vec3f(0.f, (float) lat_off, 0.f));
prism_mesh.merge(box);
}
TriangleMesh prism_mesh;
for (int i = 0; i < n_islands; ++i) {
const double lat_off = i * (w_sub + gap);
// Box dims: lateral = w_sub, along-belt = length, vertical = height.
TriangleMesh box = belt_is_y ? make_cube(w_sub, length, height) // X = lateral, Y = belt
: make_cube(length, w_sub, height); // X = belt, Y = lateral
box.translate(belt_is_y ? Vec3f((float) lat_off, 0.f, 0.f) : Vec3f(0.f, (float) lat_off, 0.f));
prism_mesh.merge(box);
ModelObject *new_object = model.add_object();
new_object->name = _u8L("Belt Purge Tower");
new_object->add_instance();
ModelVolume *new_volume = new_object->add_volume(std::move(prism_mesh));
new_volume->name = new_object->name;
auto &cfg = new_object->config;
cfg.set_key_value("belt_purge_tower_object", new ConfigOptionBool(true));
cfg.set_key_value("flush_into_objects", new ConfigOptionBool(true));
cfg.set_key_value("extruder", new ConfigOptionInt(1));
// Sacrificial solid prism: one wall, no shells, dense rectilinear infill —
// every extrusion is overriddable, so the absorbed volume matches the
// cross-section x layer-height estimate used for the height above.
cfg.set_key_value("wall_loops", new ConfigOptionInt(1));
cfg.set_key_value("top_shell_layers", new ConfigOptionInt(0));
cfg.set_key_value("bottom_shell_layers", new ConfigOptionInt(0));
cfg.set_key_value("sparse_infill_density", new ConfigOptionPercent(100));
cfg.set_key_value("sparse_infill_pattern", new ConfigOptionEnum<InfillPattern>(ipRectilinear));
cfg.set_key_value("enable_support", new ConfigOptionBool(false));
cfg.set_key_value("brim_type", new ConfigOptionEnum<BrimType>(btNoBrim));
cfg.set_key_value("seam_slope_type", new ConfigOptionEnum<SeamScarfType>(SeamScarfType::None));
cfg.set_key_value("precise_z_height", new ConfigOptionBool(false));
// Position by the belt-calibration pattern: drop to the bed, then translate
// the instance by the delta between the object's ACTUAL bbox center and the
// target. Setting the instance offset directly is unreliable here — the
// freshly added cube's local frame is not centered, so set_offset() lands
// the min corner (not the center) on the target, leaving the bar centered
// on the bed edge with half of it hanging off.
new_object->invalidate_bounding_box();
new_object->ensure_on_bed();
const BoundingBoxf3 cur = new_object->bounding_box_exact();
new_object->translate_instances(Vec3d(plan.center.x() - cur.center().x(),
plan.center.y() - cur.center().y(),
0.0));
new_object->instances.front()->set_assemble_transformation(new_object->instances.front()->get_transformation());
const size_t obj_idx = model.objects.size() - 1;
// Registers the object in the sidebar and notifies the part plates;
// selection is left untouched (auto-managed object).
obj_list->add_object_to_list(obj_idx, /*call_selection_changed=*/false);
sigs[size_t(plate_idx)] = plan.sig;
}
ModelObject *new_object = model.add_object();
new_object->name = _u8L("Belt Purge Tower");
new_object->add_instance();
ModelVolume *new_volume = new_object->add_volume(std::move(prism_mesh));
new_volume->name = new_object->name;
auto &cfg = new_object->config;
cfg.set_key_value("belt_purge_tower_object", new ConfigOptionBool(true));
cfg.set_key_value("flush_into_objects", new ConfigOptionBool(true));
cfg.set_key_value("extruder", new ConfigOptionInt(1));
// Sacrificial solid prism: one wall, no shells, dense rectilinear infill —
// every extrusion is overriddable, so the absorbed volume matches the
// cross-section x layer-height estimate used for the height above.
cfg.set_key_value("wall_loops", new ConfigOptionInt(1));
cfg.set_key_value("top_shell_layers", new ConfigOptionInt(0));
cfg.set_key_value("bottom_shell_layers", new ConfigOptionInt(0));
cfg.set_key_value("sparse_infill_density", new ConfigOptionPercent(100));
cfg.set_key_value("sparse_infill_pattern", new ConfigOptionEnum<InfillPattern>(ipRectilinear));
cfg.set_key_value("enable_support", new ConfigOptionBool(false));
cfg.set_key_value("brim_type", new ConfigOptionEnum<BrimType>(btNoBrim));
cfg.set_key_value("seam_slope_type", new ConfigOptionEnum<SeamScarfType>(SeamScarfType::None));
cfg.set_key_value("precise_z_height", new ConfigOptionBool(false));
// Position by the belt-calibration pattern: drop to the bed, then translate
// the instance by the delta between the object's ACTUAL bbox center and the
// target. Setting the instance offset directly is unreliable here — the
// freshly added cube's local frame is not centered, so set_offset() lands
// the min corner (not the center) on the target, leaving the bar centered
// on the bed edge with half of it hanging off.
new_object->invalidate_bounding_box();
new_object->ensure_on_bed();
const BoundingBoxf3 cur = new_object->bounding_box_exact();
new_object->translate_instances(Vec3d(desired_center.x() - cur.center().x(),
desired_center.y() - cur.center().y(),
0.0));
new_object->instances.front()->set_assemble_transformation(new_object->instances.front()->get_transformation());
const size_t obj_idx = model.objects.size() - 1;
// Registers the object in the sidebar and notifies the part plates;
// selection is left untouched (auto-managed object).
obj_list->add_object_to_list(obj_idx, /*call_selection_changed=*/false);
// Record the inputs that produced this prism so subsequent ticks are no-ops
// until the parts/config actually change.
sig = new_sig;
BOOST_LOG_TRIVIAL(warning) << "[BELT-DEBUG] belt purge tower generated"
<< " belt_is_y=" << belt_is_y
<< " W=" << width << " L=" << length << " H=" << height
<< " v_layer=" << v_layer << " max_flush=" << max_flush
<< " filaments=" << filaments.size()
<< " theta_deg=" << Geometry::rad2deg(theta)
<< " desired_center=(" << desired_center.x() << "," << desired_center.y() << "," << desired_center.z() << ")"
<< " achieved_center=(" << new_object->bounding_box_exact().center().x() << ","
<< new_object->bounding_box_exact().center().y() << ")";
return true;
}
+8 -5
View File
@@ -1,5 +1,7 @@
#pragma once
#include <vector>
// ORCA-Belt: auto-managed purge prism for belt printers.
//
// Kept in its own translation unit (not buried in Plater.cpp) so it stays out
@@ -22,24 +24,25 @@ struct BeltPurgeSignature
{
bool valid = false;
int filament_count = 0;
long key[12] = {0}; // rounded geometry and plate inputs (0.1 mm units)
long key[14] = {0}; // rounded geometry, plate and bed inputs (0.1 mm units)
bool operator==(const BeltPurgeSignature &o) const
{
if (valid != o.valid || filament_count != o.filament_count)
return false;
for (int i = 0; i < 12; ++i)
for (int i = 0; i < 14; ++i)
if (key[i] != o.key[i])
return false;
return true;
}
};
// Keep the auto-generated belt purge prism in sync with the current config and
// plate contents. Creates / updates / removes the marked prism ModelObject.
// Keep the auto-generated belt purge prisms, one per plate, in sync with the
// current config and plate contents. Creates / updates / removes the marked prism
// ModelObjects; `sigs` holds the last inputs per plate index.
// Returns true when the model was mutated (caller should refresh the scene).
// Runs on every background-process update, so it is idempotent: it only mutates
// the model when the desired prism differs from the cached signature in `sig`.
bool ensure_belt_purge_tower(Model &model, PartPlateList &partplate_list, ObjectList *obj_list, BeltPurgeSignature &sig);
bool ensure_belt_purge_tower(Model &model, PartPlateList &partplate_list, ObjectList *obj_list, std::vector<BeltPurgeSignature> &sigs);
} // namespace GUI
} // namespace Slic3r
+4 -2
View File
@@ -1193,8 +1193,10 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, in
toggle_line("small_area_infill_flow_compensation_model", have_small_area_infill_flow_compensation);
toggle_field("seam_slope_type", !has_spiral_vase);
bool has_seam_slope = !has_spiral_vase && config->opt_enum<SeamScarfType>("seam_slope_type") != SeamScarfType::None;
// Belt printers: the scarf would start one layer back along the belt, inside the
// previous layer (GCode::extrude_loop skips it there too).
toggle_field("seam_slope_type", !has_spiral_vase && !is_belt_printer);
bool has_seam_slope = !has_spiral_vase && !is_belt_printer && config->opt_enum<SeamScarfType>("seam_slope_type") != SeamScarfType::None;
toggle_line("seam_slope_conditional", has_seam_slope);
toggle_line("seam_slope_start_height", has_seam_slope);
toggle_line("seam_slope_entire_loop", has_seam_slope);
+16 -10
View File
@@ -1164,7 +1164,7 @@ 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 (BeltGCodeWriter::
// 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:
@@ -1223,10 +1223,8 @@ void GCodeViewer::load_as_gcode(const GCodeProcessorResult& gcode_result, const
m_loaded_as_preview = false;
// Belt printers: drive the designed/raw view UI (legend checkbox, hotkey B, canvas-toolbar
// menu item) from the loaded print here. Plater::set_bed_shape also calls set_belt_printer(),
// but only on bed-shape changes — not reliably on every slice/preview load — so the UI was
// staying hidden even though the (config-driven) designed view rendered. The tilt magnitude
// comes from the G-code header (gcode_result.belt_tilt_angle, abs of the slicing rotation).
// menu item) from the loaded print. The tilt magnitude comes from the G-code header
// (gcode_result.belt_tilt_angle, abs of the slicing rotation).
m_belt_view_enabled = print.config().belt_printer.value;
m_belt_angle_deg = gcode_result.belt_tilt_angle;
@@ -1387,8 +1385,6 @@ void GCodeViewer::load_as_gcode(const GCodeProcessorResult& gcode_result, const
// translation, which this min-corner step recovers.
const Vec3d d = model_bb.min - tp_bb.min;
belt_inv = Transform3d(Eigen::Translation3d(d)) * belt_inv;
BOOST_LOG_TRIVIAL(debug) << "[BELT-PREVIEW] anchor d=[" << d.x() << "," << d.y() << "," << d.z()
<< "] (clip kept " << n_clip << "/" << n_filtered << " moves)";
}
}
libvgcode::GCodeInputData data = libvgcode::convert(gcode_result, str_tool_colors, str_color_print_colors, m_viewer,
@@ -1522,8 +1518,18 @@ void GCodeViewer::load_as_gcode(const GCodeProcessorResult& gcode_result, const
});
m_paths_bounding_box = BoundingBoxf3(libvgcode::convert(bbox[0]).cast<double>(), libvgcode::convert(bbox[1]).cast<double>());
if (wxGetApp().is_editor())
m_contained_in_bed = wxGetApp().plater()->build_volume().all_paths_inside(gcode_result, m_paths_bounding_box);
if (wxGetApp().is_editor()) {
if (is_belt) {
// The moves are machine-frame coordinates (Z is belt travel), so the per-move
// test inside all_paths_inside() can never pass on a belt. Judge the
// back-transformed box instead, with room for the designed view's min-corner
// anchor, which is only accurate to a fraction of a millimetre.
BoundingBoxf3 bed = wxGetApp().plater()->build_volume().bounding_volume();
bed.offset(1.);
m_contained_in_bed = !m_paths_bounding_box.defined || (bed.contains(m_paths_bounding_box.min) && bed.contains(m_paths_bounding_box.max));
} else
m_contained_in_bed = wxGetApp().plater()->build_volume().all_paths_inside(gcode_result, m_paths_bounding_box);
}
m_extruders_count = gcode_result.filaments_count;
@@ -4923,7 +4929,7 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv
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::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
+13
View File
@@ -81,6 +81,7 @@
#include <openssl/evp.h>
#include "libslic3r/Utils.hpp"
#include "libslic3r/Geometry.hpp"
#include "libslic3r/Model.hpp"
#include "libslic3r/I18N.hpp"
#include "libslic3r/PresetBundle.hpp"
@@ -9812,5 +9813,17 @@ bool is_support_filament(int extruder_id, bool strict_check)
return support_option->get_at(0);
};
Vec3d build_plate_tilt_up_direction()
{
const DynamicPrintConfig &cfg = wxGetApp().preset_bundle->printers.get_edited_preset().config;
const auto *opt_x = cfg.option<ConfigOptionFloat>("build_plate_tilt_x");
const auto *opt_y = cfg.option<ConfigOptionFloat>("build_plate_tilt_y");
const double tilt_x = opt_x != nullptr ? opt_x->value : 0.;
const double tilt_y = opt_y != nullptr ? opt_y->value : 0.;
if (tilt_x == 0. && tilt_y == 0.)
return Vec3d::UnitZ();
return Vec3d(std::tan(Geometry::deg2rad(tilt_y)), std::tan(Geometry::deg2rad(tilt_x)), 1.).normalized();
}
} // GUI
} //Slic3r
+2
View File
@@ -831,6 +831,8 @@ bool is_support_filament(int extruder_id, bool strict_check = true);
bool is_soluble_filament(int extruder_id);
// check if the filament for model is in the list
bool has_filaments(const std::vector<std::string>& model_filaments);
// Up direction of the edited printer's tilted build plate (+Z when untilted).
Vec3d build_plate_tilt_up_direction();
} // namespace GUI
} // Slic3r
+1 -3
View File
@@ -63,7 +63,6 @@ static SettingsFactory::Bundle FREQ_SETTINGS_BUNDLE_FFF =
{ L("Support") , { "enable_support", "support_type", "support_threshold_angle", "support_threshold_overlap",
"support_base_pattern", "support_on_build_plate_only","support_critical_regions_only",
"support_remove_small_overhang",
"build_plate_tilt_x", "build_plate_tilt_y",
"support_base_pattern_spacing", "support_expansion"}},
//BBS
{ L("Flush options") , { "flush_into_infill", "flush_into_objects", "flush_into_support"} }
@@ -93,8 +92,7 @@ std::map<std::string, std::vector<SimpleSettingData>> SettingsFactory::OBJECT_C
{"support_bottom_z_distance", "",22},{"support_top_z_distance", "",23},{"support_base_pattern", "",24},{"support_base_pattern_spacing", "",25},
{"support_interface_top_layers", "",26},{"support_interface_bottom_layers", "",27},{"support_interface_spacing", "",28},{"support_bottom_interface_spacing", "",29},
{"support_object_xy_distance", "",30}, {"bridge_no_support", "",31},{"max_bridge_length", "",32},{"support_critical_regions_only", "",33},{"support_remove_small_overhang","",34},
{"build_plate_tilt_x","",35},{"build_plate_tilt_y","",36},
{"support_object_first_layer_gap","",37}
{"support_object_first_layer_gap","",35}
}},
{ L("Speed"), {{"support_speed", "",12}, {"support_interface_speed", "",13}
}}
+3 -17
View File
@@ -548,14 +548,6 @@ int GLGizmoFdmSupports::get_selection_support_threshold_angle()
return auto_support ? support_threshold_angle : 0;
}
std::pair<double, double> GLGizmoFdmSupports::get_build_plate_tilt()
{
const DynamicPrintConfig& cfg = wxGetApp().preset_bundle->printers.get_edited_preset().config;
double tilt_x = cfg.opt_float("build_plate_tilt_x");
double tilt_y = cfg.opt_float("build_plate_tilt_y");
return {tilt_x, tilt_y};
}
void GLGizmoFdmSupports::select_facets_by_angle(float threshold_deg, bool block)
{
float threshold = (float(M_PI)/180.f)*threshold_deg;
@@ -564,15 +556,9 @@ void GLGizmoFdmSupports::select_facets_by_angle(float threshold_deg, bool block)
const ModelInstance* mi = mo->instances[selection.get_instance_idx()];
// Compute gravity direction accounting for build plate tilt
auto [tilt_x_deg, tilt_y_deg] = get_build_plate_tilt();
double tilt_x_rad = tilt_x_deg * M_PI / 180.0;
double tilt_y_rad = tilt_y_deg * M_PI / 180.0;
const bool has_tilt = (tilt_x_deg != 0. || tilt_y_deg != 0.);
// NB: use an if, not a ?:, so each branch converts to Vec3d independently
// (the two Eigen expression types don't unify in a ternary).
Vec3d gravity_dir = -Vec3d::UnitZ();
if (has_tilt)
gravity_dir = Vec3d(-tan(tilt_y_rad), -tan(tilt_x_rad), -1.0).normalized();
const Vec3d up_dir = build_plate_tilt_up_direction();
const bool has_tilt = up_dir != Vec3d::UnitZ();
const Vec3d gravity_dir = -up_dir;
int mesh_id = -1;
for (const ModelVolume* mv : mo->volumes) {
@@ -60,7 +60,6 @@ private:
void select_facets_by_angle(float threshold, bool block);
// BBS
int get_selection_support_threshold_angle();
std::pair<double, double> get_build_plate_tilt();
int m_support_threshold_angle = -1;
+4 -11
View File
@@ -75,14 +75,7 @@ GLGizmoPainterBase::ClippingPlaneDataWrapper GLGizmoPainterBase::get_clipping_pl
Vec3f GLGizmoPainterBase::get_tilt_up_direction() const
{
const DynamicPrintConfig& cfg = wxGetApp().preset_bundle->printers.get_edited_preset().config;
double tilt_x_deg = cfg.opt_float("build_plate_tilt_x");
double tilt_y_deg = cfg.opt_float("build_plate_tilt_y");
if (tilt_x_deg == 0. && tilt_y_deg == 0.)
return Vec3f::UnitZ();
double tilt_x_rad = Geometry::deg2rad(tilt_x_deg);
double tilt_y_rad = Geometry::deg2rad(tilt_y_deg);
return Vec3f(float(tan(tilt_y_rad)), float(tan(tilt_x_rad)), 1.f).normalized();
return build_plate_tilt_up_direction().cast<float>();
}
void GLGizmoPainterBase::render_triangles(const Selection& selection) const
@@ -707,7 +700,7 @@ bool GLGizmoPainterBase::gizmo_event(SLAGizmoEventType action, const Vec2d& mous
mi->get_assemble_transformation().get_matrix() * mo->volumes[m_rr.mesh_id]->get_matrix() :
mi->get_transformation().get_matrix() * mo->volumes[m_rr.mesh_id]->get_matrix();
m_triangle_selectors[m_rr.mesh_id]->seed_fill_select_triangles(m_rr.hit, int(m_rr.facet), trafo_matrix_not_translate, this->get_clipping_plane_in_volume_coordinates(trafo_matrix), m_smart_fill_angle,
m_paint_on_overhangs_only ? m_highlight_by_angle_threshold_deg : 0.f, true, get_tilt_up_direction());
m_paint_on_overhangs_only ? m_highlight_by_angle_threshold_deg : 0.f, get_tilt_up_direction(), true);
m_triangle_selectors[m_rr.mesh_id]->request_update_render_data();
m_seed_fill_last_mesh_id = m_rr.mesh_id;
}
@@ -871,7 +864,7 @@ bool GLGizmoPainterBase::gizmo_event(SLAGizmoEventType action, const Vec2d& mous
m_triangle_selectors[mesh_idx]->seed_fill_apply_on_triangles(new_state);
if (m_tool_type == ToolType::SMART_FILL)
m_triangle_selectors[mesh_idx]->seed_fill_select_triangles(mesh_hit, facet_idx, trafo_matrix_not_translate, clp, m_smart_fill_angle,
m_paint_on_overhangs_only ? m_highlight_by_angle_threshold_deg : 0.f, true, get_tilt_up_direction());
m_paint_on_overhangs_only ? m_highlight_by_angle_threshold_deg : 0.f, get_tilt_up_direction(), true);
else if (m_tool_type == ToolType::BRUSH && m_cursor_type == TriangleSelector::CursorType::POINTER)
// BBS: add infill_angle parameter
m_triangle_selectors[mesh_idx]->bucket_fill_select_triangles(mesh_hit, facet_idx, clp, -1.f, false, true);
@@ -969,7 +962,7 @@ bool GLGizmoPainterBase::gizmo_event(SLAGizmoEventType action, const Vec2d& mous
const TriangleSelector::ClippingPlane &clp = this->get_clipping_plane_in_volume_coordinates(trafo_matrix);
if (m_tool_type == ToolType::SMART_FILL)
m_triangle_selectors[m_rr.mesh_id]->seed_fill_select_triangles(m_rr.hit, int(m_rr.facet), trafo_matrix_not_translate, clp, m_smart_fill_angle,
m_paint_on_overhangs_only ? m_highlight_by_angle_threshold_deg : 0.f, false, get_tilt_up_direction());
m_paint_on_overhangs_only ? m_highlight_by_angle_threshold_deg : 0.f, get_tilt_up_direction(), false);
else if (m_tool_type == ToolType::BRUSH && m_cursor_type == TriangleSelector::CursorType::POINTER)
// BBS: add infill_angle parameter
m_triangle_selectors[m_rr.mesh_id]->bucket_fill_select_triangles(m_rr.hit, int(m_rr.facet), clp, -1.f, false);
+93 -4
View File
@@ -15,6 +15,9 @@
#include "slic3r/GUI/GUI_ObjectList.hpp"
#include "libnest2d/common.hpp"
#include "libslic3r/Geometry.hpp"
#include <cmath>
#include <set>
#define SAVE_ARRANGE_POLY 0
@@ -63,6 +66,14 @@ public:
}
};
// The belt purge prism is generated from the arranged parts (ensure_belt_purge_tower),
// so arrange neither moves it nor packs around it; it reserves the prism's strip instead.
static bool is_belt_purge_prism(const ModelObject *mo)
{
const ConfigOption *opt = mo->config.option("belt_purge_tower_object");
return opt != nullptr && opt->getBool();
}
// BBS: add partplate logic
static WipeTower get_wipe_tower(const Plater &plater, int plate_idx)
{
@@ -77,6 +88,63 @@ arrangement::ArrangePolygon get_wipetower_arrange_poly(WipeTower* tower)
return ap;
}
// Belt printers pack their parts against the edges of the bed, so what has to stay
// free there is reserved with fixed virtual items on every plate, the way the bed's
// own exclusion areas are:
// - the strip the purge prism comes back to, flush with the far lateral edge (see
// ensure_belt_purge_tower), when the parts use more than one filament;
// - the brim along every edge: a belt brim is printed brim_width wide for every brim
// type but none. Between parts the brims may overlap, as on any printer.
void ArrangeJob::prepare_belt_regions(int num_plates)
{
if (!params.is_belt || params.is_seq_print)
return;
const DynamicPrintConfig &config = wxGetApp().preset_bundle->full_config();
const BoundingBoxf bed = get_extents(config.opt<ConfigOptionPoints>("printable_area")->values);
const bool belt_is_y = params.belt_axis == 1;
std::vector<BoundingBoxf> regions;
std::set<int> filaments;
for (const ArrangePolygons *items : { &m_selected, &m_unselected })
for (const ArrangePolygon &ap : *items)
if (!ap.is_virt_object)
filaments.insert(ap.extrude_ids.begin(), ap.extrude_ids.end());
if (config.opt_bool("enable_belt_purge_tower") && filaments.size() > 1) {
// The prism is at least one millimetre per island plus the gaps between them.
const int islands = int(filaments.size()) - 1;
const double width = std::max(config.opt_float("belt_purge_tower_width"), 2. * islands - 1.) + 1.; // + the prism's edge inset
BoundingBoxf strip = bed;
if (belt_is_y)
strip.min.x() = std::max(bed.min.x(), bed.max.x() - width);
else
strip.min.y() = std::max(bed.min.y(), bed.max.y() - width);
regions.push_back(strip);
}
// Virtual items are inflated by the one millimetre exclusion gap already.
const double brim = config.opt_enum<BrimType>("brim_type") == btNoBrim ? 0. :
config.opt_float("brim_width") + config.opt_float("brim_object_gap") + config.opt_float("extra_brim_width") - 1.;
if (brim > 0.) {
regions.emplace_back(bed.min, Vec2d(bed.min.x() + brim, bed.max.y()));
regions.emplace_back(Vec2d(bed.max.x() - brim, bed.min.y()), bed.max);
regions.emplace_back(bed.min, Vec2d(bed.max.x(), bed.min.y() + brim));
regions.emplace_back(Vec2d(bed.min.x(), bed.max.y() - brim), bed.max);
}
for (int j = 0; j < num_plates; ++j)
for (size_t i = 0; i < regions.size(); ++i) {
ArrangePolygon ap;
ap.poly.contour = scaled(regions[i]).polygon();
ap.translation = Vec2crd(0, 0);
ap.rotation = 0.f;
ap.is_virt_object = true;
ap.bed_idx = j;
ap.height = 1;
ap.name = "BeltRegion" + std::to_string(i);
m_unselected.emplace_back(std::move(ap));
}
}
void ArrangeJob::clear_input()
{
const Model &model = m_plater->model();
@@ -128,6 +196,8 @@ void ArrangeJob::prepare_selected() {
for (size_t oidx = 0; oidx < model.objects.size(); ++oidx) {
const Selection::InstanceIdxsList* instlist = obj_sel[oidx];
ModelObject* mo = model.objects[oidx];
if (is_belt_purge_prism(mo))
continue;
std::vector<bool> inst_sel(mo->instances.size(), false);
@@ -176,6 +246,7 @@ void ArrangeJob::prepare_selected() {
}
prepare_wipe_tower();
prepare_belt_regions(MAX_NUM_PLATES);
// The strides have to be removed from the fixed items. For the
@@ -206,6 +277,8 @@ void ArrangeJob::prepare_all() {
// Go through the objects and check if inside the selection
for (size_t oidx = 0; oidx < model.objects.size(); ++oidx) {
ModelObject *mo = model.objects[oidx];
if (is_belt_purge_prism(mo))
continue;
for (size_t i = 0; i < mo->instances.size(); ++i) {
ModelInstance * mi = mo->instances[i];
@@ -252,6 +325,7 @@ void ArrangeJob::prepare_all() {
// add the virtual object into unselect list if has
plate_list.preprocess_exclude_areas(m_unselected, enable_wrapping, MAX_NUM_PLATES);
prepare_belt_regions(MAX_NUM_PLATES);
}
arrangement::ArrangePolygon estimate_wipe_tower_info(int plate_index, std::set<int>& extruder_ids)
@@ -290,10 +364,9 @@ void ArrangeJob::prepare_wipe_tower()
bool enable_prime_tower = op && op->getBool();
if (!enable_prime_tower || params.is_seq_print) return;
// Belt printers have no classic wipe tower; purging goes into the belt
// purge prism, which is a real model object and arranges like any other.
if (const auto *belt_opt = wxGetApp().preset_bundle->printers.get_edited_preset().config.option<ConfigOptionBool>("belt_printer");
belt_opt && belt_opt->value)
// Belt printers have no classic wipe tower; purging goes into the belt purge
// prism, whose strip prepare_belt_regions() reserves.
if (params.is_belt)
return;
bool smooth_timelapse = false;
@@ -399,6 +472,8 @@ void ArrangeJob::prepare_partplate() {
for (size_t oidx = 0; oidx < model.objects.size(); ++oidx)
{
ModelObject* mo = model.objects[oidx];
if (is_belt_purge_prism(mo))
continue;
for (size_t inst_idx = 0; inst_idx < mo->instances.size(); ++inst_idx)
{
bool in_plate = plate->contain_instance(oidx, inst_idx) || plate->intersect_instance(oidx, inst_idx);
@@ -434,6 +509,7 @@ void ArrangeJob::prepare_partplate() {
// add the virtual object into unselect list if has
plate_list.preprocess_exclude_areas(m_unselected, enable_wrapping, current_plate_index + 1);
prepare_belt_regions(current_plate_index + 1);
}
//BBS: add partplate logic
@@ -785,6 +861,19 @@ arrangement::ArrangeParams init_arrange_params(Plater *p)
params.is_seq_print = settings.is_seq_print;
params.min_obj_distance = scaled(settings.distance);
params.align_to_y_axis = settings.align_to_y_axis;
if (print_config.belt_printer.value) {
// Parts print in belt order: the belt runs across the gantry's tilt axis, a
// rotation about X prints toward +Y and one about Y toward -X (see
// BeltTransform), a negative angle flips that, and a tilted layer reaches
// cot(angle) * height past a part's far edge.
const BeltRotationAxis axis = print_config.belt_slice_rotation.value;
const double angle = print_config.belt_slice_rotation_angle.value;
const bool tilted = (axis == BeltRotationAxis::X || axis == BeltRotationAxis::Y) && std::abs(angle) > EPSILON;
params.is_belt = true;
params.belt_axis = axis == BeltRotationAxis::Y ? 0 : 1;
params.belt_reversed = tilted && ((axis == BeltRotationAxis::Y) != (angle < 0.));
params.belt_tilt_slope = tilted ? float(1. / std::tan(Geometry::deg2rad(std::clamp(std::abs(angle), 5., 90.)))) : 0.f;
}
int state = p->get_prepare_state();
if (state == Job::JobPrepareState::PREPARE_STATE_MENU) {
+1
View File
@@ -46,6 +46,7 @@ class ArrangeJob : public Job
//BBS:prepare the items from current selected partplate
void prepare_partplate();
void prepare_wipe_tower();
void prepare_belt_regions(int num_plates);
ArrangePolygon prepare_arrange_polygon(void* instance);
+31 -4
View File
@@ -612,6 +612,17 @@ void PartPlate::calc_height_limit() {
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << "Unable to create height limit top lines\n";
}
// The plate's icons and labels grow with its depth, but they sit in the gap to the
// next plate, which grows with its width: on a long, narrow bed (a belt) they would
// otherwise run across the neighbouring plate.
float PartPlate::icon_scale_factor() const
{
const BoundingBoxf bed_ext = get_extents(m_shape);
const double by_depth = bed_ext.size().y() / 200.;
const double by_gap = bed_ext.size().x() * LOGICAL_PART_PLATE_GAP / (PARTPLATE_ICON_SIZE + 2 * PARTPLATE_ICON_GAP_LEFT);
return float(std::min(by_depth, by_gap));
}
void PartPlate::calc_vertex_for_number(int index, bool one_number, GLModel &buffer)
{
buffer.reset();
@@ -628,7 +639,7 @@ void PartPlate::calc_vertex_for_number(int index, bool one_number, GLModel &buff
#else //in the bottom
auto bed_ext = get_extents(m_shape);
Vec2d p = bed_ext[1];
float factor = bed_ext.size()(1) / 200.0;
float factor = icon_scale_factor();
float size = PARTPLATE_ICON_SIZE * factor;
float offset_y = PARTPLATE_TEXT_OFFSET_Y * factor;
float offset_x = (one_number?PARTPLATE_TEXT_OFFSET_X1: PARTPLATE_TEXT_OFFSET_X2) * factor;
@@ -650,7 +661,7 @@ void PartPlate::calc_vertex_for_plate_name_edit_icon(GLTexture *texture, int ind
ExPolygon poly;
auto bed_ext = get_extents(m_shape);
Vec2d p = bed_ext[3];
float factor = bed_ext.size()(1) / 200.0;
float factor = icon_scale_factor();
float icon_sz = factor * PARTPLATE_EDIT_PLATE_NAME_ICON_SIZE;
float width = icon_sz;
float height = icon_sz;
@@ -683,7 +694,7 @@ void PartPlate::calc_vertex_for_icons(int index, PickingModel &model)
ExPolygon poly;
auto bed_ext = get_extents(m_shape);
Vec2d p = bed_ext[2];
auto factor = bed_ext.size()(1) / 200.0;
float factor = icon_scale_factor();
float size = PARTPLATE_ICON_SIZE * factor;
float gap_left = PARTPLATE_ICON_GAP_LEFT * factor;
float gap_y = PARTPLATE_ICON_GAP_Y * factor;
@@ -2589,7 +2600,7 @@ void PartPlate::generate_plate_name_texture()
ExPolygon poly;
auto bed_ext = get_extents(m_shape);
Vec2d p = bed_ext[3];
float factor = bed_ext.size()(1) / 200.0;
float factor = icon_scale_factor();
float icon_sz = factor * PARTPLATE_EDIT_PLATE_NAME_ICON_SIZE;
float width = icon_sz * m_name_texture.get_width() / m_name_texture.get_height(); // icon size * text_bb_ratio
float height = icon_sz; // scale with icon size to preserve ratio while system scaling
@@ -2783,6 +2794,7 @@ bool PartPlate::check_outside(int obj_id, int instance_id, BoundingBoxf3* boundi
BoundingBoxf3 instance_box = bounding_box? *bounding_box: object->instance_convex_hull_bounding_box(instance_id);
Polygon hull = instance->convex_hull_2d();
BoundingBoxf3 plate_box = get_plate_box();
this->open_belt_y(plate_box);
if (instance_box.max.z() > plate_box.min.z())
plate_box.min.z() += instance_box.min.z(); // not considering outsize if sinking
@@ -3467,6 +3479,19 @@ Polygon PartPlate::get_shared_printable_polygon() const
return m_extruder_areas.empty() ? Polygon::new_scale(m_shape) : get_shared_poly(m_extruder_areas);
}
bool PartPlate::belt_open_y() const
{
// Headless (CLI) plates have no plater and no wxApp behind wxGetApp(); the CLI's own belt
// handling lives in Print::validate().
if (m_plater == nullptr || wxGetApp().preset_bundle == nullptr)
return false;
const DynamicPrintConfig &printer = wxGetApp().preset_bundle->printers.get_edited_preset().config;
const auto *belt = printer.option<ConfigOptionBool>("belt_printer");
const auto *infinite_y = printer.option<ConfigOptionBool>("belt_printer_infinite_y");
return belt != nullptr && belt->value && infinite_y != nullptr && infinite_y->value;
}
bool PartPlate::contains(const Vec3d& point) const
{
return m_bounding_box.contains(point);
@@ -3486,6 +3511,7 @@ bool PartPlate::contains(const BoundingBoxf3& bb) const
print_volume.min(1) -= Slic3r::BuildVolume::BedEpsilon;
print_volume.max(0) += Slic3r::BuildVolume::BedEpsilon;
print_volume.max(1) += Slic3r::BuildVolume::BedEpsilon;
this->open_belt_y(print_volume);
return print_volume.contains(bb);
}
@@ -3498,6 +3524,7 @@ bool PartPlate::intersects(const BoundingBoxf3& bb) const
print_volume.min(1) -= Slic3r::BuildVolume::BedEpsilon;
print_volume.max(0) += Slic3r::BuildVolume::BedEpsilon;
print_volume.max(1) += Slic3r::BuildVolume::BedEpsilon;
this->open_belt_y(print_volume);
return print_volume.intersects(bb);
}
+5
View File
@@ -178,6 +178,7 @@ private:
void calc_triangles_from_polygon(const ExPolygon &poly, GLModel& render_model);
void calc_gridlines(const ExPolygon& poly, const BoundingBox& pp_bbox);
void calc_height_limit();
float icon_scale_factor() const;
void calc_vertex_for_number(int index, bool one_number, GLModel &buffer);
void calc_vertex_for_plate_name_edit_icon(GLTexture *texture, int index, PickingModel &model);
void calc_vertex_for_icons(int index, PickingModel &model);
@@ -427,6 +428,10 @@ public:
bool contains(const GLVolume& v) const;
bool contains(const BoundingBoxf3& bb) const;
bool intersects(const BoundingBoxf3& bb) const;
// A belt printer with belt_printer_infinite_y: the plate is open along Y for the
// containment tests (the drawn plate keeps its shape).
bool belt_open_y() const;
void open_belt_y(BoundingBoxf3 &box) const { if (this->belt_open_y()) { box.min.y() = -1e5; box.max.y() = 1e5; } }
void render(const Transform3d& view_matrix, const Transform3d& projection_matrix, bool bottom, bool only_body = false, bool force_background_color = false, HeightLimitMode mode = HEIGHT_LIMIT_NONE, int hover_id = -1, bool render_cali = false, bool show_grid = true);
+5 -49
View File
@@ -6993,7 +6993,7 @@ struct Plater::priv
// config and plate contents (thin wrapper over GUI::ensure_belt_purge_tower
// in BeltPurgeTower.cpp). Returns true when the model was mutated.
bool ensure_belt_purge_tower();
BeltPurgeSignature m_belt_purge_sig;
std::vector<BeltPurgeSignature> m_belt_purge_sigs;
void delete_all_objects_from_model();
void reset(bool apply_presets_change = false);
void center_selection();
@@ -10633,7 +10633,7 @@ void Plater::priv::process_validation_warnings(const std::vector<StringObjectExc
// wrapper that hands it the model, plates, object list, and cached signature.
bool Plater::priv::ensure_belt_purge_tower()
{
return GUI::ensure_belt_purge_tower(model, partplate_list, sidebar->obj_list(), m_belt_purge_sig);
return GUI::ensure_belt_purge_tower(model, partplate_list, sidebar->obj_list(), m_belt_purge_sigs);
}
@@ -14145,37 +14145,6 @@ void Plater::priv::set_bed_shape(const Pointfs &shape,
Vec2d shape_position = partplate_list.get_current_shape_position();
bool new_shape = bed.set_shape(shape, printable_height, extruder_areas, extruder_heights, custom_model, force_as_custom, shape_position);
// Belt printer: configure build volume and bed rendering for belt mode.
{
const auto *belt_opt = config->option<ConfigOptionBool>("belt_printer");
bool is_belt = belt_opt && belt_opt->value;
if (is_belt) {
// The slicing rotation is the single source of truth for the belt tilt:
// its magnitude is the physical tilt angle and its axis is the tilt axis.
auto rot_axis = config->option<ConfigOptionEnum<BeltRotationAxis>>("belt_slice_rotation")->value;
double rot_angle = config->opt_float("belt_slice_rotation_angle");
double belt_angle = std::abs(rot_angle); // physical tilt magnitude
int tilt_axis = (rot_axis == BeltRotationAxis::Y) ? 1 : 0;
bool infinite_y = config->opt_bool("belt_printer_infinite_y");
bed.build_volume().set_belt_printer(true, belt_angle, infinite_y);
bed.set_belt_printer(true, static_cast<float>(belt_angle), tilt_axis);
if (preview)
preview->get_canvas3d()->get_gcode_viewer().set_belt_printer(true, static_cast<float>(belt_angle));
// The belt "designed view" back-transform is rebuilt from the print config at
// G-code load time (GCodeViewer::compute_belt_back_transform), so no mesh-side
// inverse needs to be pushed to the viewer here.
} else {
// Reset the BuildVolume belt state too: Bed3D::set_shape early-returns when
// the bed params are unchanged, so a belt->normal switch (or toggling belt off
// on the same printer) would otherwise leave the BuildVolume with
// m_is_belt_printer=true and an inflated Y bbox, wrongly treating out-of-bounds
// objects as printable. Idempotent for a printer that was never belt.
bed.build_volume().set_belt_printer(false, 0., false);
bed.set_belt_printer(false, 0.f);
if (preview)
preview->get_canvas3d()->get_gcode_viewer().set_belt_printer(false, 0.f);
}
}
float prev_height_lid, prev_height_rod;
partplate_list.get_height_limits(prev_height_lid, prev_height_rod);
@@ -15893,20 +15862,6 @@ void Plater::_calib_apply_belt_mode()
inst->rotate(cancel_rotation);
obj->invalidate_bounding_box();
obj->ensure_on_bed();
{
const BoundingBoxf3 rb = obj->raw_bounding_box();
const Vec3d io = inst->get_offset();
const Vec3d ir = inst->get_rotation();
BOOST_LOG_TRIVIAL(debug) << "[BELT-CALIB] helper exit: obj=" << obj->name
<< " inst_offset=(" << io.x() << "," << io.y() << "," << io.z() << ")"
<< " inst_rot=(" << ir.x() << "," << ir.y() << "," << ir.z() << ")"
<< " vol0_offset=(" << obj->volumes.front()->get_offset().x() << ","
<< obj->volumes.front()->get_offset().y() << "," << obj->volumes.front()->get_offset().z() << ")"
<< " raw_bbox=(" << rb.min.x() << "," << rb.min.y() << "," << rb.min.z()
<< ")..(" << rb.max.x() << "," << rb.max.y() << "," << rb.max.z() << ")"
<< " min_z=" << obj->min_z();
}
}
// Each object's support wedge extends upstream of it by roughly its own
@@ -15961,7 +15916,7 @@ void Plater::_calib_apply_belt_mode()
void Plater::calib_pa(const Calib_Params& params)
{
// ORCA-Belt: PA Line / PA Pattern have the belt plumbing in place
// (BeltGCodeWriter::set_world_coordinates draws them on the belt surface)
// (belt kinematics in world-coordinates mode draws them on the belt surface)
// but are not validated yet — keep them gated to the PA Tower for now.
{
double angle_rad = 0.;
@@ -16556,7 +16511,8 @@ void Plater::calib_temp(const Calib_Params& params) {
<< ", falling back to 230_190 (embossed numbers will not match)";
asset = calib_dir + "belt_temp_tower_230_190.stl";
}
add_model(false, asset);
if (!add_model(false, asset) || model().objects.empty())
return;
// Place keel-first asset at the belt entry (designed Y = 0) so Z_gcode
// starts at 0, centered laterally on the bed, resting on the conveyor.
+12 -9
View File
@@ -3230,16 +3230,17 @@ void TabPrint::toggle_options()
const auto current = m_config->opt_enum<BrimType>("brim_type");
auto &opt = const_cast<ConfigOptionDef &>(field->m_opt);
auto cb = dynamic_cast<ComboBox *>(choice->window);
if (cb != nullptr) {
// Keep the entry if it is already selected, so switching to a non-belt
// printer cannot leave the control showing a value it does not offer.
const bool offer_leading_edge = is_belt_printer || current == btLeadingEdgeOnly;
const bool offered = std::find(opt.enum_values.begin(), opt.enum_values.end(), "leading_edge_only") != opt.enum_values.end();
if (cb != nullptr && offer_leading_edge != offered) {
auto n = cb->GetValue();
opt.enum_values.clear();
opt.enum_labels.clear();
cb->Clear();
for (size_t i = 0; i < def->enum_values.size(); ++ i) {
// Keep the entry if it is already selected, so switching to a non-belt
// printer cannot leave the control showing a value it does not offer.
if (def->enum_values[i] == "leading_edge_only" && ! is_belt_printer
&& current != btLeadingEdgeOnly)
if (def->enum_values[i] == "leading_edge_only" && ! offer_leading_edge)
continue;
opt.enum_values.push_back(def->enum_values[i]);
opt.enum_labels.push_back(def->enum_labels[i]);
@@ -5104,7 +5105,7 @@ void TabPrinter::build_fff()
{
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. "
"slicing. Also drives bed rendering and support gravity tilt. "
"Isometric (no distortion); the back-transform inverts it before "
"the machine-frame remap.") };
line.append_option(belt_og->get_option("belt_slice_rotation"));
@@ -5159,7 +5160,7 @@ void TabPrinter::build_fff()
{
Line line = { L("Machine-frame tilt"),
L("The machine-frame shear (tan) and scale (1/cos) are derived from "
"the belt tilt angle. Enable 'Decouple' to set an independent "
"the belt tilt angle. Enable 'Decouple' to set an independent "
"machine-frame angle when the physical gantry tilt differs from "
"the slicing rotation.") };
line.append_option(mf->get_option("belt_frame_tilt_decouple"));
@@ -6213,8 +6214,10 @@ void TabPrinter::toggle_options()
toggle_line("belt_slice_rotation", is_belt);
// Remap, back-transform, and global mesh-transforms toggles are gated by belt
// mode here; finer mode-based visibility (Advanced vs Expert) is handled by
// each option's ConfigOptionMode in PrintConfig.cpp.
// mode here; finer mode-based visibility is handled by each option's
// ConfigOptionMode in PrintConfig.cpp. Both axis remaps are Develop-only: a
// printer profile sets them once for its kinematics, and a wrong value sends
// the gantry outside the machine.
for (auto el : {"preslice_remap_x", "gcode_remap_x", "gcode_back_transform"})
toggle_line(el, is_belt);
toggle_line("belt_preslice_global", is_belt);
+1 -1
View File
@@ -98,7 +98,7 @@ std::vector<wxString> make_shaper_type_labels()
}
// ORCA-Belt: PA Line / PA Pattern have belt plumbing in place (drawn on the
// belt surface via BeltGCodeWriter world-coordinates mode) but are not
// belt surface via BeltKinematics world-coordinates mode) but are not
// validated yet — belt printers are restricted to the PA Tower for now.
bool is_belt_printer_selected()
{
+1
View File
@@ -8,6 +8,7 @@ add_executable(${_TEST_NAME}_tests
test_extrusion_processor.cpp
test_fill.cpp
test_flow.cpp
test_gcode_processor.cpp
test_gcode_timing.cpp
test_gcodewriter.cpp
test_model.cpp
+46
View File
@@ -0,0 +1,46 @@
#include <catch2/catch_all.hpp>
#include "libslic3r/libslic3r.h"
#include "libslic3r/GCode/GCodeProcessor.hpp"
#include "libslic3r/PrintConfig.hpp"
#include "test_utils.hpp"
#include <fstream>
#include <string>
using namespace Slic3r;
using Catch::Matchers::WithinAbs;
namespace {
float processed_belt_tilt(const std::string &gcode)
{
ScopedTemporaryFile temp(".gcode");
{
std::ofstream os(temp.string());
os << gcode;
}
GCodeProcessor proc;
proc.apply_config(FullPrintConfig{});
proc.process_file(temp.string());
return proc.get_result().belt_tilt_angle;
}
constexpr const char *body = "G1 X10 Y10 Z0.2 F3000\nG1 X20 Y10 E1 F1200\n";
} // namespace
TEST_CASE("The config block's belt angle does not mark G-code as belt G-code", "[GCodeProcessor][belt]")
{
// Every printer's config block lists belt_slice_rotation_angle (default 45), belt or not.
const std::string gcode = std::string("; CONFIG_BLOCK_START\n; belt_printer = 0\n; belt_slice_rotation_angle = 45\n; CONFIG_BLOCK_END\n") + body;
CHECK_THAT(processed_belt_tilt(gcode), WithinAbs(0., 1e-6));
}
TEST_CASE("The belt header's angle marks G-code as belt G-code", "[GCodeProcessor][belt]")
{
const std::string gcode = std::string("; belt_slice_rotation_angle = -45.0\n") + body +
"; CONFIG_BLOCK_START\n; belt_printer = 1\n; belt_slice_rotation_angle = -45\n; CONFIG_BLOCK_END\n";
CHECK_THAT(processed_belt_tilt(gcode), WithinAbs(45., 1e-6));
}
+302 -9
View File
@@ -19,7 +19,7 @@
#include "libslic3r/GCode/GCodeProcessor.hpp"
#include <algorithm>
#include <limits>
#include "libslic3r/BeltGCodeWriter.hpp"
#include "libslic3r/GCode/BeltKinematics.hpp"
#include "libslic3r/BeltTransform.hpp"
#include "libslic3r/GCodeReader.hpp"
#include "libslic3r/PrintConfig.hpp"
@@ -39,9 +39,8 @@ TEST_CASE("Belt machine coordinates retain a non-45-degree slicing angle", "[GCo
config.gcode_remap_y.value = RemapAxis::PosZ;
config.gcode_remap_z.value = RemapAxis::PosY;
BeltGCodeWriter writer;
writer.set_belt_back_transform(config);
writer.set_machine_frame_transform(config);
GCodeWriter writer;
install_belt_kinematics(writer, config);
writer.set_axis_remap(int(config.gcode_remap_x.value),
int(config.gcode_remap_y.value),
int(config.gcode_remap_z.value));
@@ -52,7 +51,7 @@ TEST_CASE("Belt machine coordinates retain a non-45-degree slicing angle", "[GCo
// machine-frame shear/scale are applied.
const Vec3d model(4., 10., 3.);
Transform3d forward = BeltTransformPipeline::build_forward_transform(config);
const Vec3d machine = writer.to_machine_coords(forward * model);
const Vec3d machine = writer.kinematics().to_machine(forward * model);
// The conventional X-tilt remap produces (x, z, y). At 30 degrees the
// gantry coordinate is z/sin(30) and belt travel is y + z*cot(30).
@@ -908,7 +907,7 @@ TEST_CASE("Custom G-code motion limits are restored before generated moves", "[G
// is_current_position_clear(), mirroring the SlopeLift branch.
SCENARIO("Belt: the first travel does not lift through the uninitialised origin", "[GCodeWriter][belt]")
{
GIVEN("A fresh BeltGCodeWriter configured for an X-tilt 45 degree belt") {
GIVEN("A fresh belt-kinematics GCodeWriter configured for an X-tilt 45 degree belt") {
// Machine-frame + slicer->world back-transform config (X tilt, 45 deg).
PrintConfig belt_config;
belt_config.belt_printer.value = true;
@@ -920,9 +919,8 @@ SCENARIO("Belt: the first travel does not lift through the uninitialised origin"
belt_config.belt_frame_tilt_decouple.value = false;
belt_config.belt_frame_tilt_angle.value = 45.0;
BeltGCodeWriter writer;
writer.set_machine_frame_transform(belt_config);
writer.set_belt_back_transform(belt_config);
GCodeWriter writer;
install_belt_kinematics(writer, belt_config);
std::vector<unsigned int> extruder_ids { 0 };
writer.set_extruders(extruder_ids);
@@ -1037,3 +1035,298 @@ SCENARIO("Belt: start-gcode prepare-stage moves keep their real Z", "[GCode][bel
}
}
}
// ---------------------------------------------------------------------------
// Regression tests for the two latent bugs the MachineKinematics refactor
// preserved deliberately and the follow-up commit fixed.
// ---------------------------------------------------------------------------
// Bug 1. _travel_to_z() emits full XYZ whenever the mapping must emit every
// axis, and it builds that point from m_pos. While the position is unknown,
// m_pos.xy is the uninitialised origin, which a reverse remap maps to the far
// corner of the bed. Belt kinematics guarded this; a Cartesian writer with an
// axis remap did not, and would command a rapid across the whole bed.
static void configure_lift_writer(GCodeWriter &writer)
{
std::vector<unsigned int> extruder_ids { 0 };
writer.set_extruders(extruder_ids);
writer.set_extruder(0);
writer.config.travel_speed.values = { 100.0 };
writer.config.travel_speed_z.values = { 100.0 };
writer.config.z_hop.values = { 0.4 };
writer.config.retract_lift_above.values = { 0.0 };
writer.config.retract_lift_below.values = { 0.0 };
}
// Largest X word in a chunk of emitted G-code, or lowest() if none.
static double max_emitted_x(const std::string &gcode)
{
double max_x = std::numeric_limits<double>::lowest();
GCodeReader reader;
reader.parse_buffer(gcode, [&max_x](GCodeReader &, const GCodeReader::GCodeLine &line) {
if (line.cmd_is("G1") && line.has(X))
max_x = std::max(max_x, double(line.x()));
});
return max_x;
}
static size_t count_g1(const std::string &gcode)
{
size_t n = 0;
GCodeReader reader;
reader.parse_buffer(gcode, [&n](GCodeReader &, const GCodeReader::GCodeLine &line) {
if (line.cmd_is("G1")) ++n;
});
return n;
}
SCENARIO("Axis remap: no lift is commanded through the uninitialised origin", "[GCodeWriter][remap]")
{
// Reverse X: machine X = build_vol_max.x - logical X, so the uninitialised
// origin maps to the far edge of the bed and is unmistakable in the output.
const double bed_x = 250.0;
GIVEN("a writer with a reverse-X remap and an unknown current position") {
GCodeWriter writer;
configure_lift_writer(writer);
writer.set_axis_remap(6, 1, 2);
writer.set_build_volume_max(Vec3d(bed_x, 250.0, 250.0));
REQUIRE(writer.kinematics().must_emit_all_axes());
REQUIRE_FALSE(writer.is_current_position_clear());
WHEN("a z-hop is pending and we travel to the first point") {
writer.lazy_lift(LiftType::NormalLift);
const std::string gcode = writer.travel_to_xyz(Vec3d(10.0, 10.0, 5.0));
THEN("nothing is commanded at the image of the origin") {
// The destination maps to machine X = 250 - 10 = 240; the bogus
// origin lift would have mapped to machine X = 250.
REQUIRE(max_emitted_x(gcode) < bed_x - 1.0);
}
THEN("only the destination move is emitted") {
REQUIRE(count_g1(gcode) == 1);
}
}
}
GIVEN("the same writer once its position is known") {
GCodeWriter writer;
configure_lift_writer(writer);
writer.set_axis_remap(6, 1, 2);
writer.set_build_volume_max(Vec3d(bed_x, 250.0, 250.0));
writer.travel_to_xyz(Vec3d(20.0, 20.0, 5.0));
REQUIRE(writer.is_current_position_clear());
WHEN("a z-hop is pending and we travel again") {
writer.lazy_lift(LiftType::NormalLift);
const std::string gcode = writer.travel_to_xyz(Vec3d(30.0, 30.0, 5.0));
THEN("the separate lift move is still emitted") {
// Suppression must be pinned to the unknown position, not to the
// presence of a remap.
REQUIRE(count_g1(gcode) == 2);
}
}
}
GIVEN("an identity-mapping writer with an unknown position") {
GCodeWriter writer;
configure_lift_writer(writer);
REQUIRE_FALSE(writer.kinematics().must_emit_all_axes());
REQUIRE_FALSE(writer.is_current_position_clear());
WHEN("a z-hop is pending and we travel to the first point") {
writer.lazy_lift(LiftType::NormalLift);
const std::string gcode = writer.travel_to_xyz(Vec3d(10.0, 10.0, 5.0));
THEN("behaviour is unchanged: the lift is still emitted") {
// Three moves, not two: with no remap and an unknown position the
// destination is emitted as a separate XY move followed by its own
// Z move, on top of the lift. That split is the pre-existing
// identity-mapping path and must not change.
REQUIRE(count_g1(gcode) == 3);
}
}
}
}
SCENARIO("Axis remap: eager_lift does not lift, or record a lift, at an unknown position",
"[GCodeWriter][remap]")
{
GIVEN("a writer with a reverse-X remap and an unknown current position") {
GCodeWriter writer;
configure_lift_writer(writer);
writer.set_axis_remap(6, 1, 2);
writer.set_build_volume_max(Vec3d(250.0, 250.0, 250.0));
REQUIRE_FALSE(writer.is_current_position_clear());
WHEN("an eager lift is requested") {
const std::string lift = writer.eager_lift(LiftType::NormalLift);
THEN("no move is emitted") {
REQUIRE(lift.empty());
}
THEN("no lift is recorded, so unlift does not descend from it") {
// If m_lifted had been set while nothing was commanded, unlift()
// would emit a descent from a height the machine never reached.
REQUIRE(writer.unlift().empty());
}
}
}
GIVEN("an identity-mapping writer with an unknown position") {
GCodeWriter writer;
configure_lift_writer(writer);
WHEN("an eager lift is requested") {
const std::string lift = writer.eager_lift(LiftType::NormalLift);
THEN("behaviour is unchanged: the lift is emitted and can be undone") {
REQUIRE_FALSE(lift.empty());
REQUIRE_FALSE(writer.unlift().empty());
}
}
}
}
// Bug 2. extrude_arc_to_xy() emits G2/G3 with logical X/Y and I/J and never
// consulted the mapping. An arc is only representable when logical X and Y reach
// the machine unchanged -- which is a narrower question than "is the remap the
// identity", because a mapping that only touches Z leaves every emitted word alone.
SCENARIO("Arc support is decided by whether the mapping leaves X and Y alone", "[GCodeWriter][remap]")
{
GIVEN("a Cartesian writer") {
GCodeWriter writer;
THEN("the identity mapping supports arcs") {
REQUIRE(writer.kinematics().supports_arc_moves());
}
THEN("a Z-only negation still supports arcs") {
// (+X, +Y, -Z): non-identity, but X, Y, I and J are all untouched.
writer.set_axis_remap(0, 1, 5);
REQUIRE(writer.kinematics().must_emit_all_axes());
REQUIRE(writer.kinematics().supports_arc_moves());
}
THEN("a Z-only reversal still supports arcs") {
writer.set_axis_remap(0, 1, 8);
REQUIRE(writer.kinematics().supports_arc_moves());
}
THEN("swapping X and Y does not support arcs") {
writer.set_axis_remap(1, 0, 2);
REQUIRE_FALSE(writer.kinematics().supports_arc_moves());
}
THEN("the X-tilt style (x, z, y) remap does not support arcs") {
writer.set_axis_remap(0, 2, 1);
REQUIRE_FALSE(writer.kinematics().supports_arc_moves());
}
}
GIVEN("a belt writer") {
PrintConfig belt_config;
belt_config.belt_printer.value = true;
belt_config.belt_slice_rotation.value = BeltRotationAxis::X;
belt_config.belt_slice_rotation_angle.value = 45.0;
GCodeWriter writer;
install_belt_kinematics(writer, belt_config);
THEN("arcs are never supported, because the frame shears") {
REQUIRE_FALSE(writer.kinematics().supports_arc_moves());
}
}
}
SCENARIO("An unrepresentable arc degrades to its chord rather than emitting a wrong G2/G3",
"[GCodeWriter][remap]")
{
auto emitted_commands = [](const std::string &gcode) {
std::vector<std::string> cmds;
GCodeReader reader;
reader.parse_buffer(gcode, [&cmds](GCodeReader &, const GCodeReader::GCodeLine &line) {
if (! line.cmd().empty()) cmds.emplace_back(line.cmd());
});
return cmds;
};
GIVEN("an identity-mapping writer") {
GCodeWriter writer;
configure_lift_writer(writer);
WHEN("an arc is extruded") {
const std::string gcode = writer.extrude_arc_to_xy(
Vec2d(10.0, 0.0), Vec2d(5.0, 0.0), 0.0, /*is_ccw=*/true, "", /*force_no_extrusion=*/true);
THEN("it is still a G3") {
const auto cmds = emitted_commands(gcode);
REQUIRE(cmds.size() == 1);
REQUIRE(cmds.front() == "G3");
}
}
}
GIVEN("a writer whose mapping swaps X and Y") {
GCodeWriter writer;
configure_lift_writer(writer);
writer.set_axis_remap(1, 0, 2);
WHEN("an arc is extruded") {
const std::string gcode = writer.extrude_arc_to_xy(
Vec2d(10.0, 0.0), Vec2d(5.0, 0.0), 0.0, /*is_ccw=*/true, "", /*force_no_extrusion=*/true);
THEN("no arc is emitted; it is approximated with linear moves") {
const auto cmds = emitted_commands(gcode);
REQUIRE(! cmds.empty());
for (const auto &c : cmds)
REQUIRE(c == "G1");
}
}
}
// The first version of this test used dE = 0 with force_no_extrusion, which
// hid a real bug: the capability check sat AFTER filament()->extrude(dE), so
// the fallback into extrude_to_xy() advanced E twice. Extrusion accounting has
// to be asserted with a positive dE.
GIVEN("a writer whose mapping cannot express arcs, extruding a real amount") {
GCodeWriter writer;
configure_lift_writer(writer);
writer.set_axis_remap(1, 0, 2);
const double dE = 1.5;
// used_filament() accumulates across moves; E() is reset per line in
// relative-E mode, so it would only show the last segment.
const double used_before = writer.filament()->used_filament();
WHEN("an arc carrying that extrusion is emitted") {
const std::string gcode = writer.extrude_arc_to_xy(
Vec2d(10.0, 0.0), Vec2d(5.0, 0.0), dE, /*is_ccw=*/true, "", /*force_no_extrusion=*/false);
THEN("exactly dE is accounted for, not twice dE") {
REQUIRE_THAT(writer.filament()->used_filament() - used_before,
Catch::Matchers::WithinAbs(dE, 1e-6));
}
THEN("no G2/G3 survives") {
REQUIRE(gcode.find("G2") == std::string::npos);
REQUIRE(gcode.find("G3") == std::string::npos);
}
}
}
GIVEN("a writer whose mapping CAN express arcs, extruding a real amount") {
GCodeWriter writer;
configure_lift_writer(writer);
const double dE = 1.5;
// used_filament() accumulates across moves; E() is reset per line in
// relative-E mode, so it would only show the last segment.
const double used_before = writer.filament()->used_filament();
WHEN("an arc carrying that extrusion is emitted") {
const std::string gcode = writer.extrude_arc_to_xy(
Vec2d(10.0, 0.0), Vec2d(5.0, 0.0), dE, /*is_ccw=*/true, "", /*force_no_extrusion=*/false);
THEN("it is still a single arc and accounts for dE once") {
REQUIRE(emitted_commands(gcode).size() == 1);
REQUIRE_THAT(writer.filament()->used_filament() - used_before,
Catch::Matchers::WithinAbs(dE, 1e-6));
}
}
}
}
+231
View File
@@ -20,6 +20,10 @@
#include "test_utils.hpp"
#include <algorithm>
#include <boost/algorithm/string/predicate.hpp>
#include <cstdlib>
#include <sstream>
#include <limits>
#include <fstream>
#include <iterator>
@@ -335,6 +339,36 @@ TEST_CASE("Belt purge planning requires its managed purge object", "[Print][Purg
CHECK_FALSE(print.has_wipe_tower());
}
// The GUI creates the purge tower object; a project sliced without one (the CLI) must say
// that its filament changes go unpurged.
TEST_CASE("Belt purge tower enabled without a tower object warns", "[Print][PurgeTower][belt]")
{
DynamicPrintConfig config = multifilament_config(2, {
{ "belt_printer", 1 },
{ "enable_belt_purge_tower", 1 },
{ "layer_change_gcode", "G92 E0\n" }
});
auto purge_warnings = [](Print &print) {
std::vector<StringObjectException> warnings;
print.validate(&warnings);
return std::count_if(warnings.begin(), warnings.end(), [](const StringObjectException &w) {
return w.opt_key == "enable_belt_purge_tower";
});
};
Model model;
Print print;
build_cubes(model, print, config, /*n=*/2, /*overlap=*/false);
model.objects[1]->config.set_key_value("extruder", new ConfigOptionInt(2));
print.apply(model, config);
REQUIRE(print.extruders().size() > 1);
CHECK(purge_warnings(print) == 1);
model.objects.front()->config.set_key_value("belt_purge_tower_object", new ConfigOptionBool(true));
print.apply(model, config);
CHECK(purge_warnings(print) == 0);
}
TEST_CASE("Belt purge rejects multiple managed purge objects", "[Print][PurgeTower][Regression]")
{
DynamicPrintConfig config = multifilament_config(2, {
@@ -508,3 +542,200 @@ TEST_CASE("Sequential printing publishes the nozzle group result", "[Print][Mult
CHECK(gcode.find("; SEQ-ND-OK") != std::string::npos);
}
}
// A scarf joint starts one layer height below the layer and ramps up along the
// wall. On a tilted belt that start is a step backwards along the belt axis, into
// the previous layer's wall at the seam: 0.283 mm per 0.2 mm layer at 45 degrees.
// With an aligned seam the nozzle rams the same spot on every layer (field report
// from a BabyBelt Pro: the belt "jumped backwards" and knocked the part loose).
// Belt printers therefore never get a scarf, whatever the process preset says.
TEST_CASE("Belt printers never start a scarf seam below the layer", "[Print][belt][Seam]")
{
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
config.set_deserialize_strict({
{ "belt_printer", 1 },
{ "belt_slice_rotation", "x" },
{ "belt_slice_rotation_angle", 45 },
{ "belt_slice_rotation_global", 1 },
{ "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 },
{ "top_shell_layers", 0 },
{ "bottom_shell_layers", 1 },
{ "wall_loops", 2 },
{ "seam_position", "back" },
{ "seam_slope_type", "external" },
{ "seam_slope_inner_walls", 1 },
{ "seam_slope_start_height", 0 },
// No z-hop: on a belt a lift is a move along the belt axis (0.4 mm / sin 45 = 0.57 mm)
// and its return would read as a back-step. The shipped belt profiles print without one.
{ "z_hop", 0 },
{ "machine_start_gcode", "T[initial_tool]\n" },
{ "layer_change_gcode", "G92 E0\n" },
});
const std::string gcode = slice({ cube(20) }, config);
REQUIRE(! gcode.empty());
// The belt axis is machine Z. Within a layer it only drifts by the frame
// coupling (well under 0.1 mm across a 20 mm cube); a scarf start is a full
// layer pitch (0.283 mm) backwards.
double last_z = std::numeric_limits<double>::lowest();
double worst_backstep = 0.;
GCodeReader parser;
parser.parse_buffer(gcode, [&](GCodeReader &, const GCodeReader::GCodeLine &line) {
if (! line.cmd_is("G1") || ! line.has_z())
return;
const double z = line.z();
if (last_z != std::numeric_limits<double>::lowest())
worst_backstep = std::max(worst_backstep, last_z - z);
last_z = z;
});
CHECK(worst_backstep < 0.2);
}
// printable_height on a belt printer is the clearance under the gantry, so an object taller
// than that is refused whatever the machine-frame transform does to the emitted coordinates.
TEST_CASE("Belt printers refuse an object taller than the gantry clearance", "[Print][belt]")
{
auto belt_config = [](double printable_height) {
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
config.set_deserialize_strict({
{ "belt_printer", 1 },
{ "belt_slice_rotation", "x" },
{ "belt_slice_rotation_angle", 45 },
{ "belt_slice_rotation_global", 1 },
{ "gcode_remap_x", "rev_x" },
{ "gcode_remap_y", "pos_z" },
{ "gcode_remap_z", "pos_y" },
{ "printable_height", printable_height },
{ "skirt_loops", 0 },
{ "layer_change_gcode", "G92 E0\n" },
});
return config;
};
SECTION("a 20 mm cube fits under 50 mm of clearance") {
Print print;
Model model;
init_print({ cube(20) }, print, model, belt_config(50));
CHECK(print.validate().string.empty());
}
SECTION("a 60 mm cube does not") {
Print print;
Model model;
init_print({ cube(60) }, print, model, belt_config(50));
CHECK(print.validate().string.find("height") != std::string::npos);
}
}
// On a belt every tilted layer starts on the belt, so "the first layers" the fan stays off
// for are a band along the belt, not the first slicing layers. The generator marks where
// each extrusion segment enters and leaves that band and the cooling buffer keeps the fan
// off inside it, on every layer.
TEST_CASE("Belt printers keep the part fan off within the band above the belt", "[Print][belt][Cooling]")
{
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
config.set_deserialize_strict({
{ "belt_printer", 1 },
{ "belt_slice_rotation", "x" },
{ "belt_slice_rotation_angle", 45 },
{ "belt_slice_rotation_global", 1 },
{ "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 },
// Three layers, 0.6 mm: the lowest wall of each tilted layer is centred about 0.3 mm
// above the belt (half a line width in from the contact edge).
{ "close_fan_the_first_x_layers", 3 },
{ "full_fan_speed_layer", 0 },
{ "fan_min_speed", 100 },
{ "fan_max_speed", 100 },
{ "slow_down_layer_time", 1000 },
{ "fan_cooling_layer_time", 1001 },
{ "reduce_fan_stop_start_freq", 0 },
{ "machine_start_gcode", "T[initial_tool]\n" },
{ "layer_change_gcode", "G92 E0\n" },
});
const std::string gcode = slice({ cube(20) }, config);
REQUIRE(! gcode.empty());
// The markers are consumed by the cooling buffer and never reach the file.
CHECK(gcode.find(";_BELT_BAND") == std::string::npos);
// With this axis mapping machine Y is the height above the belt along the gantry. Walk
// the moves with the fan state: extrusions that stay within 0.45 mm of the belt are well
// inside the band and must print with the fan off; extrusions that stay 5 mm clear of it
// must print with it on. The first three slicing layers have the fan off altogether.
size_t in_band = 0, in_band_fan_on = 0, clear = 0, clear_fan_off = 0;
int layer = -1;
bool fan_on = false;
double y = 0.;
std::istringstream lines(gcode);
for (std::string line; std::getline(lines, line); ) {
if (boost::starts_with(line, ";LAYER_CHANGE")) {
++ layer;
} else if (boost::starts_with(line, "M107")) {
fan_on = false;
} else if (boost::starts_with(line, "M106")) {
const size_t s = line.find('S');
fan_on = s != std::string::npos && std::atof(line.c_str() + s + 1) > 0.;
} else if (boost::starts_with(line, "G1 ")) {
const size_t comment = line.find(';');
const std::string cmd = line.substr(0, comment);
const size_t ypos = cmd.find(" Y"), epos = cmd.find(" E");
if (ypos == std::string::npos)
continue;
const double y_new = std::atof(cmd.c_str() + ypos + 2);
const bool extruding = epos != std::string::npos && std::atof(cmd.c_str() + epos + 2) > 0.;
if (extruding && layer >= 3) {
if (std::max(y, y_new) < 0.45) {
++ in_band;
in_band_fan_on += fan_on;
} else if (std::min(y, y_new) > 5.) {
++ clear;
clear_fan_off += ! fan_on;
}
}
y = y_new;
}
}
CHECK(in_band > 20);
CHECK(in_band_fan_on == 0);
CHECK(clear > 20);
CHECK(clear_fan_off == 0);
}
// Organic supports under an overhang on a belt printer reach below the object's first layer,
// where the virtual belt raft layers sit at negative Z. The lowest of them used to get a
// negative height and abort slicing with a negative flow error.
TEST_CASE("Belt printers slice organic tree supports that reach the belt", "[Print][belt][Support]")
{
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
config.set_deserialize_strict({
{ "belt_printer", 1 },
{ "belt_slice_rotation", "x" },
{ "belt_slice_rotation_angle", 45 },
{ "belt_slice_rotation_global", 1 },
{ "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", "tree(auto)" },
{ "support_style", "organic" },
{ "machine_start_gcode", "T[initial_tool]\n" },
{ "layer_change_gcode", "G92 E0\n" },
});
std::string gcode;
REQUIRE_NOTHROW(gcode = slice({ TestMesh::overhang }, config));
CHECK(! gcode.empty());
}
+84 -2
View File
@@ -15,6 +15,7 @@
#include <limits>
#include <map>
#include <set>
#include <sstream>
#include <string>
#include "test_helpers.hpp" // get access to init_print, etc
@@ -919,12 +920,36 @@ TEST_CASE("Belt inner-only leading brim does not reject the prime tower or spira
CHECK_FALSE(print.objects().front()->has_belt_brim());
CHECK(print.validate().string.empty());
}
SECTION("a real inner brim still rejects the prime tower") {
// enable_prime_tower stays on for any multi-filament project, but a belt printer never
// prints the classic tower, so the setting alone must not cost the print its brim.
SECTION("a real inner brim is accepted with the prime tower setting on") {
Print print;
Model model;
init_inner_leading_with_prime_tower(print, model, 4);
CHECK(print.objects().front()->has_belt_brim());
CHECK_FALSE(print.validate().string.empty());
CHECK(print.validate().string.empty());
CHECK_FALSE(gcode(print).empty());
}
// A purge tower object is accepted too: the purge plan moves every object, apron
// bands included, onto one layer grid.
SECTION("a brim is accepted next to a belt purge tower object") {
DynamicPrintConfig config = belt_brim_multifilament_config(2, {
{ "brim_type", "outer_only" },
{ "brim_width", 4 },
{ "brim_object_gap", 0 },
{ "enable_belt_purge_tower", 1 },
});
const std::vector<std::vector<Slic3r::ConfigBase::SetDeserializeItem>> overrides {
{ { "extruder", 1 } }, { { "extruder", 2 } },
};
Print print;
Model model;
init_print({ cube(20), cube(20) }, print, model, config, &overrides);
model.objects.back()->config.set_key_value("belt_purge_tower_object", new ConfigOptionBool(true));
print.apply(model, config);
REQUIRE(print.has_belt_purge_tower());
CHECK(print.validate().string.empty());
CHECK_FALSE(gcode(print).empty());
}
}
@@ -1192,3 +1217,60 @@ TEST_CASE("Belt brim coexists with support material", "[SkirtBrim][belt]")
REQUIRE(! gc.empty());
CHECK(role_passes(gc, "brim") > 0);
}
// With a 0.3 mm first layer at 45 degrees the brim band on the belt is wider than one bead,
// so its lines go on the nominal lattice instead of at a fixed fraction of the band. A
// lattice line can then land where the belt is almost at the band's print_z; it must be
// moved uphill to the same 0.75 fraction the single-line case uses, not laid scraping the
// belt with its flow clamped to half a layer.
TEST_CASE("Belt brim lattice lines keep their clearance above the belt", "[SkirtBrim][belt]")
{
DynamicPrintConfig config = belt_brim_config();
config.set_deserialize_strict({
{ "layer_height", 0.3 },
{ "initial_layer_print_height", 0.3 },
{ "brim_type", "outer_only" },
{ "brim_width", 4 },
{ "brim_object_gap", 0 },
});
const std::string gcode = slice({ cube(20) }, config);
// Heights of the brim extrusions, from the ;HEIGHT: tags inside ;TYPE:Brim sections.
std::vector<double> brim_heights;
bool in_brim = false;
std::istringstream lines(gcode);
for (std::string line; std::getline(lines, line); ) {
if (boost::starts_with(line, ";TYPE:"))
in_brim = boost::starts_with(line, ";TYPE:Brim");
else if (in_brim && boost::starts_with(line, ";HEIGHT:"))
brim_heights.push_back(std::stod(line.substr(8)));
}
REQUIRE(! brim_heights.empty());
for (const double h : brim_heights) {
CHECK(h >= 0.75 * 0.3 - 1e-3);
CHECK(h <= 0.3 + 1e-3);
}
}
// The brim prints in the object's outer wall filament even when every extrusion of the object
// is offered to purging (flush_into_objects): the tool ordering registers the brim filament
// itself, so the writer always knows it.
TEST_CASE("Belt brim slices when every object is a flush target", "[SkirtBrim][belt]")
{
DynamicPrintConfig config = belt_brim_multifilament_config(2, {
{ "brim_type", "outer_only" },
{ "brim_width", 4 },
{ "brim_object_gap", 0 },
{ "flush_into_objects", 1 },
{ "flush_into_infill", 1 },
});
const std::vector<std::vector<Slic3r::ConfigBase::SetDeserializeItem>> overrides {
{ { "extruder", 1 } }, { { "extruder", 2 } },
};
Print print;
Model model;
init_print({ cube(20), cube(20) }, print, model, config, &overrides);
REQUIRE(print.validate().string.empty());
const std::string out = gcode(print);
CHECK(out.find(";TYPE:Brim") != std::string::npos);
}
+70
View File
@@ -1,3 +1,4 @@
#include <limits>
#include <catch2/catch_all.hpp>
#include "libslic3r/Arrange.hpp"
@@ -259,6 +260,75 @@ TEST_CASE("Arrange aligns the pile to a custom center", "[Arrange]")
require_no_overlap(items);
}
// A belt printer starts its parts at the leading end of the belt (best_object_pos 0.5, 0.05).
// Centring a pile on a point that close to the edge pushed everything longer than the room
// around it off the bed: four 90 mm parts on a 95 x 500 mm belt ended with one across the
// edge and one outside, with 290 mm of belt free behind them. The pile stops at the edge.
TEST_CASE("Arrange keeps a pile aligned near an edge on the bed", "[Arrange]")
{
const BoundingBox belt = bed(95, 500);
ArrangePolygons items = squares(4, 90.);
ArrangeParams params = quiet_params(scaled(2.));
params.align_center = Vec2d(0.5, 0.05);
arrange(items, belt, params);
coord_t lowest = std::numeric_limits<coord_t>::max();
for (const ArrangePolygon &ap : items) {
REQUIRE(ap.bed_idx == 0);
const BoundingBox bb = ap.transformed_poly().contour.bounding_box();
CHECK(belt.contains(bb));
lowest = std::min(lowest, bb.min.y());
}
// Snapped to the edge it was aimed at, less the spacing margin, not re-centred.
CHECK(lowest < scaled(10.));
require_no_overlap(items);
}
// On a belt the parts print in belt order, so two colours that alternate along the
// belt, or sit side by side, cost a filament change on every shared layer. Arrange
// keeps each colour together: no part shares belt length with a part of another
// colour, counting the tilted layers that run cot(angle) * height past its far edge,
// whichever end of the belt prints first.
TEST_CASE("Arrange groups the colours of a belt print along the belt", "[Arrange][belt]")
{
const bool reversed = GENERATE(false, true);
CAPTURE(reversed);
const BoundingBox belt = bed(95, 500);
ArrangePolygons items = squares(6, 30., 20.);
for (size_t i = 0; i < items.size(); ++i)
items[i].extrude_ids = { int(i % 3) + 1 }; // three colours, two parts each
ArrangeParams params = quiet_params(scaled(2.));
params.align_center = Vec2d(0.5, 0.05);
params.is_belt = true;
params.belt_axis = 1;
params.belt_reversed = reversed;
params.belt_tilt_slope = 1.f; // 45 degrees
arrange(items, belt, params);
require_no_overlap(items);
// Belt position in print order, so the same check serves both directions.
const coord_t dir = reversed ? -1 : 1;
auto start = [&](const ArrangePolygon &ap) { const BoundingBox bb = ap.transformed_poly().contour.bounding_box(); return dir * (reversed ? bb.max.y() : bb.min.y()); };
auto end = [&](const ArrangePolygon &ap) { const BoundingBox bb = ap.transformed_poly().contour.bounding_box(); return dir * (reversed ? bb.min.y() : bb.max.y()) + scaled(ap.height * params.belt_tilt_slope); };
for (const ArrangePolygon &ap : items) {
REQUIRE(ap.bed_idx == 0);
CHECK(belt.contains(ap.transformed_poly().contour.bounding_box()));
}
for (const ArrangePolygon &a : items)
for (const ArrangePolygon &b : items) {
if (a.extrude_ids == b.extrude_ids)
continue;
// The part printed later starts after the earlier one has finished.
const coord_t earlier_end = start(a) <= start(b) ? end(a) : end(b);
const coord_t later_start = std::max(start(a), start(b));
INFO("colour " << a.extrude_ids.front() << " vs " << b.extrude_ids.front());
CHECK(earlier_end <= later_start);
}
}
TEST_CASE("Sequential print floors the object distance by object height", "[Arrange]")
{
// The only place sequential-print clearance is enforced. The arrange menu offers
+44
View File
@@ -57,6 +57,50 @@ TEST_CASE("expand_mixed_filaments replaces mixed slots with their components", "
}
}
TEST_CASE("belt purge tower island count ignores virtual mixed slots", "[FilamentMixer][belt]")
{
// Regression for the "extra purge tower" on a belt printer with a mixed
// filament (MCTEST5). The belt purge prism is sized as
// n_islands = used_filaments.size() - 1
// and GUI::ensure_belt_purge_tower() collected those filaments straight off
// the model objects' extruder assignments. A mixed slot is VIRTUAL -- no
// nozzle carries it, and ToolOrdering::resolve_mixed_filaments() replaces it
// with its components before any G-code is emitted -- so counting it as a
// filament of its own provisions one island that can never be reached.
//
// MCTEST5: five cubes on extruders 1..5, where filament 5 is a 50/50 blend of
// filaments 2 and 4. The G-code uses only T0..T3 and reports
// "filament used [g] = 53.35, 141.11, 40.84, 107.23, 0.00" -- filament 5
// consumes nothing, exactly as a virtual slot should.
const std::vector<unsigned char> is_mixed = {0, 0, 0, 0, 1};
const std::vector<std::string> comp_strs = {"", "", "", "", "2,4"};
// The set the sizer used to see: slots 0..4 (filaments 1..5).
const std::vector<unsigned int> assigned = {0, 1, 2, 3, 4};
const auto physical = expand_mixed_filaments(assigned, is_mixed, comp_strs);
// Slot 4 dissolves into 1 and 3, which are already present.
REQUIRE(physical == std::vector<unsigned int>({0, 1, 2, 3}));
// Four physical filaments => three transitions => three islands, not four.
REQUIRE(int(physical.size()) - 1 == 3);
REQUIRE(int(assigned.size()) - 1 == 4); // what it produced before the fix
SECTION("A mixed slot whose components are otherwise unused still counts them") {
// Only the mixed slot is assigned: it must still yield its two components,
// i.e. one island, rather than collapsing to zero.
const auto only_mixed = expand_mixed_filaments({4}, is_mixed, comp_strs);
REQUIRE(only_mixed == std::vector<unsigned int>({1, 3}));
REQUIRE(int(only_mixed.size()) - 1 == 1);
}
SECTION("No mixed filaments anywhere leaves the set untouched") {
const std::vector<unsigned char> none_mixed = {0, 0, 0, 0, 0};
REQUIRE_FALSE(has_any_mixed_filament(none_mixed));
REQUIRE(expand_mixed_filaments(assigned, none_mixed, {"", "", "", "", ""}) == assigned);
}
}
TEST_CASE("check_mixed_filament_integrity flags dangling component references", "[FilamentMixer]")
{
const std::vector<unsigned char> is_mixed = {0, 0, 1};