Files
OrcaSlicer/src/libslic3r/GCode/MachineFrameTransform.hpp
T
harrierpigeonandClaude Opus 5 e695da66df GCodeWriter: extract MachineKinematics, delete BeltGCodeWriter
BeltGCodeWriter subclassed GCodeWriter and overrode seven methods, five of them
by copying the base body and changing the transform. The base writer already
carried an axis remap and already branched at each of its seven
coordinate-emission decisions; the subclass did the same branching with a
different transform, and the two copies had begun to drift.

Replace the inheritance with a strategy object owned by GCodeWriter:

  CartesianKinematics  to_machine = the existing apply_axis_remap; today's base
                       behaviour, moved rather than changed.
  BeltKinematics       to_machine = MachineFrameTransform o axis_remap o
                       BeltBackTransform, plus a world_coordinates variant for
                       the PA calibration generators.

New: src/libslic3r/GCode/MachineKinematics.{hpp,cpp}, GCode/BeltKinematics.{hpp,cpp}
Deleted: src/libslic3r/BeltGCodeWriter.{hpp,cpp} (341 lines)

Points worth a reviewer's attention:

  * The predicate is must_emit_all_axes(), not couples_axes(). The base returns
    true for any non-identity remap, including pure permutations that do not
    physically couple axes, so the question is "must every axis word be
    emitted", not a statement about kinematics.
  * Every per-site word-omission branch is preserved. The base deliberately
    emits X/Y only, or Z only, or drops Z when its quantised value is unchanged.
    The strategy changes which transform applies, never whether words are
    omitted.
  * set_kinematics() replays the configured remap and build volume onto a newly
    installed strategy, because BeltGCode::init_belt_writer runs before
    GCode.cpp calls set_axis_remap/set_build_volume_max.
  * uses_pointwise_travel_speed() preserves a pre-existing divergence rather
    than introducing one: the base travel_to_xyz emits the raw configured travel
    speed in its final branch, ignoring the first-layer value computed at the
    top, whereas the belt path used the first-layer-aware value throughout. Both
    are kept. Unifying them changes feedrates and belongs in its own change.
  * The [BELT-DEBUG] block is deleted; it rate-limited itself with a
    function-local static thread_local in the hot emission path, and this is the
    commit that would otherwise have moved it into shared code.

This commit is intended to preserve existing export output. That is reviewed by
construction -- each emission site keeps its own omission branch and each policy
divergence is preserved -- and is NOT verified against a G-code diff corpus.
Building that corpus is the outstanding work here.

Two API-equivalence exceptions, neither reachable by any caller today:

  * Belt kinematics with no plane pointer installed, m_is_first_layer true,
    initial and normal travel speeds differing, travel_to_xyz() reaching its
    final branch: the old belt writer selected the initial-layer speed, the new
    writer selects the normal travel speed. The pending-lift and XY-only
    branches keep their previous selection.
  * Belt kinematics installed without set_force_normal_lift(true) and a
    non-normal lift requested: the old belt writer forced a normal lift, the new
    writer can take the slope branch.

The PA-pattern generator reaches the writer through explicit travel_to_z() /
travel_to_xy(), not travel_to_xyz() or the lazy/eager lift paths, and normal
belt export installs both the plane and the forced-normal-lift policy, so
neither exception changes output produced today. They are recorded because a
future caller could reach them.

tests/fff_print/test_gcodewriter.cpp was also not compiling before this branch:
it called writer.to_machine_coords(), a method that existed only on
BeltGCodeWriter. It never surfaced because the build targets OrcaSlicer, not
all, and BUILD_TESTS defaults to OFF, so that translation unit was outside every
compile path. Fixed here; the existing 30-degree coordinate assertions are kept
verbatim as the best available regression net.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011jgzj1sf53KMLPweZ8yeUQ
2026-09-08 23:57:55 -05:00

55 lines
2.0 KiB
C++

#ifndef slic3r_MachineFrameTransform_hpp_
#define slic3r_MachineFrameTransform_hpp_
#include "../libslic3r.h"
#include "../Point.hpp"
#include "../PrintConfig.hpp"
namespace Slic3r {
// Post-stage machine-frame transform for belt printers.
//
// 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.
//
// Derived entirely from the single belt tilt (belt_slice_rotation axis +
// belt_slice_rotation_angle): a shear coupling the height axis to the belt-feed
// axis (factor cot a) plus a 1/sin a scale on the gantry-height axis. The expert
// belt_frame_tilt_decouple flag lets the machine-frame angle differ from the
// pre-slice rotation angle via belt_frame_tilt_angle.
class MachineFrameTransform
{
public:
MachineFrameTransform() = default;
// Initialize from belt printer config. Returns true if a non-identity
// transform was computed. Inactive when belt_printer is disabled or
// both shear and scale are identity.
bool init_from_config(const PrintConfig &config);
// Apply the transform to a point. Returns pos unchanged if not active.
Vec3d apply(const Vec3d &pos) const;
// Apply the inverse transform. Returns pos unchanged if not active.
// Used by validators that need to compare emitted machine-frame
// coordinates against build-volume bounds.
Vec3d apply_inverse(const Vec3d &pos) const;
bool is_active() const { return m_active; }
// The composed shear*scale transform (identity when inactive). Exposed so the
// G-code viewer can build the machine->model back-transform for the upright
// ("designed") belt preview.
const Transform3d& transform() const { return m_transform; }
private:
bool m_active = false;
Transform3d m_transform = Transform3d::Identity();
Transform3d m_transform_inverse = Transform3d::Identity();
};
} // namespace Slic3r
#endif // slic3r_MachineFrameTransform_hpp_