Part 3.2: decouple axis remapping, enable viewing settings in Developer mode or when Belt mode is active

This commit is contained in:
harrierpigeon
2026-03-30 13:25:40 -05:00
committed by Joseph Robertson
parent c7aa4ca3ef
commit 44eebdb8ad
20 changed files with 215 additions and 154 deletions
+4 -10
View File
@@ -15,13 +15,7 @@ void BeltGCode::init_belt_writer(Print &print, bool is_bbl_printers)
auto belt_writer = std::make_unique<BeltGCodeWriter>(); auto belt_writer = std::make_unique<BeltGCodeWriter>();
belt_writer->set_is_bbl_machine(is_bbl_printers); belt_writer->set_is_bbl_machine(is_bbl_printers);
belt_writer->set_belt_angle(print.config().belt_printer_angle.value); belt_writer->set_belt_angle(print.config().belt_printer_angle.value);
belt_writer->set_axis_remap( // Axis remap and build volume max are set by base GCode after init_belt_writer returns.
int(print.config().belt_gcode_remap_x.value),
int(print.config().belt_gcode_remap_y.value),
int(print.config().belt_gcode_remap_z.value));
BoundingBoxf bbox_bed(print.config().printable_area.values);
belt_writer->set_build_volume_max(Vec3d(bbox_bed.max.x(), bbox_bed.max.y(),
print.config().printable_height.value));
belt_writer->set_belt_back_transform(print.config()); belt_writer->set_belt_back_transform(print.config());
m_writer = std::move(belt_writer); m_writer = std::move(belt_writer);
@@ -59,9 +53,9 @@ void BeltGCode::write_belt_header(GCodeOutputStream &file, const Print &print)
file.write_format("; belt_scale_z = %s\n", full_cfg.opt_serialize("belt_scale_z").c_str()); file.write_format("; belt_scale_z = %s\n", full_cfg.opt_serialize("belt_scale_z").c_str());
file.write_format("; belt_scale_z_angle = %.1f\n", print.config().belt_scale_z_angle.value); file.write_format("; belt_scale_z_angle = %.1f\n", print.config().belt_scale_z_angle.value);
// Pre-slice remap configs // Pre-slice remap configs
file.write_format("; belt_preslice_remap_x = %s\n", full_cfg.opt_serialize("belt_preslice_remap_x").c_str()); file.write_format("; preslice_remap_x = %s\n", full_cfg.opt_serialize("preslice_remap_x").c_str());
file.write_format("; belt_preslice_remap_y = %s\n", full_cfg.opt_serialize("belt_preslice_remap_y").c_str()); file.write_format("; preslice_remap_y = %s\n", full_cfg.opt_serialize("preslice_remap_y").c_str());
file.write_format("; belt_preslice_remap_z = %s\n", full_cfg.opt_serialize("belt_preslice_remap_z").c_str()); file.write_format("; preslice_remap_z = %s\n", full_cfg.opt_serialize("preslice_remap_z").c_str());
} }
void BeltGCode::on_set_origin(const PrintObject *obj, const Point &inst_shift) void BeltGCode::on_set_origin(const PrintObject *obj, const Point &inst_shift)
+3 -22
View File
@@ -10,18 +10,6 @@ void BeltGCodeWriter::set_belt_angle(double angle_deg)
m_belt_angle_rad = Geometry::deg2rad(angle_deg); m_belt_angle_rad = Geometry::deg2rad(angle_deg);
} }
void BeltGCodeWriter::set_axis_remap(int rx, int ry, int rz)
{
m_remap_x = rx;
m_remap_y = ry;
m_remap_z = rz;
}
void BeltGCodeWriter::set_build_volume_max(const Vec3d &max)
{
m_build_vol_max = max;
}
void BeltGCodeWriter::set_belt_back_transform(const PrintConfig &config) void BeltGCodeWriter::set_belt_back_transform(const PrintConfig &config)
{ {
m_belt_back_transform.init_from_config(config); m_belt_back_transform.init_from_config(config);
@@ -40,16 +28,9 @@ Vec3d BeltGCodeWriter::to_machine_coords(const Vec3d &pos) const
{ {
// Step 1: Undo the shear/scale applied during slicing. // Step 1: Undo the shear/scale applied during slicing.
Vec3d p = m_belt_back_transform.apply(pos); Vec3d p = m_belt_back_transform.apply(pos);
// Step 2: Apply axis remapping for the machine's coordinate convention. // Step 2: Apply axis remap (uses inherited base class method).
// BeltRemapAxis: 0-2 = +X/+Y/+Z, 3-5 = -X/-Y/-Z, 6-8 = Rev X/Y/Z Vec3d result = apply_axis_remap(p);
auto remap = [this, &p](int r) -> double { // Step 3: Per-axis origin snap.
int axis = r % 3;
if (r < 3) return p[axis];
if (r < 6) return -p[axis];
return m_build_vol_max[axis] - p[axis];
};
Vec3d result = { remap(m_remap_x), remap(m_remap_y), remap(m_remap_z) };
// Per-axis origin snap: shift so bbox min on each enabled axis = offset.
for (int i = 0; i < 3; ++i) for (int i = 0; i < 3; ++i)
if (m_origin_snap[i]) if (m_origin_snap[i])
result[i] -= (m_origin_bbox_min[i] - m_origin_offset[i]); result[i] -= (m_origin_bbox_min[i] - m_origin_offset[i]);
+1 -7
View File
@@ -15,11 +15,9 @@ class BeltGCodeWriter : public GCodeWriter
public: public:
BeltGCodeWriter() : GCodeWriter() {} BeltGCodeWriter() : GCodeWriter() {}
// Belt configuration // Belt configuration (axis remap is inherited from GCodeWriter)
void set_belt_angle(double angle_deg); void set_belt_angle(double angle_deg);
bool is_belt_printer() const { return m_belt_angle_rad != 0.; } bool is_belt_printer() const { return m_belt_angle_rad != 0.; }
void set_axis_remap(int rx, int ry, int rz);
void set_build_volume_max(const Vec3d &max);
void set_belt_back_transform(const PrintConfig &config); void set_belt_back_transform(const PrintConfig &config);
void set_origin_snap(int axis, bool enable, double offset, double bbox_min); void set_origin_snap(int axis, bool enable, double offset, double bbox_min);
Vec3d to_machine_coords(const Vec3d &pos) const; Vec3d to_machine_coords(const Vec3d &pos) const;
@@ -37,10 +35,6 @@ protected:
private: private:
double m_belt_angle_rad = 0.; double m_belt_angle_rad = 0.;
int m_remap_x = 0;
int m_remap_y = 1;
int m_remap_z = 2;
Vec3d m_build_vol_max = Vec3d::Zero();
BeltBackTransform m_belt_back_transform; BeltBackTransform m_belt_back_transform;
bool m_origin_snap[3] = {false, false, false}; bool m_origin_snap[3] = {false, false, false};
double m_origin_offset[3] = {0., 0., 0.}; double m_origin_offset[3] = {0., 0., 0.};
+4 -11
View File
@@ -13,31 +13,24 @@ std::unique_ptr<BeltSliceStrategy> BeltSliceStrategy::create(const PrintConfig &
BeltSliceStrategy::BeltSliceStrategy(const PrintConfig &config) BeltSliceStrategy::BeltSliceStrategy(const PrintConfig &config)
{ {
m_has_remap = BeltTransformPipeline::has_preslice_remap(config);
if (m_has_remap)
m_pre_remap = BeltTransformPipeline::build_preslice_remap(config);
m_shear = BeltTransformPipeline::build_shear_matrix(config, &m_has_shear); m_shear = BeltTransformPipeline::build_shear_matrix(config, &m_has_shear);
m_scale = BeltTransformPipeline::build_scale_matrix(config, &m_has_scale); m_scale = BeltTransformPipeline::build_scale_matrix(config, &m_has_scale);
} }
void BeltSliceStrategy::apply_to_trafo(Transform3d &trafo, void BeltSliceStrategy::apply_to_trafo(Transform3d &trafo,
const ModelVolumePtrs &model_volumes, const ModelVolumePtrs &model_volumes,
bool has_remap,
double *out_belt_min_z) const double *out_belt_min_z) const
{ {
// Step 1: Pre-slice axis remap. // Shear + scale (belt-only; pre-slice remap is handled separately).
if (m_has_remap)
trafo = m_pre_remap * trafo;
// Step 2: Shear + scale.
if (m_has_shear || m_has_scale) { if (m_has_shear || m_has_scale) {
Transform3d belt_xform = Transform3d::Identity(); Transform3d belt_xform = Transform3d::Identity();
belt_xform.linear() = m_scale * m_shear; belt_xform.linear() = m_scale * m_shear;
trafo = belt_xform * trafo; trafo = belt_xform * trafo;
} }
// Step 3: Z-shift — detect if mesh clips below build plate after transforms. // Z-shift — detect if mesh clips below build plate after transforms.
if (m_has_remap || m_has_shear || m_has_scale) { if (has_remap || m_has_shear || m_has_scale) {
double min_z = std::numeric_limits<double>::max(); double min_z = std::numeric_limits<double>::max();
for (const ModelVolume *mv : model_volumes) { for (const ModelVolume *mv : model_volumes) {
if (!mv->is_model_part()) continue; if (!mv->is_model_part()) continue;
+4 -6
View File
@@ -23,21 +23,19 @@ public:
// Create a strategy if belt_printer is enabled; returns nullptr otherwise. // Create a strategy if belt_printer is enabled; returns nullptr otherwise.
static std::unique_ptr<BeltSliceStrategy> create(const PrintConfig &config); static std::unique_ptr<BeltSliceStrategy> create(const PrintConfig &config);
// Apply belt transforms to the slicing trafo. // Apply belt-specific transforms (shear + scale + z-shift) to the slicing trafo.
// Modifies trafo in-place: trafo = z_shift * scale * shear * pre_remap * trafo // Pre-slice remap is handled separately (standalone feature).
// Scans all model_part volumes to detect minimum Z and add z-shift if needed. // has_remap: whether pre-slice remap was already applied (affects z-shift detection).
// Sets *out_belt_min_z to the minimum Z of the mesh after transforms.
void apply_to_trafo(Transform3d &trafo, void apply_to_trafo(Transform3d &trafo,
const ModelVolumePtrs &model_volumes, const ModelVolumePtrs &model_volumes,
bool has_remap,
double *out_belt_min_z) const; double *out_belt_min_z) const;
private: private:
explicit BeltSliceStrategy(const PrintConfig &config); explicit BeltSliceStrategy(const PrintConfig &config);
bool m_has_remap = false;
bool m_has_shear = false; bool m_has_shear = false;
bool m_has_scale = false; bool m_has_scale = false;
Transform3d m_pre_remap = Transform3d::Identity();
Matrix3d m_shear = Matrix3d::Identity(); Matrix3d m_shear = Matrix3d::Identity();
Matrix3d m_scale = Matrix3d::Identity(); Matrix3d m_scale = Matrix3d::Identity();
}; };
+9 -9
View File
@@ -13,9 +13,9 @@ Transform3d BeltTransformPipeline::build_preslice_remap(const PrintConfig &confi
if (!has_preslice_remap(config)) if (!has_preslice_remap(config))
return pre_remap; return pre_remap;
int pre_rx = int(config.belt_preslice_remap_x.value); int pre_rx = int(config.preslice_remap_x.value);
int pre_ry = int(config.belt_preslice_remap_y.value); int pre_ry = int(config.preslice_remap_y.value);
int pre_rz = int(config.belt_preslice_remap_z.value); int pre_rz = int(config.preslice_remap_z.value);
// Each remap value selects a source axis and sign. // Each remap value selects a source axis and sign.
auto remap_column = [](int r) -> Vec3d { auto remap_column = [](int r) -> Vec3d {
@@ -114,13 +114,13 @@ Transform3d BeltTransformPipeline::build_forward_transform(const PrintConfig &co
BoundingBoxf3 BeltTransformPipeline::remap_bbox(const BoundingBoxf3 &bb, const PrintConfig &config) BoundingBoxf3 BeltTransformPipeline::remap_bbox(const BoundingBoxf3 &bb, const PrintConfig &config)
{ {
int pre_rx = int(config.belt_preslice_remap_x.value); int pre_rx = int(config.preslice_remap_x.value);
int pre_ry = int(config.belt_preslice_remap_y.value); int pre_ry = int(config.preslice_remap_y.value);
int pre_rz = int(config.belt_preslice_remap_z.value); int pre_rz = int(config.preslice_remap_z.value);
if (pre_rx == int(BeltRemapAxis::PosX) && if (pre_rx == int(RemapAxis::PosX) &&
pre_ry == int(BeltRemapAxis::PosY) && pre_ry == int(RemapAxis::PosY) &&
pre_rz == int(BeltRemapAxis::PosZ)) pre_rz == int(RemapAxis::PosZ))
return bb; // Identity remap. return bb; // Identity remap.
auto remap_coord = [](int r, const Vec3d &v) -> double { auto remap_coord = [](int r, const Vec3d &v) -> double {
+7 -7
View File
@@ -58,21 +58,21 @@ public:
static bool has_preslice_remap(const PrintConfig &config) static bool has_preslice_remap(const PrintConfig &config)
{ {
return int(config.belt_preslice_remap_x.value) != int(BeltRemapAxis::PosX) || return int(config.preslice_remap_x.value) != int(RemapAxis::PosX) ||
int(config.belt_preslice_remap_y.value) != int(BeltRemapAxis::PosY) || int(config.preslice_remap_y.value) != int(RemapAxis::PosY) ||
int(config.belt_preslice_remap_z.value) != int(BeltRemapAxis::PosZ); int(config.preslice_remap_z.value) != int(RemapAxis::PosZ);
} }
// Overload accepting DynamicPrintConfig (used in static slicing_parameters). // Overload accepting DynamicPrintConfig (used in static slicing_parameters).
static bool has_preslice_remap(const DynamicPrintConfig &config) static bool has_preslice_remap(const DynamicPrintConfig &config)
{ {
auto get_int = [&](const char *key) -> int { auto get_int = [&](const char *key) -> int {
auto *opt = config.option<ConfigOptionEnum<BeltRemapAxis>>(key); auto *opt = config.option<ConfigOptionEnum<RemapAxis>>(key);
return opt ? int(opt->value) : 0; return opt ? int(opt->value) : 0;
}; };
return get_int("belt_preslice_remap_x") != int(BeltRemapAxis::PosX) || return get_int("preslice_remap_x") != int(RemapAxis::PosX) ||
get_int("belt_preslice_remap_y") != int(BeltRemapAxis::PosY) || get_int("preslice_remap_y") != int(RemapAxis::PosY) ||
get_int("belt_preslice_remap_z") != int(BeltRemapAxis::PosZ); get_int("preslice_remap_z") != int(RemapAxis::PosZ);
} }
static bool has_shear(const PrintConfig &config) static bool has_shear(const PrintConfig &config)
+13
View File
@@ -2425,6 +2425,19 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
// Belt printer: initialize belt-specific writer via virtual hook. // Belt printer: initialize belt-specific writer via virtual hook.
this->init_belt_writer(print, is_bbl_printers); this->init_belt_writer(print, is_bbl_printers);
// Standalone axis remap (works with or without belt mode).
{
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));
}
}
// How many times will be change_layer() called? // How many times will be change_layer() called?
// change_layer() in turn increments the progress bar status. // change_layer() in turn increments the progress bar status.
m_layer_count = 0; m_layer_count = 0;
+1 -1
View File
@@ -8,7 +8,7 @@ bool BeltBackTransform::init_from_config(const PrintConfig &config)
m_active = false; m_active = false;
m_inverse = Transform3d::Identity(); m_inverse = Transform3d::Identity();
if (!config.belt_printer.value || !config.belt_gcode_back_transform.value) if (!config.belt_printer.value || !config.gcode_back_transform.value)
return false; return false;
// Require at least one active transform to proceed. // Require at least one active transform to proceed.
+1 -1
View File
@@ -15,7 +15,7 @@ namespace Slic3r {
// Initialized once from PrintConfig, then applied per-point in // Initialized once from PrintConfig, then applied per-point in
// GCodeWriter::to_machine_coords() before axis remapping. // GCodeWriter::to_machine_coords() before axis remapping.
// //
// Active when belt_gcode_back_transform is true AND at least one of: // Active when gcode_back_transform is true AND at least one of:
// - a shear axis has global mode enabled, or // - a shear axis has global mode enabled, or
// - a pre-slice axis remap is non-identity. // - a pre-slice axis remap is non-identity.
class BeltBackTransform class BeltBackTransform
+17 -17
View File
@@ -3129,26 +3129,26 @@ void GCodeProcessor::process_tags(const std::string_view comment, bool producers
try { m_result.belt_scale_z_angle = std::stof(std::string(comment.substr(22))); } catch (...) {} return; try { m_result.belt_scale_z_angle = std::stof(std::string(comment.substr(22))); } catch (...) {} return;
} }
// Pre-slice axis remap // Pre-slice axis remap
auto parse_remap_axis = [](const std::string &s) -> BeltRemapAxis { auto parse_remap_axis = [](const std::string &s) -> RemapAxis {
if (s == "pos_x") return BeltRemapAxis::PosX; if (s == "pos_x") return RemapAxis::PosX;
if (s == "pos_y") return BeltRemapAxis::PosY; if (s == "pos_y") return RemapAxis::PosY;
if (s == "pos_z") return BeltRemapAxis::PosZ; if (s == "pos_z") return RemapAxis::PosZ;
if (s == "neg_x") return BeltRemapAxis::NegX; if (s == "neg_x") return RemapAxis::NegX;
if (s == "neg_y") return BeltRemapAxis::NegY; if (s == "neg_y") return RemapAxis::NegY;
if (s == "neg_z") return BeltRemapAxis::NegZ; if (s == "neg_z") return RemapAxis::NegZ;
if (s == "rev_x") return BeltRemapAxis::RevX; if (s == "rev_x") return RemapAxis::RevX;
if (s == "rev_y") return BeltRemapAxis::RevY; if (s == "rev_y") return RemapAxis::RevY;
if (s == "rev_z") return BeltRemapAxis::RevZ; if (s == "rev_z") return RemapAxis::RevZ;
return BeltRemapAxis::PosX; return RemapAxis::PosX;
}; };
if (boost::starts_with(comment, " belt_preslice_remap_x = ")) { if (boost::starts_with(comment, " preslice_remap_x = ")) {
m_result.belt_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(25)))); return;
} }
if (boost::starts_with(comment, " belt_preslice_remap_y = ")) { if (boost::starts_with(comment, " preslice_remap_y = ")) {
m_result.belt_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(25)))); return;
} }
if (boost::starts_with(comment, " belt_preslice_remap_z = ")) { if (boost::starts_with(comment, " preslice_remap_z = ")) {
m_result.belt_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(25)))); return;
} }
} }
// wipe start tag // wipe start tag
+6 -6
View File
@@ -246,9 +246,9 @@ class Print;
float belt_scale_y_angle{ 45.f }; float belt_scale_y_angle{ 45.f };
BeltScaleMode belt_scale_z{ BeltScaleMode::None }; BeltScaleMode belt_scale_z{ BeltScaleMode::None };
float belt_scale_z_angle{ 45.f }; float belt_scale_z_angle{ 45.f };
BeltRemapAxis belt_preslice_remap_x{ BeltRemapAxis::PosX }; RemapAxis preslice_remap_x{ RemapAxis::PosX };
BeltRemapAxis belt_preslice_remap_y{ BeltRemapAxis::PosY }; RemapAxis preslice_remap_y{ RemapAxis::PosY };
BeltRemapAxis belt_preslice_remap_z{ BeltRemapAxis::PosZ }; RemapAxis preslice_remap_z{ RemapAxis::PosZ };
SettingsIds settings_ids; SettingsIds settings_ids;
size_t filaments_count; size_t filaments_count;
bool backtrace_enabled; bool backtrace_enabled;
@@ -324,9 +324,9 @@ class Print;
belt_scale_y_angle = other.belt_scale_y_angle; belt_scale_y_angle = other.belt_scale_y_angle;
belt_scale_z = other.belt_scale_z; belt_scale_z = other.belt_scale_z;
belt_scale_z_angle = other.belt_scale_z_angle; belt_scale_z_angle = other.belt_scale_z_angle;
belt_preslice_remap_x = other.belt_preslice_remap_x; preslice_remap_x = other.preslice_remap_x;
belt_preslice_remap_y = other.belt_preslice_remap_y; preslice_remap_y = other.preslice_remap_y;
belt_preslice_remap_z = other.belt_preslice_remap_z; preslice_remap_z = other.preslice_remap_z;
#if ENABLE_GCODE_VIEWER_STATISTICS #if ENABLE_GCODE_VIEWER_STATISTICS
time = other.time; time = other.time;
#endif #endif
+53 -3
View File
@@ -20,6 +20,36 @@ namespace Slic3r {
bool GCodeWriter::full_gcode_comment = true; bool GCodeWriter::full_gcode_comment = true;
void GCodeWriter::set_axis_remap(int rx, int ry, int rz)
{
m_remap_x = rx;
m_remap_y = ry;
m_remap_z = rz;
}
void GCodeWriter::set_build_volume_max(const Vec3d &max)
{
m_build_vol_max = max;
}
bool GCodeWriter::has_axis_remap() const
{
return m_remap_x != 0 || m_remap_y != 1 || m_remap_z != 2;
}
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) };
}
bool GCodeWriter::supports_separate_travel_acceleration(GCodeFlavor flavor) bool GCodeWriter::supports_separate_travel_acceleration(GCodeFlavor flavor)
{ {
return (flavor == gcfRepetier || flavor == gcfMarlinFirmware || flavor == gcfRepRapFirmware); return (flavor == gcfRepetier || flavor == gcfMarlinFirmware || flavor == gcfRepRapFirmware);
@@ -548,7 +578,13 @@ std::string GCodeWriter::travel_to_xy(const Vec2d &point, const std::string &com
Vec2d point_on_plate = { point(0) - m_x_offset, point(1) - m_y_offset }; Vec2d point_on_plate = { point(0) - m_x_offset, point(1) - m_y_offset };
GCodeG1Formatter w; GCodeG1Formatter w;
w.emit_xy(point_on_plate); if (has_axis_remap()) {
// Axis remap may couple XY with Z; emit full XYZ in machine coordinates.
Vec3d machine = apply_axis_remap(Vec3d(point_on_plate.x(), point_on_plate.y(), m_pos.z()));
w.emit_xyz(machine);
} else {
w.emit_xy(point_on_plate);
}
auto speed = m_is_first_layer auto speed = m_is_first_layer
? this->config.get_abs_value("initial_layer_travel_speed") : this->config.travel_speed.value; ? this->config.get_abs_value("initial_layer_travel_speed") : this->config.travel_speed.value;
w.emit_f(speed * 60.0); w.emit_f(speed * 60.0);
@@ -780,7 +816,13 @@ std::string GCodeWriter::_travel_to_z(double z, const std::string &comment)
} }
GCodeG1Formatter w; GCodeG1Formatter w;
w.emit_z(z); if (has_axis_remap()) {
// Remap may couple Z with other axes; emit full XYZ.
Vec3d machine = apply_axis_remap(Vec3d(m_pos.x() - m_x_offset, m_pos.y() - m_y_offset, z));
w.emit_xyz(machine);
} else {
w.emit_z(z);
}
w.emit_f(speed * 60.0); w.emit_f(speed * 60.0);
//BBS //BBS
w.emit_comment(GCodeWriter::full_gcode_comment, comment); w.emit_comment(GCodeWriter::full_gcode_comment, comment);
@@ -885,7 +927,12 @@ std::string GCodeWriter::extrude_to_xy(const Vec2d &point, double dE, const std:
Vec2d point_on_plate = { point(0) - m_x_offset, point(1) - m_y_offset }; Vec2d point_on_plate = { point(0) - m_x_offset, point(1) - m_y_offset };
GCodeG1Formatter w; GCodeG1Formatter w;
w.emit_xy(point_on_plate); if (has_axis_remap()) {
Vec3d machine = apply_axis_remap(Vec3d(point_on_plate.x(), point_on_plate.y(), m_pos.z()));
w.emit_xyz(machine);
} else {
w.emit_xy(point_on_plate);
}
if (!force_no_extrusion) if (!force_no_extrusion)
w.emit_e(filament()->E()); w.emit_e(filament()->E());
//BBS //BBS
@@ -925,6 +972,9 @@ std::string GCodeWriter::extrude_to_xyz(const Vec3d &point, double dE, const std
//BBS: take plate offset into consider //BBS: take plate offset into consider
Vec3d point_on_plate = { point(0) - m_x_offset, point(1) - m_y_offset, point(2) }; 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; GCodeG1Formatter w;
w.emit_xyz(point_on_plate); w.emit_xyz(point_on_plate);
if (!force_no_extrusion) if (!force_no_extrusion)
+15
View File
@@ -125,6 +125,12 @@ public:
void set_is_first_layer(bool bval) { m_is_first_layer = bval; } void set_is_first_layer(bool bval) { m_is_first_layer = bval; }
GCodeFlavor get_gcode_flavor() const { return config.gcode_flavor; } GCodeFlavor get_gcode_flavor() const { return config.gcode_flavor; }
// Axis remap: permute/negate/reverse axes in G-code output.
// Works standalone (without belt mode) for printers with non-standard axis conventions.
void set_axis_remap(int rx, int ry, int rz);
void set_build_volume_max(const Vec3d &max);
bool has_axis_remap() const;
// Returns whether this flavor supports separate print and travel acceleration. // Returns whether this flavor supports separate print and travel acceleration.
static bool supports_separate_travel_acceleration(GCodeFlavor flavor); static bool supports_separate_travel_acceleration(GCodeFlavor flavor);
protected: protected:
@@ -141,6 +147,15 @@ protected:
virtual std::string _travel_to_z(double z, const std::string &comment); virtual std::string _travel_to_z(double z, const std::string &comment);
// Axis remap state — accessible to subclasses.
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.
Vec3d apply_axis_remap(const Vec3d &pos) const;
private: private:
// Extruders are sorted by their ID, so that binary search is possible. // Extruders are sorted by their ID, so that binary search is possible.
std::vector<Extruder> m_filament_extruders; std::vector<Extruder> m_filament_extruders;
+2 -2
View File
@@ -1014,8 +1014,8 @@ static std::vector<std::string> s_Preset_printer_options {
"belt_shear_y", "belt_shear_y_angle", "belt_shear_y_from", "belt_shear_y_global", "belt_shear_y", "belt_shear_y_angle", "belt_shear_y_from", "belt_shear_y_global",
"belt_shear_z", "belt_shear_z_angle", "belt_shear_z_from", "belt_shear_z_global", "belt_shear_z", "belt_shear_z_angle", "belt_shear_z_from", "belt_shear_z_global",
"belt_scale_x", "belt_scale_x_angle", "belt_scale_y", "belt_scale_y_angle", "belt_scale_z", "belt_scale_z_angle", "belt_scale_x", "belt_scale_x_angle", "belt_scale_y", "belt_scale_y_angle", "belt_scale_z", "belt_scale_z_angle",
"belt_preslice_remap_x", "belt_preslice_remap_y", "belt_preslice_remap_z", "preslice_remap_x", "preslice_remap_y", "preslice_remap_z",
"belt_gcode_remap_x", "belt_gcode_remap_y", "belt_gcode_remap_z", "belt_gcode_back_transform", "gcode_remap_x", "gcode_remap_y", "gcode_remap_z", "gcode_back_transform",
"belt_origin_snap_x", "belt_origin_offset_x", "belt_origin_snap_x", "belt_origin_offset_x",
"belt_origin_snap_y", "belt_origin_offset_y", "belt_origin_snap_y", "belt_origin_offset_y",
"belt_origin_snap_z", "belt_origin_offset_z", "belt_origin_snap_z", "belt_origin_offset_z",
+6 -6
View File
@@ -101,9 +101,9 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n
// or they are only notes not influencing the generated G-code. // or they are only notes not influencing the generated G-code.
static std::unordered_set<std::string> steps_gcode = { static std::unordered_set<std::string> steps_gcode = {
// Belt printer G-code axis remap (only affects G-code output, not slicing). // Belt printer G-code axis remap (only affects G-code output, not slicing).
"belt_gcode_remap_x", "gcode_remap_x",
"belt_gcode_remap_y", "gcode_remap_y",
"belt_gcode_remap_z", "gcode_remap_z",
"belt_origin_snap_x", "belt_origin_offset_x", "belt_origin_snap_x", "belt_origin_offset_x",
"belt_origin_snap_y", "belt_origin_offset_y", "belt_origin_snap_y", "belt_origin_offset_y",
"belt_origin_snap_z", "belt_origin_offset_z", "belt_origin_snap_z", "belt_origin_offset_z",
@@ -308,9 +308,9 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n
|| opt_key == "belt_scale_y_angle" || opt_key == "belt_scale_y_angle"
|| opt_key == "belt_scale_z" || opt_key == "belt_scale_z"
|| opt_key == "belt_scale_z_angle" || opt_key == "belt_scale_z_angle"
|| opt_key == "belt_preslice_remap_x" || opt_key == "preslice_remap_x"
|| opt_key == "belt_preslice_remap_y" || opt_key == "preslice_remap_y"
|| opt_key == "belt_preslice_remap_z") { || opt_key == "preslice_remap_z") {
osteps.emplace_back(posSlice); osteps.emplace_back(posSlice);
} else if ( } else if (
opt_key == "belt_support_floor_offset" opt_key == "belt_support_floor_offset"
+26 -26
View File
@@ -314,18 +314,18 @@ static t_config_enum_values s_keys_map_BeltScaleMode {
}; };
CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(BeltScaleMode) CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(BeltScaleMode)
static t_config_enum_values s_keys_map_BeltRemapAxis { static t_config_enum_values s_keys_map_RemapAxis {
{ "pos_x", int(BeltRemapAxis::PosX) }, { "pos_x", int(RemapAxis::PosX) },
{ "pos_y", int(BeltRemapAxis::PosY) }, { "pos_y", int(RemapAxis::PosY) },
{ "pos_z", int(BeltRemapAxis::PosZ) }, { "pos_z", int(RemapAxis::PosZ) },
{ "neg_x", int(BeltRemapAxis::NegX) }, { "neg_x", int(RemapAxis::NegX) },
{ "neg_y", int(BeltRemapAxis::NegY) }, { "neg_y", int(RemapAxis::NegY) },
{ "neg_z", int(BeltRemapAxis::NegZ) }, { "neg_z", int(RemapAxis::NegZ) },
{ "rev_x", int(BeltRemapAxis::RevX) }, { "rev_x", int(RemapAxis::RevX) },
{ "rev_y", int(BeltRemapAxis::RevY) }, { "rev_y", int(RemapAxis::RevY) },
{ "rev_z", int(BeltRemapAxis::RevZ) }, { "rev_z", int(RemapAxis::RevZ) },
}; };
CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(BeltRemapAxis) CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(RemapAxis)
static t_config_enum_values s_keys_map_BeltSupportFloorMode { static t_config_enum_values s_keys_map_BeltSupportFloorMode {
{ "none", int(BeltSupportFloorMode::None) }, { "none", int(BeltSupportFloorMode::None) },
@@ -6120,49 +6120,49 @@ void PrintConfigDef::init_fff_params()
add_belt_scale_angle("belt_scale_z_angle", "Angle"); add_belt_scale_angle("belt_scale_z_angle", "Angle");
// G-code axis remap with sign // G-code axis remap with sign
auto add_belt_remap = [this](const char *key, const char *label, const char *tooltip, BeltRemapAxis default_axis) { auto add_belt_remap = [this](const char *key, const char *label, const char *tooltip, RemapAxis default_axis) {
auto def = this->add(key, coEnum); auto def = this->add(key, coEnum);
def->label = L(label); def->label = L(label);
def->category = L("Printable space"); def->category = L("Printable space");
def->tooltip = L(tooltip); def->tooltip = L(tooltip);
def->enum_keys_map = &ConfigOptionEnum<BeltRemapAxis>::get_enum_values(); def->enum_keys_map = &ConfigOptionEnum<RemapAxis>::get_enum_values();
def->enum_values = {"pos_x", "pos_y", "pos_z", "neg_x", "neg_y", "neg_z", "rev_x", "rev_y", "rev_z"}; def->enum_values = {"pos_x", "pos_y", "pos_z", "neg_x", "neg_y", "neg_z", "rev_x", "rev_y", "rev_z"};
def->enum_labels = {L("+X"), L("+Y"), L("+Z"), L("-X"), L("-Y"), L("-Z"), L("Rev X"), L("Rev Y"), L("Rev Z")}; def->enum_labels = {L("+X"), L("+Y"), L("+Z"), L("-X"), L("-Y"), L("-Z"), L("Rev X"), L("Rev Y"), L("Rev Z")};
def->mode = comAdvanced; def->mode = comSimple; // Visibility controlled by toggle_line in Tab.cpp
def->set_default_value(new ConfigOptionEnum<BeltRemapAxis>(default_axis)); def->set_default_value(new ConfigOptionEnum<RemapAxis>(default_axis));
}; };
add_belt_remap("belt_preslice_remap_x", "X", add_belt_remap("preslice_remap_x", "X",
"Before slicing, which model-space axis becomes the slicer's X axis. " "Before slicing, which model-space axis becomes the slicer's X axis. "
"Use this to re-orient the coordinate system so the slicer's XY plane matches " "Use this to re-orient the coordinate system so the slicer's XY plane matches "
"your belt printer's physical bed plane. For a printer whose bed is in the XZ plane, " "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. " "set Y to +Z and Z to +Y (or -Y) to swap the vertical and belt-travel axes. "
"Default +X: no change.", "Default +X: no change.",
BeltRemapAxis::PosX); RemapAxis::PosX);
add_belt_remap("belt_preslice_remap_y", "Y", add_belt_remap("preslice_remap_y", "Y",
"Before slicing, which model-space axis becomes the slicer's Y axis. " "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 " "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 " "belt surface runs along the Z axis, map Y to +Z here so the slicer slices "
"along the correct plane. Default +Y: no change.", "along the correct plane. Default +Y: no change.",
BeltRemapAxis::PosY); RemapAxis::PosY);
add_belt_remap("belt_preslice_remap_z", "Z", add_belt_remap("preslice_remap_z", "Z",
"Before slicing, which model-space axis becomes the slicer's Z axis (layer stacking direction). " "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 " "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). " "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.", "Rev mode mirrors relative to the build volume maximum. Default +Z: no change.",
BeltRemapAxis::PosZ); RemapAxis::PosZ);
add_belt_remap("belt_gcode_remap_x", "X", "Which slicing axis maps to machine X in G-code output. Applied AFTER slicing, during G-code generation.", BeltRemapAxis::PosX); 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);
add_belt_remap("belt_gcode_remap_y", "Y", "Which slicing axis maps to machine Y in G-code output. Applied AFTER slicing, during G-code generation.", BeltRemapAxis::PosY); 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);
add_belt_remap("belt_gcode_remap_z", "Z", "Which slicing axis maps to machine Z in G-code output. Applied AFTER slicing, during G-code generation.", BeltRemapAxis::PosZ); 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);
def = this->add("belt_gcode_back_transform", coBool); def = this->add("gcode_back_transform", coBool);
def->label = L("G-code back-transform"); def->label = L("G-code back-transform");
def->category = L("Printable space"); def->category = L("Printable space");
def->tooltip = L("Reverse the shear/scale transform applied during slicing so G-code " def->tooltip = L("Reverse the shear/scale transform applied during slicing so G-code "
"coordinates are in the machine's physical coordinate space. " "coordinates are in the machine's physical coordinate space. "
"Requires at least one shear axis with global mode enabled."); "Requires at least one shear axis with global mode enabled.");
def->mode = comAdvanced; def->mode = comSimple; // Visibility controlled by toggle_line in Tab.cpp
def->set_default_value(new ConfigOptionBool(false)); def->set_default_value(new ConfigOptionBool(false));
auto add_belt_origin_snap = [this](const char *key_snap, const char *key_offset, auto add_belt_origin_snap = [this](const char *key_snap, const char *key_offset,
+9 -9
View File
@@ -182,7 +182,7 @@ enum class BeltAxis
Z = 2, Z = 2,
}; };
enum class BeltRemapAxis enum class RemapAxis
{ {
PosX = 0, PosY = 1, PosZ = 2, PosX = 0, PosY = 1, PosZ = 2,
NegX = 3, NegY = 4, NegZ = 5, NegX = 3, NegY = 4, NegZ = 5,
@@ -550,7 +550,7 @@ CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(SlicingMode)
CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(BeltShearMode) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(BeltShearMode)
CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(BeltScaleMode) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(BeltScaleMode)
CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(BeltAxis) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(BeltAxis)
CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(BeltRemapAxis) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(RemapAxis)
CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(BeltSupportFloorMode) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(BeltSupportFloorMode)
CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(BeltSupportZOffsetMode) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(BeltSupportZOffsetMode)
CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(SupportMaterialPattern) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(SupportMaterialPattern)
@@ -1488,13 +1488,13 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE(
((ConfigOptionFloat, belt_scale_y_angle)) ((ConfigOptionFloat, belt_scale_y_angle))
((ConfigOptionEnum<BeltScaleMode>, belt_scale_z)) ((ConfigOptionEnum<BeltScaleMode>, belt_scale_z))
((ConfigOptionFloat, belt_scale_z_angle)) ((ConfigOptionFloat, belt_scale_z_angle))
((ConfigOptionEnum<BeltRemapAxis>, belt_preslice_remap_x)) ((ConfigOptionEnum<RemapAxis>, preslice_remap_x))
((ConfigOptionEnum<BeltRemapAxis>, belt_preslice_remap_y)) ((ConfigOptionEnum<RemapAxis>, preslice_remap_y))
((ConfigOptionEnum<BeltRemapAxis>, belt_preslice_remap_z)) ((ConfigOptionEnum<RemapAxis>, preslice_remap_z))
((ConfigOptionEnum<BeltRemapAxis>, belt_gcode_remap_x)) ((ConfigOptionEnum<RemapAxis>, gcode_remap_x))
((ConfigOptionEnum<BeltRemapAxis>, belt_gcode_remap_y)) ((ConfigOptionEnum<RemapAxis>, gcode_remap_y))
((ConfigOptionEnum<BeltRemapAxis>, belt_gcode_remap_z)) ((ConfigOptionEnum<RemapAxis>, gcode_remap_z))
((ConfigOptionBool, belt_gcode_back_transform)) ((ConfigOptionBool, gcode_back_transform))
((ConfigOptionBool, belt_origin_snap_x)) ((ConfigOptionBool, belt_origin_snap_x))
((ConfigOptionFloat, belt_origin_offset_x)) ((ConfigOptionFloat, belt_origin_offset_x))
((ConfigOptionBool, belt_origin_snap_y)) ((ConfigOptionBool, belt_origin_snap_y))
+22 -2
View File
@@ -144,11 +144,31 @@ static std::vector<VolumeSlices> slice_volumes_inner(
params_base.closing_radius = print_object_config.slice_closing_radius.value; params_base.closing_radius = print_object_config.slice_closing_radius.value;
params_base.extra_offset = 0; params_base.extra_offset = 0;
params_base.trafo = object_trafo; params_base.trafo = object_trafo;
// Standalone pre-slice axis remap (works without belt mode).
bool has_remap = BeltTransformPipeline::has_preslice_remap(print_config);
if (has_remap)
params_base.trafo = BeltTransformPipeline::build_preslice_remap(print_config) * params_base.trafo;
{ {
// Belt printer: apply pre-slice transforms (remap, shear, scale, z-shift) via strategy. // Belt-specific transforms (shear, scale, z-shift) via strategy.
auto belt_strategy = BeltSliceStrategy::create(print_config); auto belt_strategy = BeltSliceStrategy::create(print_config);
if (belt_strategy) if (belt_strategy)
belt_strategy->apply_to_trafo(params_base.trafo, model_volumes, out_belt_min_z); belt_strategy->apply_to_trafo(params_base.trafo, model_volumes, has_remap, out_belt_min_z);
}
// If only remap (no belt), still need z-shift detection.
if (has_remap && !print_config.belt_printer.value) {
double min_z = std::numeric_limits<double>::max();
for (const ModelVolume *mv : model_volumes) {
if (!mv->is_model_part()) continue;
for (const stl_vertex &v : mv->mesh().its.vertices) {
Vec3d pt = params_base.trafo * v.cast<double>();
min_z = std::min(min_z, pt.z());
}
}
if (min_z < 0. && min_z != std::numeric_limits<double>::max()) {
Transform3d z_shift = Transform3d::Identity();
z_shift.matrix()(2, 3) = -min_z;
params_base.trafo = z_shift * params_base.trafo;
}
} }
//BBS: 0.0025mm is safe enough to simplify the data to speed slicing up for high-resolution model. //BBS: 0.0025mm is safe enough to simplify the data to speed slicing up for high-resolution model.
//Also has on influence on arc fitting which has default resolution 0.0125mm. //Also has on influence on arc fitting which has default resolution 0.0125mm.
+12 -9
View File
@@ -4435,19 +4435,19 @@ void TabPrinter::build_fff()
L("Remap model axes before slicing so the slicer's coordinate system matches " L("Remap model axes before slicing so the slicer's coordinate system matches "
"the physical bed orientation. For belt printers whose bed is NOT in the XY plane, " "the physical bed orientation. For belt printers whose bed is NOT in the XY plane, "
"use this to swap axes so layers are stacked in the correct physical direction.") }; "use this to swap axes so layers are stacked in the correct physical direction.") };
line.append_option(optgroup->get_option("belt_preslice_remap_x")); line.append_option(optgroup->get_option("preslice_remap_x"));
line.append_option(optgroup->get_option("belt_preslice_remap_y")); line.append_option(optgroup->get_option("preslice_remap_y"));
line.append_option(optgroup->get_option("belt_preslice_remap_z")); line.append_option(optgroup->get_option("preslice_remap_z"));
optgroup->append_line(line); optgroup->append_line(line);
} }
{ {
Line line = { L("G-code axis remap (post-slice)"), L("Remap slicing-frame axes to machine axes in G-code output. Applied AFTER slicing, during G-code generation.") }; Line line = { L("G-code axis remap (post-slice)"), L("Remap slicing-frame axes to machine axes in G-code output. Applied AFTER slicing, during G-code generation.") };
line.append_option(optgroup->get_option("belt_gcode_remap_x")); line.append_option(optgroup->get_option("gcode_remap_x"));
line.append_option(optgroup->get_option("belt_gcode_remap_y")); line.append_option(optgroup->get_option("gcode_remap_y"));
line.append_option(optgroup->get_option("belt_gcode_remap_z")); line.append_option(optgroup->get_option("gcode_remap_z"));
optgroup->append_line(line); optgroup->append_line(line);
} }
optgroup->append_single_option_line("belt_gcode_back_transform"); optgroup->append_single_option_line("gcode_back_transform");
{ {
Line line = { L("Origin snap X"), L("Snap object bbox min X to offset in G-code output") }; Line line = { L("Origin snap X"), L("Snap object bbox min X to offset in G-code output") };
line.append_option(optgroup->get_option("belt_origin_snap_x")); line.append_option(optgroup->get_option("belt_origin_snap_x"));
@@ -5320,11 +5320,14 @@ void TabPrinter::toggle_options()
toggle_line("belt_printer_infinite_y", is_belt); toggle_line("belt_printer_infinite_y", is_belt);
for (auto el : {"belt_shear_x", "belt_shear_y", "belt_shear_z", for (auto el : {"belt_shear_x", "belt_shear_y", "belt_shear_z",
"belt_scale_x", "belt_scale_y", "belt_scale_z", "belt_scale_x", "belt_scale_y", "belt_scale_z",
"belt_preslice_remap_x",
"belt_gcode_remap_x", "belt_gcode_back_transform",
"belt_origin_snap_x", "belt_origin_snap_y", "belt_origin_snap_z"}) "belt_origin_snap_x", "belt_origin_snap_y", "belt_origin_snap_z"})
toggle_line(el, is_belt); toggle_line(el, is_belt);
// Remap options: visible when belt mode is on OR when UI is in developer mode.
bool show_remap = is_belt || (m_mode >= comDevelop);
for (auto el : {"preslice_remap_x", "gcode_remap_x", "gcode_back_transform"})
toggle_line(el, show_remap);
// Gray out angle/from sub-options when their parent shear/scale mode is None. // Gray out angle/from sub-options when their parent shear/scale mode is None.
auto sx = m_config->option<ConfigOptionEnum<BeltShearMode>>("belt_shear_x")->value; auto sx = m_config->option<ConfigOptionEnum<BeltShearMode>>("belt_shear_x")->value;
toggle_option("belt_shear_x_angle", is_belt && sx != BeltShearMode::None); toggle_option("belt_shear_x_angle", is_belt && sx != BeltShearMode::None);