belt: anchor the designed-view G-code preview onto the model bounding box

The belt designed (upright) preview back-transforms the machine-frame G-code
into model space with the linear belt inverse. That inverse recovers the
print's shape and orientation, but not the per-object placement/lift
translation: the object's position on the belt, the BeltSliceStrategy min-Z
lift, and the centering pre-translate are applied OUTSIDE
build_forward_transform() (see PrintObjectSlice.cpp), so its linear inverse
cannot undo them. The result was a constant offset (~20 mm on the belt-advance
axis) of the toolpaths from the model shell, on every model.

Recover the missing translation generally — independent of the offset's exact
source or the axis remap — by anchoring the back-transformed object body
(extrusions on layer_id >= 1, i.e. excluding the layer-0 prime/skirt) onto the
upright model bounding box, the same space the shells render in, and folding
that translation into the belt inverse before converting to libvgcode.

Replaces the previous Y=0 anchoring in LibVGCodeWrapper, which pinned the
toolpaths to the belt entry rather than to the model and so left the offset in
place for any object not sitting at the origin.
This commit is contained in:
Tommaso Bianchi
2026-06-06 19:39:57 +02:00
parent 695a1f897a
commit 3fc3b8a8ae
2 changed files with 31 additions and 20 deletions

View File

@@ -1229,10 +1229,36 @@ void GCodeViewer::load_as_gcode(const GCodeProcessorResult& gcode_result, const
// inverse (handles any mesh rotation + shear + axis remap). When off, the raw
// machine-frame G-code is shown (useful for debugging the transform itself).
const bool is_belt = print.config().belt_printer.value;
const Transform3d belt_inv = (is_belt && m_belt_show_designed)
Transform3d belt_inv = (is_belt && m_belt_show_designed)
? compute_belt_back_transform(print.config()) : Transform3d::Identity();
const bool apply_belt = is_belt && m_belt_show_designed
&& !belt_inv.matrix().isApprox(Transform3d::Identity().matrix());
if (apply_belt) {
// The linear belt back-transform recovers the print's shape and orientation but not
// the per-object placement/lift translation: the object's position on the belt, the
// BeltSliceStrategy min-Z lift, and the centering pre-translate are applied OUTSIDE
// build_forward_transform() (see PrintObjectSlice.cpp), so its linear inverse leaves
// them un-undone. Uncorrected, the upright toolpaths float a fixed offset from the
// model shell. Recover the translation generally — independent of the offset's exact
// source or the axis remap — by anchoring the back-transformed object body onto the
// upright model bounding box (the same space the shells render in).
BoundingBoxf3 model_bb;
for (const PrintObject* po : print.objects())
for (const ModelInstance* mi : po->model_object()->instances)
model_bb.merge(po->model_object()->instance_bounding_box(*mi));
BoundingBoxf3 tp_bb;
for (const GCodeProcessorResult::MoveVertex& mv : gcode_result.moves)
if (mv.type == EMoveType::Extrude && mv.layer_id >= 1) // skip layer-0 prime/skirt
tp_bb.merge(Vec3d(belt_inv * mv.position.cast<double>()));
if (model_bb.defined && tp_bb.defined) {
const Vec3d d = model_bb.min - tp_bb.min;
belt_inv = Transform3d(Eigen::Translation3d(d)) * belt_inv;
BOOST_LOG_TRIVIAL(debug) << "[BELT-PREVIEW] anchor: model_bb.min=("
<< model_bb.min.x() << "," << model_bb.min.y() << "," << model_bb.min.z()
<< ") tp_bb.min=(" << tp_bb.min.x() << "," << tp_bb.min.y() << "," << tp_bb.min.z()
<< ") d=(" << d.x() << "," << d.y() << "," << d.z() << ")";
}
}
libvgcode::GCodeInputData data = libvgcode::convert(gcode_result, str_tool_colors, str_color_print_colors, m_viewer,
apply_belt ? &belt_inv : nullptr);

View File

@@ -275,25 +275,10 @@ GCodeInputData convert(const Slic3r::GCodeProcessorResult& result, const std::ve
}
ret.vertices.shrink_to_fit();
// Belt designed view: the linear back-transform recovers the correct shape
// and orientation, but not the constant machine-frame origin offset baked
// into the G-code (e.g. the start-G-code belt advance + a G92 reset leaves a
// ~20 mm Z residual). Anchor the lowest extrusion to the belt entry (Y=0) so
// the toolpaths sit on the bed under the model shell. Independent of the
// offset's source, so it stays general across machines.
if (belt_xform != nullptr && !ret.vertices.empty()) {
// Anchor on the OBJECT extrusions only (layer_id >= 1): the start-G-code
// prime lines (layer 0) print at the belt origin, while the object prints
// after the start-G-code belt advance, so anchoring on the global min
// would lock onto the prime and leave the object offset.
float min_y = std::numeric_limits<float>::max();
for (const PathVertex& v : ret.vertices)
if (v.type == EMoveType::Extrude && v.layer_id >= 1 && v.position[1] < min_y)
min_y = v.position[1];
if (min_y != std::numeric_limits<float>::max() && std::abs(min_y) > 1e-3f)
for (PathVertex& v : ret.vertices)
v.position[1] -= min_y;
}
// Note: the belt designed-view anchoring (recovering the per-object placement/
// lift translation the linear back-transform cannot) is folded into belt_xform
// by the caller (GCodeViewer::load_as_gcode), which anchors onto the upright
// model bounding box. Nothing extra to do here.
ret.spiral_vase_mode = result.spiral_vase_mode;