mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-05 14:51:06 +00:00
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
226 lines
8.1 KiB
C++
226 lines
8.1 KiB
C++
#include "FirstLayerPlane.hpp"
|
|
#include "BeltTransform.hpp"
|
|
|
|
#include <algorithm>
|
|
#include <climits>
|
|
#include <cmath>
|
|
|
|
namespace Slic3r {
|
|
|
|
namespace {
|
|
|
|
// Build the row of the gcode-axis-remap matrix R that produces machine_Z,
|
|
// AS A FUNCTION OF a slicing-frame point in the GCode generator's coordinate
|
|
// space. Without back-transform this is just R.row(2). With back-transform
|
|
// the writer applies F^-1 before R, so the effective row is (R * F^-1).row(2).
|
|
//
|
|
// Returns a pair (gradient, constant) such that:
|
|
// machine_Z(p_slicing) = gradient.dot(p_slicing) + constant
|
|
struct MachineZAffine {
|
|
Vec3d gradient = Vec3d::UnitZ();
|
|
double constant = 0.0;
|
|
};
|
|
|
|
MachineZAffine compute_machine_z_affine(const PrintConfig &config)
|
|
{
|
|
MachineZAffine out;
|
|
|
|
// R is the matrix form of GCodeWriter::apply_axis_remap. Each output axis
|
|
// i picks one slicing-frame component (with sign + optional Rev mode
|
|
// translation) based on m_remap_{x,y,z}. We only need row 2 (the z output)
|
|
// since machine_Z is what defines the first-layer plane.
|
|
int rz = int(config.gcode_remap_z.value);
|
|
int axis = rz % 3;
|
|
double sign;
|
|
double trans;
|
|
if (rz < int(RemapAxis::NegX)) { // 0..2 = PosX/Y/Z
|
|
sign = 1.0;
|
|
trans = 0.0;
|
|
} else if (rz < int(RemapAxis::RevX)) { // 3..5 = NegX/Y/Z
|
|
sign = -1.0;
|
|
trans = 0.0;
|
|
} else { // 6..8 = RevX/Y/Z
|
|
sign = -1.0;
|
|
BoundingBoxf bbox_bed(config.printable_area.values);
|
|
Vec3d vol_max(bbox_bed.max.x(),
|
|
bbox_bed.max.y(),
|
|
config.printable_height.value);
|
|
trans = vol_max[axis];
|
|
}
|
|
|
|
Vec3d r_row = Vec3d::Zero();
|
|
r_row[axis] = sign;
|
|
|
|
// Without back-transform, machine_Z(slicing) = r_row · slicing + trans.
|
|
out.gradient = r_row;
|
|
out.constant = trans;
|
|
|
|
if (config.gcode_back_transform.value && config.belt_printer.value) {
|
|
// 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
|
|
// a row vector). Eigen makes this easy: it's just F^-1.transpose() * r_row.
|
|
Transform3d forward = BeltTransformPipeline::build_forward_transform(config);
|
|
Transform3d inverse = forward.inverse();
|
|
// Note: forward.translation() is normally zero (per-print transforms
|
|
// don't add a translation; the per-object z_shift is added separately
|
|
// in PrintObjectSlice). We still incorporate inverse.translation() in
|
|
// case a Rev-mode preslice_remap puts a translation in F.
|
|
Vec3d composed_grad = inverse.linear().transpose() * r_row;
|
|
double composed_trans =
|
|
r_row.dot(inverse.translation()) + trans;
|
|
out.gradient = composed_grad;
|
|
out.constant = composed_trans;
|
|
}
|
|
|
|
return out;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
FirstLayerPlane::FirstLayerPlane(const PrintConfig &config)
|
|
{
|
|
// -------- Resolve Auto -------------------------------------------------
|
|
FirstLayerPlaneMode mode = config.first_layer_plane.value;
|
|
if (mode == FirstLayerPlaneMode::Auto) {
|
|
bool belt_affine_active = config.belt_printer.value &&
|
|
config.belt_slice_rotation.value != BeltRotationAxis::None &&
|
|
std::abs(config.belt_slice_rotation_angle.value) > EPSILON;
|
|
mode = belt_affine_active ? FirstLayerPlaneMode::BeltAffine
|
|
: FirstLayerPlaneMode::XY;
|
|
}
|
|
m_mode = mode;
|
|
|
|
// -------- Band thickness ----------------------------------------------
|
|
// Note: layer_height lives in PrintObjectConfig, not PrintConfig, so we
|
|
// can't fall back to it from here. initial_layer_print_height is in
|
|
// PrintConfig and is the right default anyway (the legacy first-layer
|
|
// semantics used initial_layer_print_height, not the regular one).
|
|
double thickness = config.first_layer_plane_thickness.value;
|
|
if (thickness <= 0.0)
|
|
thickness = config.initial_layer_print_height.value;
|
|
if (thickness <= 0.0)
|
|
thickness = 0.2;
|
|
m_thickness_mm = thickness;
|
|
|
|
const double user_offset = config.first_layer_plane_offset.value;
|
|
|
|
// -------- Build the plane ---------------------------------------------
|
|
auto set_axis_aligned = [&](const Vec3d &n_unit, double offset_along_n) {
|
|
m_normal = n_unit;
|
|
m_offset = offset_along_n;
|
|
};
|
|
|
|
switch (mode) {
|
|
case FirstLayerPlaneMode::XY:
|
|
// Legacy XY plane. Inactive: short-circuit to layer-index path.
|
|
set_axis_aligned(Vec3d::UnitZ(), user_offset);
|
|
m_active = false;
|
|
return;
|
|
|
|
case FirstLayerPlaneMode::YZ:
|
|
set_axis_aligned(Vec3d::UnitX(), user_offset);
|
|
m_active = true;
|
|
return;
|
|
|
|
case FirstLayerPlaneMode::XZ:
|
|
set_axis_aligned(Vec3d::UnitY(), user_offset);
|
|
m_active = true;
|
|
return;
|
|
|
|
case FirstLayerPlaneMode::BeltAffine: {
|
|
// Compute the slicing-frame plane that maps to machine_Z = user_offset
|
|
// under the gcode axis remap (and optional back-transform).
|
|
MachineZAffine mz = compute_machine_z_affine(config);
|
|
double cmag = mz.gradient.norm();
|
|
if (cmag < EPSILON) {
|
|
// Degenerate: slicing point doesn't affect machine_Z. Fall back.
|
|
set_axis_aligned(Vec3d::UnitZ(), user_offset);
|
|
m_active = false;
|
|
return;
|
|
}
|
|
// Plane equation: gradient · slicing = user_offset - constant
|
|
const double K = user_offset - mz.constant;
|
|
m_normal = mz.gradient / cmag;
|
|
m_offset = K / cmag;
|
|
m_active = true;
|
|
return;
|
|
}
|
|
|
|
case FirstLayerPlaneMode::Auto:
|
|
// Should have been resolved above.
|
|
m_active = false;
|
|
return;
|
|
}
|
|
|
|
m_active = false;
|
|
}
|
|
|
|
double FirstLayerPlane::distance_from_plane(const Vec3d &point_slicing_mm) const
|
|
{
|
|
return m_normal.dot(point_slicing_mm) - m_offset;
|
|
}
|
|
|
|
bool FirstLayerPlane::is_first_layer(const Vec3d &point_slicing_mm,
|
|
double first_layer_height_mm) const
|
|
{
|
|
if (!m_active)
|
|
return false;
|
|
return distance_from_plane(point_slicing_mm) < first_layer_height_mm;
|
|
}
|
|
|
|
int FirstLayerPlane::effective_layer_index(const Vec3d &point_slicing_mm) const
|
|
{
|
|
if (!m_active)
|
|
return INT_MAX / 2; // Effectively "way past first layer".
|
|
double d = distance_from_plane(point_slicing_mm);
|
|
if (d <= 0.0)
|
|
return 0;
|
|
return int(std::floor(d / m_thickness_mm));
|
|
}
|
|
|
|
int FirstLayerPlane::min_effective_index_for_xy_bbox(
|
|
const BoundingBoxf &xy_bbox_mm, double slicing_z_mm) const
|
|
{
|
|
if (!m_active)
|
|
return INT_MAX / 2;
|
|
// For the rectangular bbox in (x, y) at fixed z, the smallest value of
|
|
// (n.x*x + n.y*y + n.z*z - offset) is achieved at one of the four
|
|
// corners, with the smaller component picked when the corresponding
|
|
// normal coefficient is positive.
|
|
const double x_for_min = (m_normal.x() >= 0.0)
|
|
? xy_bbox_mm.min.x() : xy_bbox_mm.max.x();
|
|
const double y_for_min = (m_normal.y() >= 0.0)
|
|
? xy_bbox_mm.min.y() : xy_bbox_mm.max.y();
|
|
const double dmin = m_normal.x() * x_for_min
|
|
+ m_normal.y() * y_for_min
|
|
+ m_normal.z() * slicing_z_mm
|
|
- m_offset;
|
|
if (dmin <= 0.0)
|
|
return 0;
|
|
return int(std::floor(dmin / m_thickness_mm));
|
|
}
|
|
|
|
int FirstLayerPlane::min_effective_index_for_bbox3(
|
|
const BoundingBoxf3 &bbox_mm) const
|
|
{
|
|
if (!m_active)
|
|
return INT_MAX / 2;
|
|
const double x_for_min = (m_normal.x() >= 0.0)
|
|
? bbox_mm.min.x() : bbox_mm.max.x();
|
|
const double y_for_min = (m_normal.y() >= 0.0)
|
|
? bbox_mm.min.y() : bbox_mm.max.y();
|
|
const double z_for_min = (m_normal.z() >= 0.0)
|
|
? bbox_mm.min.z() : bbox_mm.max.z();
|
|
const double dmin = m_normal.x() * x_for_min
|
|
+ m_normal.y() * y_for_min
|
|
+ m_normal.z() * z_for_min
|
|
- m_offset;
|
|
if (dmin <= 0.0)
|
|
return 0;
|
|
return int(std::floor(dmin / m_thickness_mm));
|
|
}
|
|
|
|
} // namespace Slic3r
|