mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-23 17:02:39 +00:00
## Summary Fixes for the `belt-printer` branch ahead of upstreaming, in two groups (10 commits). Targets `belt-printer` (not `main`) since group 2 fixes the not-yet-merged Belt Printer Brims feature. Every fix keeps non-belt (and brim-disabled) output unchanged; belt-only behavior is corrected. All changed translation units and the two test files were type-checked (`-fsyntax-only`); a full build + `ctest` still needs to run in an environment with current deps. ## Group 1 — pre-existing belt-printer regressions - **[HIGH] BuildVolume belt state not reset when leaving belt mode** — toggling belt off (or switching belt→normal with matching bed geometry) left the `BuildVolume` with `m_is_belt_printer=true` and inflated Y bounds, so out-of-bounds objects were treated as printable on a normal printer. - **[HIGH] `GCodeProcessorResult::reset()` didn't clear belt fields** (`belt_tilt_angle`, `belt_z_origin`, `preslice_remap_*`) — a reused result corrupted a normal print's start-gcode preview Z. - **[LOW-MED] `TreeSupport::drop_nodes`** — restored the single critical section around node invalidation (the two `valid=false` writes had been moved outside the mutex on the shared tree-support path); removed an unused local. - **[LOW] Support overhang hot paths** — avoid unconditional lower-layer polygon copies when there is no build-plate tilt (`SupportMaterial`, `TreeSupport3D`); untilted output matches upstream exactly. - **[LOW] Render loop** — hoisted the frame-invariant slope `up_direction`/`normal_z` (and their per-volume config lookup) out of the per-volume loop. - **[LOW] FDM-support "select by angle"** — restored the exact upstream threshold when the build plate is untilted (the generalized form differed for non-uniformly-scaled objects); tilted-gravity form kept only under tilt. - **[LOW] Printer tab tilt sync** — only clears the belt-derived `build_plate_tilt` on a genuine in-place belt→off toggle (tracked, seeded on preset load), no longer wiping a manually-set tilt. - **[LOW / opt-in] Axis-remap G-code emission** — always emit full XYZ under an active `gcode_remap_*`, apply the remap on all base `travel_to_xyz` destinations, fall back to a linear lift for spiral/arc under remap, sync `set_axis_remap` each export; fixed belt first-layer travel speed. Identity/default output unchanged. ## Group 2 — Belt Printer Brims (#15155) fixes - **[CRITICAL] Dropped brim at first belt contact** — a coincident brim band on an object layer with no extrusion pass (zero-extrusion leading slice, or belt support below the Z=0 floor with no coinciding object extrusion) was never emitted. Now each coincident band's brim filament is registered in `ToolOrdering`, each band is emitted exactly once in its brim-filament pass, and an end-of-layer orphan sweep emits any band whose object layer produced no visit. - **[Multi-extruder] Wrong tool / double emission** — apron and coincident bands now print once, in the correct brim-filament pass, brim-first (were previously emitted with the active tool and could double-emit per filament plan). Single-extruder / single-object output is byte-identical apart from the previously-dropped bands now printing. - **Inner-only predicate** — `has_belt_brim()` no longer reports a brim (and no longer rejects the prime tower / spiral vase) for `inner_only` + `brim_width=0` + leading/extra > 0, which produces no inner geometry; mirrored in `wants_brim`. - **ToolOrdering raft-gap comment** — clarified why raft-gap synthesis is suppressed for all belt printers (belt has no rafts; sub-object-bottom layers are apron / belt-support-below-floor / lead-in). No behavior change. - **Tests** — deterministic coverage: brim present at first belt contact (support on/off), brim-before-perimeters once (no drop/double), single- and multi-extruder tool selection with no doubling, multi-object per-filament ordering, inner-only+leading-only not rejecting prime tower/spiral, and inner-ring / leading-edge-only geometry units. ## Testing - `-fsyntax-only` passes for all 16 changed source TUs + 2 test TUs against this branch. - Please run the full build and `ctest -R 'SkirtBrim|BeltBrim'` before merging. ## Known follow-up (out of scope) `extrude_arc_to_xy` does not remap its I-J center, so arc-fitted *extrusions* under standalone axis-remap would be geometrically wrong — a separate fix if that combination is supported. Opened as **draft**.
This commit is contained in:
@@ -264,7 +264,9 @@ std::string BeltGCodeWriter::travel_to_xyz(const Vec3d &point, const std::string
|
||||
// Belt mode: always emit full XYZ
|
||||
GCodeG1Formatter w;
|
||||
w.emit_xyz(point_on_plate);
|
||||
w.emit_f(this->config.travel_speed.get_at(m_cached_extruder_idx) * 60.0);
|
||||
// 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;
|
||||
|
||||
+112
-18
@@ -2952,16 +2952,18 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
|
||||
this->init_belt_writer(print, 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
|
||||
// the identity mapping (0,1,2) — so a reused writer never retains a stale
|
||||
// non-identity mapping from a prior export. has_axis_remap() returns false at
|
||||
// identity, so identity/default output stays unchanged.
|
||||
{
|
||||
int rx = int(print.config().gcode_remap_x.value);
|
||||
int ry = int(print.config().gcode_remap_y.value);
|
||||
int rz = int(print.config().gcode_remap_z.value);
|
||||
if (rx != 0 || ry != 1 || rz != 2) {
|
||||
m_writer->set_axis_remap(rx, ry, rz);
|
||||
BoundingBoxf bbox_bed(print.config().printable_area.values);
|
||||
m_writer->set_build_volume_max(Vec3d(bbox_bed.max.x(), bbox_bed.max.y(),
|
||||
print.config().printable_height.value));
|
||||
}
|
||||
m_writer->set_axis_remap(rx, ry, rz);
|
||||
BoundingBoxf bbox_bed(print.config().printable_area.values);
|
||||
m_writer->set_build_volume_max(Vec3d(bbox_bed.max.x(), bbox_bed.max.y(),
|
||||
print.config().printable_height.value));
|
||||
}
|
||||
|
||||
// Build the FirstLayerPlane evaluator. When inactive (non-belt printers
|
||||
@@ -5889,16 +5891,12 @@ LayerResult GCode::process_layer(
|
||||
//BBS: set layer time fan speed after layer change gcode
|
||||
gcode += ";_SET_FAN_SPEED_CHANGING_LAYER\n";
|
||||
|
||||
// Belt printers: an apron band prints below its own object's first layer, but with
|
||||
// several objects on the belt another one can already be printing at this print_z.
|
||||
// The layer then has an object layer and takes this ordinary path instead of the
|
||||
// brim-only branch, so the band has to be emitted here or it would be dropped.
|
||||
// Before any object extrusion at this Z, as the brim must go down first.
|
||||
if (print.has_belt_brim()) {
|
||||
const Vec2d saved_origin = m_origin;
|
||||
gcode += this->emit_belt_brim_bands(print, layers, single_object_instance_idx);
|
||||
this->set_origin(saved_origin);
|
||||
}
|
||||
// Belt printers: ordinary-layer apron bands (a band whose print_z coincides with an
|
||||
// object/support layer, so it takes this path rather than the brim-only branch) are
|
||||
// NOT emitted here anymore. They used to be laid down with whatever tool happened to
|
||||
// be active; instead they are now emitted inside the extruder loop below, in their
|
||||
// own brim-filament pass and before that pass's object extrusion, so the brim goes
|
||||
// down first with the correct tool. See the emit_belt_brim_for_extruder call.
|
||||
|
||||
//Calibration Layer-specific GCode
|
||||
// ORCA-Belt: on belt printers the calibration object is counter-rotated to
|
||||
@@ -6582,6 +6580,49 @@ LayerResult GCode::process_layer(
|
||||
|
||||
// Extrude the skirt, brim, support, perimeters, infill ordered by the extruders.
|
||||
m_skirt_group_done.resize(print.skirt_brim_groups().size());
|
||||
|
||||
// Belt brim bookkeeping. A coincident belt_brim_by_layer band must be emitted
|
||||
// exactly once, in its object's brim-filament pass; this records which have gone
|
||||
// down so the in-visit emit and the end-of-layer orphan sweep never double it.
|
||||
// Key = (LayerToPrint index, instance_id).
|
||||
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
|
||||
// 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.
|
||||
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 <p : 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;
|
||||
};
|
||||
|
||||
for (unsigned int extruder_id : layer_tools.extruders)
|
||||
{
|
||||
if (print.config().skirt_type == stCombined && !print.skirt_brim_groups().empty()) {
|
||||
@@ -6679,6 +6720,16 @@ LayerResult GCode::process_layer(
|
||||
if (layer_tools.has_wipe_tower && m_wipe_tower)
|
||||
m_last_processor_extrusion_role = erWipeTower;
|
||||
|
||||
// Belt printers: now that this pass's tool is selected, lay down any ordinary-layer
|
||||
// apron band whose brim filament is this extruder, before the object extrusion at
|
||||
// this Z (brim goes down first, with the correct tool). Restore the origin so the
|
||||
// object-setup code below is unaffected.
|
||||
if (print.has_belt_brim()) {
|
||||
const Vec2d saved_origin = m_origin;
|
||||
gcode += emit_belt_brim_for_extruder(extruder_id);
|
||||
this->set_origin(saved_origin);
|
||||
}
|
||||
|
||||
auto &filament_plan = filament_to_print_instances[extruder_id];
|
||||
std::vector<InstanceToPrint> &instances_to_print = filament_plan.first;
|
||||
const std::vector<InstanceVisit> &instance_visits = filament_plan.second;
|
||||
@@ -6695,8 +6746,20 @@ LayerResult GCode::process_layer(
|
||||
const LayerToPrint &layer_to_print = layers[instance_to_print.layer_id];
|
||||
if (visit.first_visit && print_wipe_extrusions == (is_anything_overridden ? 1 : 0)) {
|
||||
gcode += generate_object_skirt_group(print, instance_to_print.print_object, instance_to_print.instance_id, layer_tools, layer, extruder_id);
|
||||
gcode += generate_object_brim(print, instance_to_print.print_object, instance_to_print.instance_id, first_layer,
|
||||
layer_to_print.object_layer);
|
||||
const PrintObject &vobj = instance_to_print.print_object;
|
||||
if (vobj.has_belt_brim()) {
|
||||
// Coincident belt brim: emit once, only in this object's brim-filament
|
||||
// pass (extruder_id and belt_brim_filament()-1 are both 0-based here),
|
||||
// and dedup on the LayerToPrint index (not Layer::id()) so the orphan
|
||||
// sweep below never re-emits it.
|
||||
if (extruder_id == (unsigned int)(vobj.belt_brim_filament() - 1) &&
|
||||
belt_brim_emitted.insert({ instance_to_print.layer_id, instance_to_print.instance_id }).second)
|
||||
gcode += generate_object_brim(print, vobj, instance_to_print.instance_id, first_layer,
|
||||
layer_to_print.object_layer);
|
||||
} else {
|
||||
gcode += generate_object_brim(print, vobj, instance_to_print.instance_id, first_layer,
|
||||
layer_to_print.object_layer);
|
||||
}
|
||||
}
|
||||
|
||||
// To control print speed of the 1st object layer printed over raft interface.
|
||||
@@ -6884,6 +6947,37 @@ LayerResult GCode::process_layer(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Belt brim orphan sweep (Finding C). A coincident belt_brim_by_layer band lives on
|
||||
// an object layer, but that layer can yield no InstanceVisit above - a zero-extrusion
|
||||
// lead-in slice with no coinciding support - so the in-visit emit never fired and the
|
||||
// band would be dropped. Emit any such band exactly once here, keyed the same way as
|
||||
// the in-visit emit so already-printed bands are skipped. These orphan layers carry
|
||||
// no object material, so ending on the brim's position is harmless; we still save and
|
||||
// restore m_origin, and only toolchange when the brim filament differs from the active
|
||||
// one - a no-op on single-extruder prints, keeping their output unchanged.
|
||||
if (print.has_belt_brim()) {
|
||||
const Vec2d saved_origin = m_origin;
|
||||
for (const LayerToPrint <p : layers) {
|
||||
const PrintObject *obj = ltp.original_object;
|
||||
if (obj == nullptr || ! obj->has_belt_brim() || ltp.object_layer == nullptr)
|
||||
continue;
|
||||
const size_t ltp_idx = size_t(<p - layers.data());
|
||||
const unsigned int brim0 = (unsigned int)(obj->belt_brim_filament() - 1);
|
||||
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) ? obj->instances().size()
|
||||
: single_object_instance_idx + 1;
|
||||
for (size_t instance_id = i_begin; instance_id < i_end && instance_id < obj->instances().size(); ++ instance_id) {
|
||||
if (! belt_brim_emitted.insert({ ltp_idx, instance_id }).second)
|
||||
continue;
|
||||
if (m_writer->filament() == nullptr || m_writer->filament()->id() != brim0)
|
||||
gcode += this->set_extruder(brim0, print_z);
|
||||
gcode += generate_object_brim(print, *obj, instance_id, first_layer, ltp.object_layer);
|
||||
}
|
||||
}
|
||||
this->set_origin(saved_origin);
|
||||
}
|
||||
|
||||
if (first_layer) {
|
||||
for (auto iter = by_extruder.begin(); iter != by_extruder.end(); ++iter) {
|
||||
if (!iter->second.empty())
|
||||
|
||||
@@ -2531,6 +2531,11 @@ void GCodeProcessorResult::reset() {
|
||||
timelapse_warning_code = 0;
|
||||
printable_height = 0.0f;
|
||||
machine_frame_transform_active = false;
|
||||
belt_tilt_angle = 0.f;
|
||||
belt_z_origin = 0.f;
|
||||
preslice_remap_x = RemapAxis::PosX;
|
||||
preslice_remap_y = RemapAxis::PosY;
|
||||
preslice_remap_z = RemapAxis::PosZ;
|
||||
settings_ids.reset();
|
||||
filaments_count = 0;
|
||||
backtrace_enabled = false;
|
||||
|
||||
@@ -876,14 +876,9 @@ void ToolOrdering::collect_extruders(const PrintObject &object, const std::vecto
|
||||
// push. Deliberately not layer_tools.has_object, which drives skirt marking
|
||||
// and wiping overrides.
|
||||
if (! object.belt_brim_prologue().empty()) {
|
||||
unsigned int brim_filament = 0;
|
||||
for (size_t i = 0; i < object.num_printing_regions(); ++ i) {
|
||||
const unsigned int f = object.printing_region(i).config().outer_wall_filament_id.value;
|
||||
if (f > 0 && (brim_filament == 0 || f < brim_filament))
|
||||
brim_filament = f;
|
||||
}
|
||||
if (brim_filament == 0)
|
||||
brim_filament = 1;
|
||||
// 1-based, same domain the object/support pushes above use; reindexed to 0-based
|
||||
// with the rest of the list later.
|
||||
const unsigned int brim_filament = object.belt_brim_filament();
|
||||
for (const BeltBrimBand &band : object.belt_brim_prologue()) {
|
||||
if (band.fills.empty())
|
||||
continue;
|
||||
@@ -893,6 +888,25 @@ void ToolOrdering::collect_extruders(const PrintObject &object, const std::vecto
|
||||
}
|
||||
}
|
||||
|
||||
// Coincident brim bands (belt_brim_by_layer) print ON an object layer rather than
|
||||
// below it, but that layer can produce no InstanceVisit in process_layer - a
|
||||
// zero-extrusion lead-in slice with no coinciding support - and the band would then
|
||||
// be silently dropped. Register the brim filament on every layer that carries a
|
||||
// coincident band, in the same 1-based domain as the prologue push above, so a brim
|
||||
// pass always exists there.
|
||||
if (object.has_belt_brim()) {
|
||||
const unsigned int brim_filament = object.belt_brim_filament();
|
||||
const auto &by_layer = object.belt_brim_by_layer();
|
||||
const size_t n = std::min(by_layer.size(), object.layers().size());
|
||||
for (size_t i = 0; i < n; ++ i) {
|
||||
if (by_layer[i].empty())
|
||||
continue;
|
||||
LayerTools &layer_tools = this->tools_for_layer(object.layers()[i]->print_z);
|
||||
layer_tools.extruders.push_back(brim_filament);
|
||||
layer_tools.has_belt_brim = true;
|
||||
}
|
||||
}
|
||||
|
||||
for (auto& layer : m_layer_tools) {
|
||||
// Sort and remove duplicates
|
||||
sort_remove_duplicates(layer.extruders);
|
||||
@@ -940,6 +954,14 @@ void ToolOrdering::fill_wipe_tower_partitions(const PrintConfig &config, coordf_
|
||||
// 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.
|
||||
//
|
||||
// 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
|
||||
// apron, belt support printed below Z0, or the object's own lead-in - and none of
|
||||
// them is ever raft, because Print::validate() rejects raft_layers>0 on a belt
|
||||
// printer outright. Narrowing this to has_belt_brim would reclassify
|
||||
// belt-support-below-floor layers as raft on brim-less belt prints and reintroduce
|
||||
// the negative-Z wipe tower, so the broad belt_printer gate is correct.
|
||||
const bool belt_no_raft_gap = config.belt_printer.value;
|
||||
for (LayerTools < : m_layer_tools)
|
||||
lt.has_wipe_tower |= (lt.has_object && (config.timelapse_type == TimelapseType::tlSmooth || lt.wipe_tower_partitions > 0))
|
||||
|
||||
@@ -936,7 +936,10 @@ std::string GCodeWriter::travel_to_xyz(const Vec3d &point, const std::string &co
|
||||
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;
|
||||
GCodeG1Formatter w0;
|
||||
w0.emit_xyz(slope_top_point);
|
||||
// A slope lift is a straight (linear) diagonal move, so remapping its
|
||||
// endpoint is exact. Route the destination through apply_axis_remap()
|
||||
// when a remap is active (no-op at identity).
|
||||
w0.emit_xyz(has_axis_remap() ? apply_axis_remap(slope_top_point) : slope_top_point);
|
||||
w0.emit_f(travel_speed * 60.0);
|
||||
//BBS
|
||||
w0.emit_comment(GCodeWriter::full_gcode_comment, comment);
|
||||
@@ -950,7 +953,14 @@ std::string GCodeWriter::travel_to_xyz(const Vec3d &point, const std::string &co
|
||||
std::string xy_z_move;
|
||||
{
|
||||
GCodeG1Formatter w0;
|
||||
if (this->is_current_position_clear()) {
|
||||
if (has_axis_remap()) {
|
||||
// Remap may couple XY with Z; emit full XYZ in machine coordinates.
|
||||
w0.emit_xyz(apply_axis_remap(target));
|
||||
w0.emit_f(travel_speed * 60.0);
|
||||
w0.emit_comment(GCodeWriter::full_gcode_comment, comment);
|
||||
xy_z_move = w0.string();
|
||||
}
|
||||
else if (this->is_current_position_clear()) {
|
||||
w0.emit_xyz(target);
|
||||
w0.emit_f(travel_speed * 60.0);
|
||||
w0.emit_comment(GCodeWriter::full_gcode_comment, comment);
|
||||
@@ -988,7 +998,13 @@ std::string GCodeWriter::travel_to_xyz(const Vec3d &point, const std::string &co
|
||||
Vec3d point_on_plate = { dest_point(0) - m_x_offset, dest_point(1) - m_y_offset, dest_point(2) };
|
||||
std::string out_string;
|
||||
GCodeG1Formatter w;
|
||||
if (!this->is_current_position_clear())
|
||||
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_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()));
|
||||
@@ -1053,6 +1069,14 @@ std::string GCodeWriter::_travel_to_z(double z, const std::string &comment)
|
||||
|
||||
std::string GCodeWriter::_spiral_travel_to_z(double z, const Vec2d &ij_offset, const std::string &comment)
|
||||
{
|
||||
// A circular XY arc / spiral lift cannot be correctly axis-remapped by
|
||||
// transforming only its endpoint: the arc plane (G17/XY) and the I-J center
|
||||
// would change under the remap. When an axis remap is active, fall back to a
|
||||
// plain linear lift instead of emitting a possibly-wrong spiral/arc. This
|
||||
// single guard covers every spiral call site (lazy/eager lift and travel_to_xyz).
|
||||
if (has_axis_remap())
|
||||
return _travel_to_z(z, comment);
|
||||
|
||||
std::string output;
|
||||
double speed = this->config.travel_speed_z.get_at(m_cached_extruder_idx);
|
||||
|
||||
@@ -1199,14 +1223,19 @@ std::string GCodeWriter::extrude_to_xyz(const Vec3d &point, double dE, const std
|
||||
//BBS: take plate offset into consider
|
||||
Vec3d point_on_plate = { point(0) - m_x_offset, point(1) - m_y_offset, point(2) };
|
||||
|
||||
if (has_axis_remap())
|
||||
point_on_plate = apply_axis_remap(point_on_plate);
|
||||
|
||||
GCodeG1Formatter w;
|
||||
if (z_changed)
|
||||
if (has_axis_remap()) {
|
||||
// z_changed was computed from the ORIGINAL slicing Z, but an axis remap can
|
||||
// make machine-Z depend on slicing X/Y. An X/Y-only move (slicing-Z
|
||||
// unchanged) would then drop the required machine-Z word, so always emit
|
||||
// full XYZ whenever a remap is active.
|
||||
point_on_plate = apply_axis_remap(point_on_plate);
|
||||
w.emit_xyz(point_on_plate);
|
||||
else
|
||||
} else if (z_changed) {
|
||||
w.emit_xyz(point_on_plate);
|
||||
} else {
|
||||
w.emit_xy(Vec2d(point_on_plate.x(), point_on_plate.y()));
|
||||
}
|
||||
if (!force_no_extrusion)
|
||||
w.emit_e(filament()->E());
|
||||
//BBS
|
||||
|
||||
@@ -1374,9 +1374,15 @@ StringObjectException Print::validate(std::vector<StringObjectException> *warnin
|
||||
|
||||
for (const PrintObject *object : m_objects) {
|
||||
const PrintObjectConfig &ocfg = object->config();
|
||||
// Mirror PrintObject::has_belt_brim(): an inner-only brim needs a positive
|
||||
// brim_width (leading/extra widen only the outer ring), so keep this
|
||||
// predicate in step or the belt-brim warnings below would fire for a brim
|
||||
// that has_belt_brim() rejects.
|
||||
const bool wants_brim = ocfg.brim_type != btNoBrim
|
||||
&& (ocfg.brim_width.value > 0. || ocfg.leading_brim_length.value > 0.
|
||||
|| ocfg.extra_brim_width.value > 0.);
|
||||
&& (ocfg.brim_type == btInnerOnly
|
||||
? ocfg.brim_width.value > 0.
|
||||
: (ocfg.brim_width.value > 0. || ocfg.leading_brim_length.value > 0.
|
||||
|| ocfg.extra_brim_width.value > 0.));
|
||||
if (! wants_brim)
|
||||
continue;
|
||||
|
||||
|
||||
@@ -392,6 +392,12 @@ public:
|
||||
// trimming and the spiral vase probe, and widening it would perturb belt
|
||||
// support output.
|
||||
bool has_belt_brim() const;
|
||||
// Brim filament for this object's belt brim, returned in the 1-based domain of
|
||||
// PrintRegion::outer_wall_filament_id and the raw values pushed into
|
||||
// LayerTools::extruders in ToolOrdering::collect_extruders (ToolOrdering reindexes
|
||||
// the whole list to 0-based afterwards). Lowest positive outer_wall_filament_id
|
||||
// over the printing regions, 1 if none is explicitly set.
|
||||
unsigned int belt_brim_filament() const;
|
||||
// False when this object's instances sit at different points ALONG the belt, which
|
||||
// would need a separate set of bands each. Public so validate() can explain it.
|
||||
bool belt_brim_instances_compatible() const;
|
||||
|
||||
@@ -1171,12 +1171,37 @@ bool PrintObject::has_belt_brim() const
|
||||
return false;
|
||||
if (m_config.brim_type == btNoBrim)
|
||||
return false;
|
||||
if (m_config.brim_width.value <= 0. && m_config.leading_brim_length.value <= 0.
|
||||
&& m_config.extra_brim_width.value <= 0.)
|
||||
// An inner-only brim has no leading/extra geometry: leading_brim_length and
|
||||
// extra_brim_width both widen the OUTER ring, which btInnerOnly never emits, so it
|
||||
// produces nothing unless brim_width itself is positive. Every other brim type is
|
||||
// satisfied by any one of the three widths. Requiring the width here (instead of
|
||||
// "any width") stops has_belt_brim() - and therefore Print::validate() - from
|
||||
// rejecting the prime tower / spiral vase for a brim that would never be drawn.
|
||||
if (m_config.brim_type == btInnerOnly) {
|
||||
if (m_config.brim_width.value <= 0.)
|
||||
return false;
|
||||
} else if (m_config.brim_width.value <= 0. && m_config.leading_brim_length.value <= 0.
|
||||
&& m_config.extra_brim_width.value <= 0.) {
|
||||
return false;
|
||||
}
|
||||
return ! this->has_raft();
|
||||
}
|
||||
|
||||
unsigned int PrintObject::belt_brim_filament() const
|
||||
{
|
||||
// 1-based, matching PrintRegion::outer_wall_filament_id and the raw values pushed
|
||||
// into LayerTools::extruders in ToolOrdering::collect_extruders (the whole list is
|
||||
// reindexed to 0-based later). Lowest positive outer-wall filament over the
|
||||
// printing regions; 1 when none is explicitly set.
|
||||
unsigned int brim_filament = 0;
|
||||
for (size_t i = 0; i < this->num_printing_regions(); ++ i) {
|
||||
const unsigned int f = this->printing_region(i).config().outer_wall_filament_id.value;
|
||||
if (f > 0 && (brim_filament == 0 || f < brim_filament))
|
||||
brim_filament = f;
|
||||
}
|
||||
return brim_filament == 0 ? 1u : brim_filament;
|
||||
}
|
||||
|
||||
bool PrintObject::belt_brim_instances_compatible() const
|
||||
{
|
||||
// One set of bands is shared by every instance of this object, so they must all sit at
|
||||
|
||||
@@ -1475,6 +1475,19 @@ static inline ExPolygons detect_overhangs(
|
||||
}
|
||||
}
|
||||
|
||||
// Apply build plate tilt: shift lower layer polygons to simulate tilted gravity.
|
||||
// This is loop-invariant across regions, so compute it once here.
|
||||
const Polygons *effective_lower = &lower_layer_polygons;
|
||||
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);
|
||||
effective_lower = &tilted_lower;
|
||||
}
|
||||
|
||||
for (LayerRegion *layerm : layer.regions()) {
|
||||
// Extrusion width accounts for the roundings of the extrudates.
|
||||
// It is the maximum widh of the extrudate.
|
||||
@@ -1491,17 +1504,9 @@ static inline ExPolygons detect_overhangs(
|
||||
// Overhang polygons for this layer and region.
|
||||
Polygons diff_polygons;
|
||||
Polygons layerm_polygons = to_polygons(layerm->slices.surfaces);
|
||||
// Apply build plate tilt: shift lower layer polygons to simulate tilted gravity
|
||||
Polygons effective_lower = lower_layer_polygons;
|
||||
if (has_tilt) {
|
||||
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(effective_lower, tilt_shift);
|
||||
}
|
||||
if (lower_layer_offset == 0.f) {
|
||||
// Support everything.
|
||||
diff_polygons = diff(layerm_polygons, effective_lower);
|
||||
diff_polygons = diff(layerm_polygons, *effective_lower);
|
||||
if (buildplate_only) {
|
||||
// Don't support overhangs above the top surfaces.
|
||||
// This step is done before the contact surface is calculated by growing the overhang region.
|
||||
@@ -1512,7 +1517,7 @@ static inline ExPolygons detect_overhangs(
|
||||
//FIXME cache the lower layer offset if this layer has multiple regions.
|
||||
diff_polygons =
|
||||
diff(layerm_polygons,
|
||||
expand(effective_lower, lower_layer_offset, SUPPORT_SURFACES_OFFSET_PARAMETERS));
|
||||
expand(*effective_lower, lower_layer_offset, SUPPORT_SURFACES_OFFSET_PARAMETERS));
|
||||
if (buildplate_only && ! annotations.buildplate_covered[layer_id].empty()) {
|
||||
// Don't support overhangs above the top surfaces.
|
||||
// This step is done before the contact surface is calculated by growing the overhang region.
|
||||
@@ -1522,7 +1527,7 @@ static inline ExPolygons detect_overhangs(
|
||||
// Offset the support regions back to a full overhang, restrict them to the full overhang.
|
||||
// This is done to increase size of the supporting columns below, as they are calculated by
|
||||
// propagating these contact surfaces downwards.
|
||||
diff_polygons = diff(intersection(expand(diff_polygons, lower_layer_offset, SUPPORT_SURFACES_OFFSET_PARAMETERS), layerm_polygons), effective_lower);
|
||||
diff_polygons = diff(intersection(expand(diff_polygons, lower_layer_offset, SUPPORT_SURFACES_OFFSET_PARAMETERS), layerm_polygons), *effective_lower);
|
||||
}
|
||||
//FIXME add user defined filtering here based on minimal area or minimum radius or whatever.
|
||||
|
||||
|
||||
@@ -2859,7 +2859,6 @@ void TreeSupport::drop_nodes()
|
||||
const size_t tip_layers = base_radius / layer_height; //The number of layers to be shrinking the circle to create a tip. This produces a 45 degree angle.
|
||||
const coordf_t radius_sample_resolution = m_ts_data->m_radius_sample_resolution;
|
||||
const bool support_on_buildplate_only = config.support_on_build_plate_only.value;
|
||||
const size_t top_interface_layers = config.support_interface_top_layers.value;
|
||||
const auto belt_floor_mode = m_print_config->belt_support_floor_mode.value;
|
||||
const bool has_belt_floor = std::abs(m_slicing_params.belt_floor_shear_factor) > EPSILON
|
||||
&& belt_floor_mode == BeltSupportFloorMode::GeneratorOnly;
|
||||
@@ -3100,19 +3099,21 @@ void TreeSupport::drop_nodes()
|
||||
// Treat as object-surface termination (not buildplate) so
|
||||
// the node gets floor/interface areas instead of base pads.
|
||||
if (has_belt_floor && print_z_next <= belt_floor_print_z(next_position)) {
|
||||
std::scoped_lock lock(m_ts_data->m_mutex);
|
||||
node_parent->to_buildplate = false;
|
||||
neighbour->valid = false;
|
||||
p_node->valid = false;
|
||||
} else {
|
||||
const bool to_buildplate = !is_inside_ex(get_collision(0, obj_layer_nr_next), next_position);
|
||||
SupportNode* next_node = m_ts_data->create_node(next_position, node_parent->distance_to_top + 1, obj_layer_nr_next,
|
||||
node_parent->support_roof_layers_below - (node_parent->distance_to_top >= 0 ? 1 : 0),
|
||||
to_buildplate, node_parent, print_z_next, height_next);
|
||||
get_max_move_dist(next_node);
|
||||
m_ts_data->m_mutex.lock();
|
||||
std::scoped_lock lock(m_ts_data->m_mutex);
|
||||
contact_nodes[layer_nr_next].push_back(next_node);
|
||||
m_ts_data->m_mutex.unlock();
|
||||
neighbour->valid = false;
|
||||
p_node->valid = false;
|
||||
}
|
||||
neighbour->valid = false;
|
||||
p_node->valid = false;
|
||||
}
|
||||
else if (neighbours.size() > 1) //Don't merge leaf nodes because we would then incur movement greater than the maximum move distance.
|
||||
{
|
||||
|
||||
@@ -260,14 +260,17 @@ static std::vector<std::pair<TreeSupportSettings, std::vector<size_t>>> group_me
|
||||
} else
|
||||
lower_layer_offset = scaled<float>(lower_layer.height / tan_threshold);
|
||||
// Apply build plate tilt: shift lower layer polygons to simulate tilted gravity
|
||||
Polygons lower_src = to_polygons(lower_layer.lslices_extrudable);
|
||||
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);
|
||||
lower_layer_offseted = offset(lower_src, lower_layer_offset);
|
||||
} else {
|
||||
lower_layer_offseted = offset(lower_layer.lslices_extrudable, lower_layer_offset);
|
||||
}
|
||||
Polygons lower_layer_offseted = offset(lower_src, lower_layer_offset);
|
||||
overhangs = diff(current_layer.lslices_extrudable, lower_layer_offseted);
|
||||
if (lower_layer_offset == 0) {
|
||||
raw_overhangs = overhangs;
|
||||
|
||||
+15
-16
@@ -1075,6 +1075,20 @@ void GLVolumeCollection::render(GLVolumeCollection::ERenderType type,
|
||||
|
||||
const float support_normal_z = get_selection_support_normal_z();
|
||||
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
|
||||
for (GLVolumeWithIdAndZ& volume : to_render) {
|
||||
#if ENABLE_MODIFIERS_ALWAYS_TRANSPARENT
|
||||
if (type == ERenderType::Transparent) {
|
||||
@@ -1132,21 +1146,6 @@ void GLVolumeCollection::render(GLVolumeCollection::ERenderType type,
|
||||
shader->set_uniform("print_volume.type", -1);
|
||||
}
|
||||
|
||||
const float normal_z = get_selection_support_normal_z();
|
||||
|
||||
// Compute up direction accounting for build plate tilt
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
// Per-extruder printable-height shading. The flag is set to
|
||||
// 2.0 only for multi-extruder printers (two per-extruder heights); otherwise it is forced to 0.0
|
||||
// on every render so no stale flag survives a multi->single-extruder plate switch, keeping the
|
||||
@@ -1167,7 +1166,7 @@ void GLVolumeCollection::render(GLVolumeCollection::ERenderType type,
|
||||
shader->set_uniform("volume_world_matrix", volume.first->world_matrix());
|
||||
shader->set_uniform("slope.actived", m_slope.isGlobalActive && !volume.first->is_modifier && !volume.first->is_wipe_tower);
|
||||
shader->set_uniform("slope.volume_world_normal_matrix", static_cast<Matrix3f>(volume.first->world_matrix().matrix().block(0, 0, 3, 3).inverse().transpose().cast<float>()));
|
||||
shader->set_uniform("slope.normal_z", normal_z);
|
||||
shader->set_uniform("slope.normal_z", support_normal_z);
|
||||
shader->set_uniform("slope.up_direction", up_direction);
|
||||
|
||||
#if ENABLE_ENVIRONMENT_MAP
|
||||
|
||||
@@ -567,7 +567,12 @@ void GLGizmoFdmSupports::select_facets_by_angle(float threshold_deg, bool block)
|
||||
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;
|
||||
Vec3d gravity_dir = Vec3d(-tan(tilt_y_rad), -tan(tilt_x_rad), -1.0).normalized();
|
||||
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();
|
||||
|
||||
int mesh_id = -1;
|
||||
for (const ModelVolume* mv : mo->volumes) {
|
||||
@@ -578,7 +583,16 @@ void GLGizmoFdmSupports::select_facets_by_angle(float threshold_deg, bool block)
|
||||
|
||||
const Transform3d trafo_matrix = mi->get_matrix_no_offset() * mv->get_matrix_no_offset();
|
||||
Vec3f down = (trafo_matrix.inverse() * gravity_dir).cast<float>().normalized();
|
||||
float dot_limit = std::cos(threshold);
|
||||
float dot_limit;
|
||||
if (!has_tilt) {
|
||||
// Exact upstream computation: threshold derived from a tilted limit
|
||||
// vector transformed into mesh space, so non-uniform/mirror/shear
|
||||
// transforms behave identically to upstream.
|
||||
Vec3f limit = (trafo_matrix.inverse() * Vec3d(std::sin(threshold), 0, -std::cos(threshold))).cast<float>().normalized();
|
||||
dot_limit = limit.dot(down);
|
||||
} else {
|
||||
dot_limit = std::cos(threshold);
|
||||
}
|
||||
|
||||
// Now calculate dot product of vert_direction and facets' normals.
|
||||
int idx = 0;
|
||||
|
||||
@@ -12387,6 +12387,12 @@ void Plater::priv::set_bed_shape(const Pointfs &shape,
|
||||
// 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);
|
||||
|
||||
+32
-11
@@ -5875,6 +5875,18 @@ if (is_marlin_flavor)
|
||||
// this gets executed after preset is loaded and before GUI fields are updated
|
||||
void TabPrinter::on_preset_loaded()
|
||||
{
|
||||
// R8: reset the belt-tilt transition tracking to reflect the freshly loaded preset WITHOUT
|
||||
// running update_fff()'s reset logic. on_preset_loaded() is called from Tab::load_current_preset()
|
||||
// on every printer preset load, right before update()->update_fff(). Seeding m_was_belt_printer
|
||||
// from the loaded preset's belt_printer flag means a preset switch (belt preset -> non-belt preset)
|
||||
// enters update_fff() with m_was_belt_printer==false, so the belt->off clear branch is skipped and
|
||||
// the newly loaded preset's manual tilt is preserved. An in-place belt toggle does NOT go through
|
||||
// here (only through on_value_change->update), so m_was_belt_printer stays true there and the clear
|
||||
// still fires. Seed m_belt_synced_tilt from the loaded tilt as a safeguard.
|
||||
m_was_belt_printer = m_config->opt_bool("belt_printer");
|
||||
m_belt_synced_tilt_x = m_config->opt_float("build_plate_tilt_x");
|
||||
m_belt_synced_tilt_y = m_config->opt_float("build_plate_tilt_y");
|
||||
|
||||
// Orca
|
||||
//update nozzle_volume_type
|
||||
const Preset& current_printer = m_preset_bundle->printers.get_selected_preset();
|
||||
@@ -6477,9 +6489,15 @@ void TabPrinter::update_fff()
|
||||
|
||||
// Belt printer: auto-sync build_plate_tilt_{x,y} (which drives support gravity tilt)
|
||||
// from the belt slicing rotation, the single source of truth for the physical tilt.
|
||||
// Tilt about X drives tilt_x, tilt about Y drives tilt_y. When belt mode is off,
|
||||
// reset whichever tilt axis matches a leftover belt value so we don't clobber a
|
||||
// manually-set tilt on a non-belt tilted printer.
|
||||
// Tilt about X drives tilt_x, tilt about Y drives tilt_y.
|
||||
//
|
||||
// R8: value-guessing (zeroing any tilt matching the dormant belt-derived tilt) wiped a
|
||||
// legitimate manual build_plate_tilt on a non-belt tilted-bed printer, because the belt
|
||||
// defaults (rotation=X, angle=45) make a manual tilt of 45 look belt-derived. Instead we
|
||||
// track the belt->non-belt transition and the exact values belt-sync wrote, and clear the
|
||||
// tilt only on a genuine in-place belt-off toggle, and only if the value is still what
|
||||
// belt-sync last wrote. Preset switches reset the tracking in on_preset_loaded(), so they
|
||||
// never trip the reset.
|
||||
if (m_config->opt_bool("belt_printer")) {
|
||||
auto rot_axis = m_config->option<ConfigOptionEnum<BeltRotationAxis>>("belt_slice_rotation")->value;
|
||||
const auto tilt = BeltTransformPipeline::physical_tilt(
|
||||
@@ -6488,17 +6506,20 @@ void TabPrinter::update_fff()
|
||||
m_config->set_key_value("build_plate_tilt_x", new ConfigOptionFloat(tilt.tilt_x_deg));
|
||||
if (m_config->opt_float("build_plate_tilt_y") != tilt.tilt_y_deg)
|
||||
m_config->set_key_value("build_plate_tilt_y", new ConfigOptionFloat(tilt.tilt_y_deg));
|
||||
} else {
|
||||
const auto tilt = BeltTransformPipeline::physical_tilt(
|
||||
m_config->option<ConfigOptionEnum<BeltRotationAxis>>("belt_slice_rotation")->value,
|
||||
m_config->opt_float("belt_slice_rotation_angle"));
|
||||
double tx = m_config->opt_float("build_plate_tilt_x");
|
||||
double ty = m_config->opt_float("build_plate_tilt_y");
|
||||
if (tx != 0. && std::abs(tx - tilt.tilt_x_deg) < 0.01)
|
||||
// Remember exactly what belt-sync wrote, so an in-place belt-off toggle can distinguish
|
||||
// a still-belt-derived tilt (safe to clear) from a since-edited manual one (keep).
|
||||
m_belt_synced_tilt_x = tilt.tilt_x_deg;
|
||||
m_belt_synced_tilt_y = tilt.tilt_y_deg;
|
||||
} else if (m_was_belt_printer) {
|
||||
// Genuine in-place belt->off toggle on the same preset (on_preset_loaded() was not called
|
||||
// since the last update, so m_was_belt_printer still reflects belt mode). Clear each axis
|
||||
// only if it still holds the value belt-sync last wrote; a manual override is preserved.
|
||||
if (m_config->opt_float("build_plate_tilt_x") == m_belt_synced_tilt_x)
|
||||
m_config->set_key_value("build_plate_tilt_x", new ConfigOptionFloat(0.));
|
||||
if (ty != 0. && std::abs(ty - tilt.tilt_y_deg) < 0.01)
|
||||
if (m_config->opt_float("build_plate_tilt_y") == m_belt_synced_tilt_y)
|
||||
m_config->set_key_value("build_plate_tilt_y", new ConfigOptionFloat(0.));
|
||||
}
|
||||
m_was_belt_printer = m_config->opt_bool("belt_printer");
|
||||
|
||||
toggle_options();
|
||||
}
|
||||
|
||||
@@ -623,6 +623,13 @@ private:
|
||||
bool m_rebuild_kinematics_page = false;
|
||||
void update_input_shaper_menu(GCodeFlavor flavor);
|
||||
|
||||
// R8: track the belt->non-belt transition so update_fff() only clears the belt-derived
|
||||
// build_plate_tilt on a genuine in-place belt-off toggle, never on a manual tilt or a
|
||||
// preset switch. m_belt_synced_tilt_{x,y} hold the exact values belt-sync last wrote.
|
||||
bool m_was_belt_printer = false;
|
||||
double m_belt_synced_tilt_x = 0.;
|
||||
double m_belt_synced_tilt_y = 0.;
|
||||
|
||||
ogStaticText* m_fff_print_host_upload_description_line {nullptr};
|
||||
ogStaticText* m_sla_print_host_upload_description_line {nullptr};
|
||||
|
||||
|
||||
@@ -9,7 +9,12 @@
|
||||
|
||||
#include <boost/algorithm/string.hpp>
|
||||
|
||||
#include <cctype>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
#include <map>
|
||||
#include <set>
|
||||
#include <string>
|
||||
|
||||
#include "test_helpers.hpp" // get access to init_print, etc
|
||||
|
||||
@@ -473,6 +478,298 @@ static DynamicPrintConfig belt_brim_config()
|
||||
return config;
|
||||
}
|
||||
|
||||
// Same belt as belt_brim_config(), but with `filaments` distinct filaments so the brim's
|
||||
// tool selection can be observed. Kept separate from belt_brim_config() so the existing
|
||||
// single-filament belt tests are untouched.
|
||||
static DynamicPrintConfig belt_brim_multifilament_config(unsigned int filaments,
|
||||
std::initializer_list<Slic3r::ConfigBase::SetDeserializeItem> extra = {})
|
||||
{
|
||||
DynamicPrintConfig config = multifilament_config(filaments);
|
||||
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 },
|
||||
{ "machine_start_gcode", "T[initial_tool]\n" },
|
||||
});
|
||||
if (extra.size() > 0)
|
||||
config.set_deserialize_strict(extra);
|
||||
return config;
|
||||
}
|
||||
|
||||
// 0-based tool indices used by extrusions whose role comment contains `role` (needs
|
||||
// gcode_comments). Mirrors tools_for_role in test_multifilament.cpp; statics do not cross
|
||||
// translation units, so it is repeated here.
|
||||
static std::set<int> belt_tools_for_role(const std::string &gcode, const std::string &role)
|
||||
{
|
||||
std::set<int> tools;
|
||||
int current_tool = 0;
|
||||
GCodeReader reader;
|
||||
reader.parse_buffer(gcode, [&](GCodeReader &self, const GCodeReader::GCodeLine &line) {
|
||||
const std::string cmd(line.cmd());
|
||||
if (cmd.size() >= 2 && cmd[0] == 'T' && std::isdigit((unsigned char) cmd[1]))
|
||||
current_tool = std::stoi(cmd.substr(1));
|
||||
else if (line.extruding(self) && std::string(line.comment()).find(role) != std::string::npos)
|
||||
tools.insert(current_tool);
|
||||
});
|
||||
return tools;
|
||||
}
|
||||
|
||||
// Machine Z of the first extruding move whose role comment contains `role`, in file order;
|
||||
// numeric_limits<double>::max() when the role never extrudes.
|
||||
static double first_role_z(const std::string &gcode, const std::string &role)
|
||||
{
|
||||
double z = std::numeric_limits<double>::max();
|
||||
GCodeReader parser;
|
||||
parser.parse_buffer(gcode, [&z, &role](GCodeReader &self, const GCodeReader::GCodeLine &line) {
|
||||
if (line.extruding(self) && line.comment().find(role) != std::string_view::npos) {
|
||||
z = self.z();
|
||||
self.quit_parsing();
|
||||
}
|
||||
});
|
||||
return z;
|
||||
}
|
||||
|
||||
// Number of object layers that carry a belt brim band. Each such band is emitted as one
|
||||
// contiguous brim pass, so for a single object whose first-contact layer carries a band
|
||||
// (the apron prologue folds into that layer's pass) this equals role_passes(gcode, "brim").
|
||||
static int nonempty_belt_brim_layers(const PrintObject &object)
|
||||
{
|
||||
int n = 0;
|
||||
for (const ExtrusionEntityCollection &band : object.belt_brim_by_layer())
|
||||
if (! band.empty())
|
||||
++ n;
|
||||
return n;
|
||||
}
|
||||
|
||||
// For each active tool, the ordinal (1-based, over extruding moves) of the FIRST move whose
|
||||
// role comment contains `role`. Lets a per-object ordering check key off the object's
|
||||
// unique wall filament.
|
||||
static std::map<int, long> first_move_by_tool(const std::string &gcode, const std::string &role)
|
||||
{
|
||||
std::map<int, long> first;
|
||||
int tool = 0;
|
||||
long idx = 0;
|
||||
GCodeReader reader;
|
||||
reader.parse_buffer(gcode, [&](GCodeReader &self, const GCodeReader::GCodeLine &line) {
|
||||
const std::string cmd(line.cmd());
|
||||
if (cmd.size() >= 2 && cmd[0] == 'T' && std::isdigit((unsigned char) cmd[1])) {
|
||||
tool = std::stoi(cmd.substr(1));
|
||||
return;
|
||||
}
|
||||
if (! line.extruding(self))
|
||||
return;
|
||||
++ idx;
|
||||
if (std::string(line.comment()).find(role) != std::string::npos && ! first.count(tool))
|
||||
first[tool] = idx;
|
||||
});
|
||||
return first;
|
||||
}
|
||||
|
||||
// C - the band coincident with the object's FIRST contact with the belt must not be dropped:
|
||||
// a belt brim has to appear at or below the object's first perimeter. On the unfixed feature
|
||||
// the first-contact band is dropped and the first brim then appears only at a later (higher)
|
||||
// layer. Machine Z is meaningful and shared between roles under the belt remap, so the first
|
||||
// brim's Z must not exceed the first perimeter's. Both with and without support.
|
||||
TEST_CASE("Belt brim is laid at the object's first belt contact", "[SkirtBrim][belt]")
|
||||
{
|
||||
const bool support = GENERATE(false, true);
|
||||
DYNAMIC_SECTION("enable_support=" << support) {
|
||||
DynamicPrintConfig config = belt_brim_config();
|
||||
config.set_deserialize_strict({
|
||||
{ "brim_type", "outer_only" },
|
||||
{ "brim_width", 4 },
|
||||
{ "leading_brim_length", 0 },
|
||||
{ "extra_brim_width", 0 },
|
||||
{ "brim_object_gap", 0 },
|
||||
{ "enable_support", support ? 1 : 0 },
|
||||
});
|
||||
const std::string gcode = slice({ cube(20) }, config);
|
||||
|
||||
const double brim_z = first_role_z(gcode, "brim");
|
||||
const double peri_z = first_role_z(gcode, "perimeter");
|
||||
REQUIRE(brim_z < std::numeric_limits<double>::max());
|
||||
REQUIRE(peri_z < std::numeric_limits<double>::max());
|
||||
CHECK(brim_z <= peri_z + EPSILON);
|
||||
}
|
||||
}
|
||||
|
||||
// C control - when the band's own object layer has extrusion (any interior layer of a solid
|
||||
// cube), the band takes the ordinary process_layer() path and must be drawn immediately
|
||||
// before that layer's perimeters, and exactly once: never dropped, never double-emitted.
|
||||
TEST_CASE("Belt brim on an object layer precedes its perimeters, once", "[SkirtBrim][belt]")
|
||||
{
|
||||
DynamicPrintConfig config = belt_brim_config();
|
||||
config.set_deserialize_strict({
|
||||
{ "brim_type", "outer_only" },
|
||||
{ "brim_width", 4 },
|
||||
{ "brim_object_gap", 0 },
|
||||
});
|
||||
Print print;
|
||||
Model model;
|
||||
init_print({ cube(20) }, print, model, config);
|
||||
const std::string gc = gcode(print);
|
||||
|
||||
// Ordering: the first thing extruded is brim, then perimeter.
|
||||
const std::vector<std::string> seq = role_sequence(gc, { "brim", "perimeter" });
|
||||
REQUIRE(seq.size() >= 2);
|
||||
CHECK(seq[0] == "brim");
|
||||
CHECK(seq[1] == "perimeter");
|
||||
|
||||
// Exactly once: every band is one contiguous pass (the apron prologue folds into the
|
||||
// first layer's), so the pass count equals the number of layers carrying a band - not
|
||||
// twice it, which double-emission would give, nor fewer, which a dropped band would.
|
||||
const int bands = nonempty_belt_brim_layers(*print.objects().front());
|
||||
REQUIRE(bands > 0);
|
||||
CHECK(role_passes(gc, "brim") == bands);
|
||||
}
|
||||
|
||||
// B - single extruder (filament id 1). Every band must survive the 1-based -> 0-based
|
||||
// filament-id conversion the apron path performs: a wrong conversion drops all single-extruder
|
||||
// bands, so the pass count would collapse. The expected count is derived from the sliced
|
||||
// layers, not a ratio.
|
||||
TEST_CASE("Belt brim on a single extruder emits every band once", "[SkirtBrim][belt]")
|
||||
{
|
||||
DynamicPrintConfig config = belt_brim_config();
|
||||
config.set_deserialize_strict({
|
||||
{ "brim_type", "outer_only" },
|
||||
{ "brim_width", 4 },
|
||||
{ "brim_object_gap", 0 },
|
||||
});
|
||||
Print print;
|
||||
Model model;
|
||||
init_print({ cube(20) }, print, model, config);
|
||||
const std::string gc = gcode(print);
|
||||
|
||||
const int expected = nonempty_belt_brim_layers(*print.objects().front());
|
||||
REQUIRE(expected > 0);
|
||||
CHECK(role_passes(gc, "brim") == expected);
|
||||
CHECK(belt_tools_for_role(gc, "brim") == std::set<int>{ 0 }); // filament 1 -> tool 0
|
||||
}
|
||||
|
||||
// B - multi extruder (wall filament id 2). Every belt-brim line must print on the object's
|
||||
// wall filament (index 2 -> tool 1), and the total number of passes must equal the
|
||||
// single-extruder baseline: no per-filament doubling.
|
||||
TEST_CASE("Belt brim on a multi-extruder object uses the wall filament, no doubling", "[SkirtBrim][belt]")
|
||||
{
|
||||
// Single-extruder baseline built the same way (same nozzle/flow), so the band geometry -
|
||||
// and thus the band count - is identical and only the filament assignment differs.
|
||||
DynamicPrintConfig base = belt_brim_multifilament_config(1, {
|
||||
{ "brim_type", "outer_only" },
|
||||
{ "brim_width", 4 },
|
||||
{ "brim_object_gap", 0 },
|
||||
});
|
||||
const int baseline = role_passes(slice({ cube(20) }, base), "brim");
|
||||
REQUIRE(baseline > 0);
|
||||
|
||||
DynamicPrintConfig config = belt_brim_multifilament_config(2, {
|
||||
{ "brim_type", "outer_only" },
|
||||
{ "brim_width", 4 },
|
||||
{ "brim_object_gap", 0 },
|
||||
{ "outer_wall_filament_id", 2 },
|
||||
{ "inner_wall_filament_id", 2 },
|
||||
});
|
||||
const std::string gc = slice({ cube(20) }, config);
|
||||
|
||||
CHECK(belt_tools_for_role(gc, "brim") == std::set<int>{ 1 }); // filament 2 -> tool 1
|
||||
CHECK(role_passes(gc, "brim") == baseline);
|
||||
}
|
||||
|
||||
// B - two objects offset ALONG the belt (Y, since the tilt is about X), each with its own
|
||||
// wall filament. Each object's brim/apron must print on that object's filament AND before
|
||||
// that object's own perimeters. The object is identified by its unique tool.
|
||||
TEST_CASE("Belt brim of each object precedes its perimeters on its own filament", "[SkirtBrim][belt]")
|
||||
{
|
||||
DynamicPrintConfig config = belt_brim_multifilament_config(2, {
|
||||
{ "brim_type", "outer_only" },
|
||||
{ "brim_width", 4 },
|
||||
{ "leading_brim_length", 6 },
|
||||
{ "brim_object_gap", 0 },
|
||||
});
|
||||
|
||||
std::vector<TriangleMesh> meshes;
|
||||
meshes.emplace_back(cube(20));
|
||||
TriangleMesh second = cube(20);
|
||||
second.translate(0.f, 40.f, 0.f); // offset along the belt so it lands well after the first
|
||||
meshes.emplace_back(std::move(second));
|
||||
|
||||
const std::vector<std::vector<Slic3r::ConfigBase::SetDeserializeItem>> overrides {
|
||||
{ { "outer_wall_filament_id", 1 }, { "inner_wall_filament_id", 1 } },
|
||||
{ { "outer_wall_filament_id", 2 }, { "inner_wall_filament_id", 2 } },
|
||||
};
|
||||
Print print;
|
||||
Model model;
|
||||
init_print(std::move(meshes), print, model, config, &overrides, /*arrange=*/false);
|
||||
print.process();
|
||||
const std::string gc = gcode(print);
|
||||
|
||||
// Both brims appear, each on its object's wall filament (1 -> T0, 2 -> T1).
|
||||
CHECK(belt_tools_for_role(gc, "brim") == std::set<int>{ 0, 1 });
|
||||
|
||||
const std::map<int, long> brim_first = first_move_by_tool(gc, "brim");
|
||||
const std::map<int, long> peri_first = first_move_by_tool(gc, "perimeter");
|
||||
for (int tool : { 0, 1 }) {
|
||||
REQUIRE(brim_first.count(tool) == 1);
|
||||
REQUIRE(peri_first.count(tool) == 1);
|
||||
CHECK(brim_first.at(tool) < peri_first.at(tool));
|
||||
}
|
||||
}
|
||||
|
||||
// D - the belt-brim predicate must not fire on a request that produces no belt brim.
|
||||
// leading_brim_length / extra_brim_width only feed the OUTER ring, so inner_only with zero
|
||||
// brim_width yields nothing and must not claim the layers the prime tower / spiral vase need.
|
||||
TEST_CASE("Belt inner-only leading brim does not reject the prime tower or spiral vase", "[SkirtBrim][belt]")
|
||||
{
|
||||
auto inner_leading = [](std::initializer_list<Slic3r::ConfigBase::SetDeserializeItem> extra) {
|
||||
DynamicPrintConfig config = belt_brim_config();
|
||||
config.set_deserialize_strict({
|
||||
{ "brim_type", "inner_only" },
|
||||
{ "brim_width", 0 },
|
||||
{ "leading_brim_length", 6 },
|
||||
{ "brim_object_gap", 0 },
|
||||
});
|
||||
config.set_deserialize_strict(extra);
|
||||
return config;
|
||||
};
|
||||
|
||||
SECTION("prime tower is left alone") {
|
||||
Print print;
|
||||
Model model;
|
||||
init_print({ cube(20) }, print, model, inner_leading({ { "enable_prime_tower", 1 } }));
|
||||
CHECK_FALSE(print.objects().front()->has_belt_brim());
|
||||
CHECK(print.validate().string.empty());
|
||||
}
|
||||
SECTION("spiral vase is left alone") {
|
||||
Print print;
|
||||
Model model;
|
||||
init_print({ cube(20) }, print, model, inner_leading({ { "spiral_mode", 1 } }));
|
||||
CHECK_FALSE(print.objects().front()->has_belt_brim());
|
||||
CHECK(print.validate().string.empty());
|
||||
}
|
||||
SECTION("a real inner brim still rejects the prime tower") {
|
||||
DynamicPrintConfig config = belt_brim_config();
|
||||
config.set_deserialize_strict({
|
||||
{ "brim_type", "inner_only" },
|
||||
{ "brim_width", 4 },
|
||||
{ "brim_object_gap", 0 },
|
||||
{ "enable_prime_tower", 1 },
|
||||
});
|
||||
Print print;
|
||||
Model model;
|
||||
init_print({ cube(20) }, print, model, config);
|
||||
CHECK(print.objects().front()->has_belt_brim());
|
||||
CHECK_FALSE(print.validate().string.empty());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Belt brim spans many layers instead of one", "[SkirtBrim][belt]")
|
||||
{
|
||||
DynamicPrintConfig config = belt_brim_config();
|
||||
|
||||
@@ -275,6 +275,80 @@ SCENARIO("belt_brim_region reduces to the plate brim without an apron", "[BeltBr
|
||||
}
|
||||
}
|
||||
|
||||
SCENARIO("belt_brim_region builds an inner ring inside a hole", "[BeltBrim]") {
|
||||
// Holed prisms (a washer) are the only footprints an inner brim has anything to grab.
|
||||
// The inner path offsets the hole boundary inward and keeps the ring between the two
|
||||
// offsets, clipped back inside the hole - it must be non-empty and live in the hole,
|
||||
// never spill out onto the plate. No apron is applied to the inner ring.
|
||||
const coord_t mm = scale_(1.);
|
||||
const BeltBrimFrame frame { 1.0, 1 };
|
||||
const coord_t width = 3 * mm;
|
||||
const coord_t gap = 1 * mm;
|
||||
|
||||
GIVEN("a 40x40 mm washer with a 20 mm square hole") {
|
||||
ExPolygon washer = make_box(0, 0, 40 * mm, 40 * mm);
|
||||
add_hole(washer, 10 * mm, 10 * mm, 30 * mm, 30 * mm);
|
||||
const ExPolygons footprint { washer };
|
||||
const BoundingBox hole_bb = get_extents(washer.holes.front());
|
||||
|
||||
WHEN("an inner-only brim is requested") {
|
||||
const ExPolygons region = belt_brim_region(footprint, false, true, width, gap, 0, 0, frame);
|
||||
THEN("a non-empty ring is produced strictly inside the hole") {
|
||||
REQUIRE(! region.empty());
|
||||
CHECK(area(region) > 0);
|
||||
const BoundingBox rb = get_extents(region);
|
||||
CHECK(rb.min.x() >= hole_bb.min.x());
|
||||
CHECK(rb.min.y() >= hole_bb.min.y());
|
||||
CHECK(rb.max.x() <= hole_bb.max.x());
|
||||
CHECK(rb.max.y() <= hole_bb.max.y());
|
||||
}
|
||||
}
|
||||
WHEN("no inner brim is requested") {
|
||||
THEN("the hole contributes nothing") {
|
||||
CHECK(belt_brim_region(footprint, false, false, width, gap, 0, 0, frame).empty());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SCENARIO("Leading-edge-only retains the downhill half of the brim region", "[BeltBrim]") {
|
||||
// BeltBrim.cpp ~445-458 clips the region to the object's first-contact band and keeps
|
||||
// only what lies at or downhill of it. That clip is built with band_box(), which is
|
||||
// file-static, so the rectangular half-band is reconstructed here with the SAME sign
|
||||
// rule the code uses (low_side = shear > 0, i.e. downhill is -u) to pin the convention
|
||||
// for both tilt signs. downhill_sign() is the exported accessor the flag mirrors.
|
||||
const coord_t mm = scale_(1.);
|
||||
const double shear = GENERATE(1.0, -1.0);
|
||||
DYNAMIC_SECTION("shear " << shear) {
|
||||
const BeltBrimFrame frame { shear, 1 }; // from_axis 1 => u is Y
|
||||
CHECK((frame.downhill_sign() < 0) == (frame.shear > 0.));
|
||||
|
||||
const ExPolygons region { make_box(0, 0, 20 * mm, 20 * mm) }; // straddles the cut
|
||||
const coord_t u_cut = 8 * mm;
|
||||
const BoundingBox bb = get_extents(region);
|
||||
|
||||
const bool low_side = frame.shear > 0.;
|
||||
const coord_t lo = low_side ? bb.min.y() : u_cut;
|
||||
const coord_t hi = low_side ? u_cut : bb.max.y();
|
||||
Polygon keep;
|
||||
keep.points = { Point(bb.min.x(), lo), Point(bb.max.x(), lo),
|
||||
Point(bb.max.x(), hi), Point(bb.min.x(), hi) };
|
||||
const ExPolygons kept = intersection_ex(region, Polygons{ keep });
|
||||
|
||||
REQUIRE(! kept.empty());
|
||||
const BoundingBox kb = get_extents(kept);
|
||||
if (frame.shear > 0.) {
|
||||
// downhill is -u: nothing above the cut survives.
|
||||
CHECK(kb.max.y() <= u_cut + 2);
|
||||
CHECK(kb.min.y() < u_cut);
|
||||
} else {
|
||||
// downhill is +u: nothing below the cut survives.
|
||||
CHECK(kb.min.y() >= u_cut - 2);
|
||||
CHECK(kb.max.y() > u_cut);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SCENARIO("The apron follows the sign of the shear", "[BeltBrim]") {
|
||||
// Guards the one sign convention that is easiest to get backwards: which way
|
||||
// is downhill, i.e. which way the belt carries the part.
|
||||
|
||||
Reference in New Issue
Block a user