Belt printer: retire the redundant and unused options (#16236)

Follow-up to #16195 and the review discussion on #14394 (yw4z's note
about the third column on the *Belt tilt* row). Removes the belt options
that are redundant or unused before the branch ships, so they never need
compatibility handling after a release, and fixes supports under a
leading overhang. Every removed key is on `handle_legacy()`'s ignore
list, so existing profiles and 3MFs load silently.

## Removed

- **`belt_slice_rotation_global`**, **`preslice_remap_global`**,
**`belt_preslice_global`** (*Global mesh transforms*) and
**`gcode_back_transform`** — the global mode and the back-transform are
what belt printing is; they are presumed on wherever the flags were
consulted (`PrintObjectSlice`, `BeltBackTransform`, `BeltGCode`,
`Print::process`, `PrintApply`, `GCodeViewer`). The *Belt tilt* row is
axis + angle only; the three `fdm_belt_common.json` drop the keys.
- **`preslice_remap_x/y/z`** — no profile used the pre-slice axis remap;
the belt tilt axis plus the G-code axis remap cover the machines that
exist, and its implementation only agreed with itself for a plain swap.
The forward transform is the rotation.
- **`belt_support_z_offset_mode`** and **`belt_support_floor_mode`** —
the first was never read by a generator; the second's only shipped value
(*Generator only*) is now the behaviour.
- **`first_layer_plane`**, **`first_layer_plane_offset`**,
**`first_layer_plane_thickness`** and `FirstLayerPlane.{cpp,hpp}` — the
first-layer band is measured from the belt surface and is one first
layer height thick.
- `belt_brim_instances_compatible()` and its validation warning:
instances along the belt get their brim.

## Supports under a leading overhang (the clipping at the object's local
Z = 0)

The slicing frame of a belt object started at its lowest vertex, but the
belt under the leading end of an overhang lies below that, so no
generator could reach it: normal supports stopped at the object's lowest
layer, and both tree generators carried extension hacks sized from the
pre-rotation bbox and capped at global Z = 0 (right only for the
trailing half of the belt). The frame now starts at the lowest
belt-floor point under the footprint, less a 10 mm margin along the belt
for the base of a support column, and the extensions are gone:

- **Normal supports** run in the object frame and get the global belt Z
offset shifted onto the result (as organic already did). With the offset
on the object layers, a top contact at negative Z turned the
intermediate-layer count negative and the generator allocated layers
until the kernel killed it — any overhang in the leading half of the
belt did this. The first-layer flange expansion is skipped on a belt
(the first support layer is the leading tip, not a flange).
- **Classic tree** nodes keep dropping until their whole circle is in
the belt, so a branch tapers to a tip on the belt instead of stopping a
radius above it.
- **Organic**: the belt is no longer a support blocker. A blocker is a
collision, and a branch descending onto one slides off it, down the belt
and ahead of the part; the belt is where branches end, which the
per-layer floor clipping already does.

Regression test *Belt supports reach the belt under a leading overhang*:
a cube with a fin whose underside is parallel to the layers, 20 mm ahead
of the cube and up to 41 mm of slicing Z above the belt, for normal,
organic and classic tree supports; the lowest support layer must sit on
the belt beneath its own lines.

The belt object height (the layer range) is now estimated from the box
of the mesh as placed on the bed. `raw_bounding_box()` has the
instance's Z offset removed, which was harmless for the old
rotated-extent estimate but not for one anchored at the belt floor (a
point's rotated z and the floor under it move in opposite directions
under a Z shift): with the first version of this change every part came
out as a wedge, sliced only up to its diagonal, in the GUI and CLI
alike. Caught by a GUI test pass; the leading-overhang test now also
checks that the whole part is sliced.

## Belt brim after the parallel support step

`belt_brim_obstacles()` reads every object's layers and support layers,
which another object's support step rebuilds (and now shifts) at the
same time. The brim is generated sequentially once the parallel step is
over (`PrintObject::generate_belt_brim()`). This is the race behind the
Windows arm64 segfault in *Belt brim of each object precedes its
perimeters on its own filament*.

## UI

- *Belt tilt* is two rows: the angle (Advanced) and the axis (Developer;
a profile-level kinematics choice). A shared line is shown by its first
option's mode, so they cannot share one.
- *Machine frame transforms* is five single-option rows (G-code remap X
/ Y / Z, Decouple machine-frame tilt, Machine-frame tilt angle — the
angle row only appears when decoupled) instead of two multi-column
lines; the remap fields got full labels since they stand alone now.
- The gravity indicator on the bed is a plain line along the up
direction (no cone, 60 % of the axes' length), per yw4z.
- The *Show raw G-code (belt only)* legend/canvas toggle and its `B`
shortcut are gone; the preview is the designed view.

Also carries the two-line `phong.fs` fix from #16226 (merges as a
no-op).

## Verification

- `libslic3r_tests` 1116 passed (92 648 assertions); `fff_print_tests`
351 passed (561 696 assertions).
- `scripts/clang_tidy_diff.py --base upstream/belt-printer`: no
findings.
- `scripts/orca_profile_tool.py check`: no profile references a removed
key.
- GUI target builds; a scripted GUI pass (xdotool) checked the settings
groups in every mode, slicing, export, instances, the purge tower,
calibration dialogs, the wizard, printer switching and 3MF round-trip.

The wiki pages (OrcaSlicer/OrcaSlicer_WIKI#374) get a follow-up dropping
the removed sections once this is in.
This commit is contained in:
Joseph Robertson
2026-10-07 00:44:41 -05:00
committed by GitHub
46 changed files with 530 additions and 1544 deletions
@@ -88,7 +88,6 @@
"belt_printer": "1",
"belt_slice_rotation": "x",
"belt_slice_rotation_angle": "45",
"belt_slice_rotation_global": "1",
"build_plate_tilt_x": "45",
"purge_in_prime_tower": "0",
"scan_first_layer": "0",
@@ -91,7 +91,6 @@
"belt_printer": "1",
"belt_slice_rotation": "x",
"belt_slice_rotation_angle": "45",
"belt_slice_rotation_global": "1",
"build_plate_tilt_x": "45",
"purge_in_prime_tower": "0",
"scan_first_layer": "0",
@@ -91,7 +91,6 @@
"belt_printer": "1",
"belt_slice_rotation": "x",
"belt_slice_rotation_angle": "45",
"belt_slice_rotation_global": "1",
"build_plate_tilt_x": "45",
"purge_in_prime_tower": "0",
"scan_first_layer": "0",
+4 -27
View File
@@ -25,13 +25,6 @@ void BeltGCode::write_belt_header(GCodeOutputStream &file, const Print &print)
// for the physical tilt the G-code viewer uses to enable belt view.
file.write_format("; belt_slice_rotation = %s\n", full_cfg.opt_serialize("belt_slice_rotation").c_str());
file.write_format("; belt_slice_rotation_angle = %.1f\n", print.config().belt_slice_rotation_angle.value);
file.write_format("; belt_slice_rotation_global = %d\n", print.config().belt_slice_rotation_global.value ? 1 : 0);
// Pre-slice remap configs
file.write_format("; preslice_remap_x = %s\n", full_cfg.opt_serialize("preslice_remap_x").c_str());
file.write_format("; preslice_remap_y = %s\n", full_cfg.opt_serialize("preslice_remap_y").c_str());
file.write_format("; preslice_remap_z = %s\n", full_cfg.opt_serialize("preslice_remap_z").c_str());
file.write_format("; preslice_remap_global = %d\n", print.config().preslice_remap_global.value ? 1 : 0);
file.write_format("; belt_preslice_global = %d\n", print.config().belt_preslice_global.value ? 1 : 0);
// Machine-frame transform: shear (tan) + scale (1/cos) derived from the belt
// tilt angle (or belt_frame_tilt_angle when decoupled).
file.write_format("; belt_frame_tilt_decouple = %d\n", print.config().belt_frame_tilt_decouple.value ? 1 : 0);
@@ -40,26 +33,10 @@ void BeltGCode::write_belt_header(GCodeOutputStream &file, const Print &print)
void BeltGCode::on_set_origin(const PrintObject * /*obj*/, const Point & /*inst_shift*/)
{
// Global pre-slice mode: adjust origin using computed correction.
// Transform the origin through the belt pipeline so that
// back_transform(T * origin) = origin (correct machine position).
//
// Flags that trigger this path:
// belt_preslice_global — full pipeline (rotation * remap) is global
// preslice_remap_global — only the pre-slice remap is global
// belt_slice_rotation_global — slicing rotation treated as global (matches
// the per-instance Z-offset added in PrintObjectSlice.cpp)
// The XY origin adjustment uses the FULL forward transform, because the
// back_transform applied during G-code emission is always the inverse of
// the full pipeline.
bool use_global = m_config.belt_preslice_global.value
|| (m_config.preslice_remap_global.value
&& BeltTransformPipeline::has_preslice_remap(m_config))
|| (m_config.belt_slice_rotation_global.value
&& m_config.belt_slice_rotation.value != BeltRotationAxis::None
&& std::abs(m_config.belt_slice_rotation_angle.value) > EPSILON);
if (!use_global)
return;
// Matches the per-instance Z-offset added in PrintObjectSlice.cpp: transform
// the origin through the belt pipeline so that back_transform(T * origin) =
// origin (correct machine position). The back_transform applied during
// G-code emission is the inverse of the forward transform.
// Adjust origin: transform through belt forward pipeline so that
// the back-transform correctly recovers model-space positions.
+21 -8
View File
@@ -14,12 +14,7 @@ void BeltSliceStrategy::apply_preslice_transforms(Transform3d &trafo,
const ModelVolumePtrs &model_volumes,
double *out_belt_min_z)
{
// 1. Standalone pre-slice axis remap (works without belt mode).
const bool has_remap = BeltTransformPipeline::has_preslice_remap(config);
if (has_remap)
trafo = BeltTransformPipeline::build_preslice_remap(config) * trafo;
// 2. Belt rotation — the sole mesh-side belt transform (matching
// 1. Belt rotation — the sole mesh-side belt transform (matching
// BeltTransformPipeline::build_forward_transform). Only active in
// belt-printer mode.
bool has_rotation = false;
@@ -32,10 +27,10 @@ void BeltSliceStrategy::apply_preslice_transforms(Transform3d &trafo,
}
}
if (!has_remap && !has_rotation)
if (!has_rotation)
return;
// 3. Z-shift — detect if the mesh clips below the build plate after the
// 2. Z-shift — detect if the mesh clips below the build plate after the
// transforms and lift it. Each mesh vertex must be brought into object space
// via mv->get_matrix() before applying the full trafo (which is in object
// space). Missing this on assemblies (where per-volume get_matrix() positions
@@ -43,6 +38,20 @@ void BeltSliceStrategy::apply_preslice_transforms(Transform3d &trafo,
// coordinates rather than object-space coordinates, so volumes translated along
// the slicer's Z axis would be silently excluded from the bound check.
//
// The lift is measured to the lowest point of the SUPPORT region, not of the
// mesh: the belt floor (z = shear * u in this rotated frame, u the from-axis
// coordinate) runs below every vertex, and under the leading end of an
// overhang it lies below the lowest vertex by up to the overhang's length
// times the shear. Supports have to reach that floor, and every support
// generator works in layers at z >= 0, so z = 0 has to be the lowest floor
// point under the footprint. The layers between it and the first vertex
// come out empty, which belt slicing already tolerates (the bottom corner
// of a tilted part is a point). Vertices on the belt have z == floor, so
// for a part resting on the belt this is simply the floor at its leading
// extreme, less the frame margin (see BeltTransformPipeline::frame_margin).
BeltTransformPipeline::BeltFloorParams floor;
const bool has_floor = BeltTransformPipeline::floor_shear(config, floor);
double min_z = std::numeric_limits<double>::max();
for (const ModelVolume *mv : model_volumes) {
if (!mv->is_model_part()) continue;
@@ -52,8 +61,12 @@ void BeltSliceStrategy::apply_preslice_transforms(Transform3d &trafo,
Vec3d vm = v.cast<double>();
Vec3d pt = vol_trafo * vm;
min_z = std::min(min_z, pt.z());
if (has_floor)
min_z = std::min(min_z, floor.shear_factor * (floor.from_axis == 0 ? pt.x() : pt.y()));
}
}
if (has_floor && min_z != std::numeric_limits<double>::max())
min_z -= BeltTransformPipeline::frame_margin(floor);
const double z_shift_val = (min_z < 0. && min_z != std::numeric_limits<double>::max()) ? -min_z : 0.;
if (z_shift_val > 0.) {
Transform3d z_shift = Transform3d::Identity();
+60 -111
View File
@@ -16,50 +16,6 @@ namespace Slic3r {
// ---- Matrix builders ------------------------------------------------------
Transform3d BeltTransformPipeline::build_preslice_remap(const PrintConfig &config)
{
Transform3d pre_remap = Transform3d::Identity();
if (!has_preslice_remap(config))
return pre_remap;
int pre_rx = int(config.preslice_remap_x.value);
int pre_ry = int(config.preslice_remap_y.value);
int pre_rz = int(config.preslice_remap_z.value);
// Each remap value selects a source axis and sign.
auto remap_column = [](int r) -> Vec3d {
int axis = r % 3;
Vec3d col = Vec3d::Zero();
if (r < 3) col[axis] = 1.0; // +axis
else if (r < 6) col[axis] = -1.0; // -axis
else col[axis] = -1.0; // Rev: max - pos = -(pos - max)
return col;
};
Matrix3d remap_lin;
remap_lin.col(0) = remap_column(pre_rx);
remap_lin.col(1) = remap_column(pre_ry);
remap_lin.col(2) = remap_column(pre_rz);
pre_remap.linear() = remap_lin;
// Translation for Rev modes (needs build volume extents).
if (pre_rx >= 6 || pre_ry >= 6 || pre_rz >= 6) {
BoundingBoxf bbox_bed(config.printable_area.values);
Vec3d vol_max(bbox_bed.max.x(), bbox_bed.max.y(),
config.printable_height.value);
Vec3d remap_trans = Vec3d::Zero();
auto add_rev = [&](int r, int out) {
if (r >= 6) remap_trans[out] = vol_max[r % 3];
};
add_rev(pre_rx, 0);
add_rev(pre_ry, 1);
add_rev(pre_rz, 2);
pre_remap.translation() = remap_trans;
}
return pre_remap;
}
Matrix3d BeltTransformPipeline::build_rotation_matrix(const PrintConfig &config, bool *has_rot_out)
{
BeltRotationAxis axis = config.belt_slice_rotation.value;
@@ -81,58 +37,50 @@ Matrix3d BeltTransformPipeline::build_rotation_matrix(const PrintConfig &config,
Transform3d BeltTransformPipeline::build_forward_transform(const PrintConfig &config)
{
// Mesh-side belt transform: rotation applied after the pre-slice axis remap.
// (Shear & scale are a g-code-side stage, not part of the mesh transform.)
Transform3d pre_remap = build_preslice_remap(config);
Matrix3d rot = build_rotation_matrix(config);
// Mesh-side belt transform: the rotation. (Shear & scale are a g-code-side
// stage, not part of the mesh transform.)
Transform3d combined = Transform3d::Identity();
combined.linear() = rot;
combined = combined * pre_remap;
combined.linear() = build_rotation_matrix(config);
return combined;
}
// ---- Bounding box remap ---------------------------------------------------
BoundingBoxf3 BeltTransformPipeline::remap_bbox(const BoundingBoxf3 &bb, const PrintConfig &config)
{
if (!has_preslice_remap(config))
return bb; // Identity remap, or belt mode off.
int pre_rx = int(config.preslice_remap_x.value);
int pre_ry = int(config.preslice_remap_y.value);
int pre_rz = int(config.preslice_remap_z.value);
auto remap_coord = [](int r, const Vec3d &v) -> double {
int axis = r % 3;
if (r < 3) return v[axis];
return -v[axis];
};
Vec3d mn = bb.min.cast<double>(), mx = bb.max.cast<double>();
BoundingBoxf3 rbb;
for (int i = 0; i < 8; ++i) {
Vec3d c((i & 1) ? mx.x() : mn.x(),
(i & 2) ? mx.y() : mn.y(),
(i & 4) ? mx.z() : mn.z());
Vec3d rc(remap_coord(pre_rx, c), remap_coord(pre_ry, c), remap_coord(pre_rz, c));
if (i == 0) rbb = BoundingBoxf3(rc, rc);
else rbb.merge(rc);
}
return rbb;
}
BoundingBoxf3 BeltTransformPipeline::remap_bbox(const ModelObject &model_object, const PrintConfig &config)
{
return remap_bbox(model_object.raw_bounding_box(), config);
}
// ---- Belt floor parameters ------------------------------------------------
// Shared implementation for both PrintConfig and DynamicPrintConfig.
// Template avoids duplicating the math for the two config types.
namespace {
// Belt floor in the rotated slicer frame: the image of z_machine = 0 under R.
// R(+α, X): point (·, y, 0) → (·, cos α · y, sin α · y) ⇒ z = tan(α) · y_s
// R(+α, Y): point (x, ·, 0) → (cos α · x, ·, -sin α · x) ⇒ z = -tan(α) · x_s
// R(+α, Z): point (·, ·, 0) → (·, ·, 0); no tilt → no floor
void belt_floor_shear(BeltRotationAxis rot_axis, double angle_rad, BeltTransformPipeline::BeltFloorParams &out)
{
double sin_a = std::sin(angle_rad), cos_a = std::cos(angle_rad);
switch (rot_axis) {
case BeltRotationAxis::X:
out.shear_factor = (std::abs(cos_a) > EPSILON) ? sin_a / cos_a : 0.;
out.from_axis = 1; // Y
break;
case BeltRotationAxis::Y:
out.shear_factor = (std::abs(cos_a) > EPSILON) ? -sin_a / cos_a : 0.;
out.from_axis = 0; // X
break;
case BeltRotationAxis::Z:
default:
out.shear_factor = 0.0;
out.from_axis = 1;
break;
}
}
// Z of the belt floor directly under a point of the rotated (unshifted) frame.
inline double belt_floor_z(const BeltTransformPipeline::BeltFloorParams &fp, const Vec3d &pt)
{
return fp.shear_factor * (fp.from_axis == 0 ? pt.x() : pt.y());
}
template<typename Config>
BeltTransformPipeline::BeltHeightResult compute_belt_height_and_floor_impl(
const Config &config, const BoundingBoxf3 &bb, double original_height)
@@ -176,38 +124,28 @@ BeltTransformPipeline::BeltHeightResult compute_belt_height_and_floor_impl(
default: unit_axis = Vec3d::UnitX(); break;
}
Matrix3d R = Eigen::AngleAxisd(angle_rad, unit_axis).toRotationMatrix();
belt_floor_shear(rot_axis, angle_rad, result.floor_params);
// The slicing frame starts at the lowest point of the support region: the
// lowest belt-floor point under the footprint, not the lowest vertex. The
// belt under the leading end of an overhang lies below every vertex of the
// part, and supports have to be able to reach it (see
// BeltSliceStrategy::apply_preslice_transforms for the exact vertex-scan
// counterpart of this bbox estimate).
double min_rz = std::numeric_limits<double>::max();
double max_rz = std::numeric_limits<double>::lowest();
for (int i = 0; i < 8; ++i) {
Vec3d c((i & 1) ? bb.max.x() : bb.min.x(),
(i & 2) ? bb.max.y() : bb.min.y(),
(i & 4) ? bb.max.z() : bb.min.z());
double z = (R * c).z();
Vec3d rc = R * c;
double z = rc.z();
min_rz = std::min(min_rz, z);
max_rz = std::max(max_rz, z);
min_rz = std::min(min_rz, belt_floor_z(result.floor_params, rc));
}
min_rz -= BeltTransformPipeline::frame_margin(result.floor_params);
result.object_height = max_rz - min_rz;
// Belt floor in slicer-frame is the image of z_machine = 0 under R.
// R(+α, X): point (·, y, 0) → (·, cos α · y, sin α · y) ⇒ z = tan(α) · y_s
// R(+α, Y): point (x, ·, 0) → (cos α · x, ·, -sin α · x) ⇒ z = -tan(α) · x_s
// R(+α, Z): point (·, ·, 0) → (·, ·, 0); no tilt → no floor
double sin_a = std::sin(angle_rad), cos_a = std::cos(angle_rad);
switch (rot_axis) {
case BeltRotationAxis::X:
result.floor_params.shear_factor = (std::abs(cos_a) > EPSILON) ? sin_a / cos_a : 0.;
result.floor_params.from_axis = 1; // Y
break;
case BeltRotationAxis::Y:
result.floor_params.shear_factor = (std::abs(cos_a) > EPSILON) ? -sin_a / cos_a : 0.;
result.floor_params.from_axis = 0; // X
break;
case BeltRotationAxis::Z:
default:
result.floor_params.shear_factor = 0.0;
result.floor_params.from_axis = 1;
break;
}
result.floor_params.z_shift = bb.min.z() + ((min_rz < 0.) ? -min_rz : 0.);
return result;
@@ -216,15 +154,26 @@ BeltTransformPipeline::BeltHeightResult compute_belt_height_and_floor_impl(
} // anonymous namespace
BeltTransformPipeline::BeltHeightResult BeltTransformPipeline::compute_belt_height_and_floor(
const PrintConfig &config, const BoundingBoxf3 &remapped_bbox, double original_height)
const PrintConfig &config, const BoundingBoxf3 &bbox, double original_height)
{
return compute_belt_height_and_floor_impl(config, remapped_bbox, original_height);
return compute_belt_height_and_floor_impl(config, bbox, original_height);
}
BeltTransformPipeline::BeltHeightResult BeltTransformPipeline::compute_belt_height_and_floor(
const DynamicPrintConfig &config, const BoundingBoxf3 &remapped_bbox, double original_height)
const DynamicPrintConfig &config, const BoundingBoxf3 &bbox, double original_height)
{
return compute_belt_height_and_floor_impl(config, remapped_bbox, original_height);
return compute_belt_height_and_floor_impl(config, bbox, original_height);
}
bool BeltTransformPipeline::floor_shear(const PrintConfig &config, BeltFloorParams &out)
{
out = BeltFloorParams{};
const BeltRotationAxis rot_axis = config.belt_slice_rotation.value;
const double rot_angle = config.belt_slice_rotation_angle.value;
if (rot_axis == BeltRotationAxis::None || std::abs(rot_angle) <= EPSILON)
return false;
belt_floor_shear(rot_axis, Geometry::deg2rad(rot_angle), out);
return std::abs(out.shear_factor) > EPSILON;
}
} // namespace Slic3r
+23 -48
View File
@@ -16,7 +16,7 @@ class ModelObject;
// Shared belt-printer transform math.
//
// The pre-slice pipeline applied in PrintObjectSlice.cpp is:
// trafo_out = z_shift * rotation * pre_remap * trafo_in
// trafo_out = z_shift * rotation * trafo_in
//
// Rotation is the sole mesh-side belt transform; shear & scale are applied
// to the g-code instead (see MachineFrameTransform). This class provides the
@@ -47,39 +47,11 @@ class BeltTransformPipeline
public:
// ---- Identity checks --------------------------------------------------
// Whether the axis remaps (preslice_remap_* and gcode_remap_*) apply at all.
// The remap fields are only offered in the belt printer group, so a value
// left in a profile must not change a non-belt print: with belt mode off every
// belt-only key is a no-op. This is the one place to widen if a non-belt use
// ever needs them.
// Whether the G-code axis remap applies at all. The remap fields are only
// offered in the belt printer group, so a value left in a profile must not
// change a non-belt print: with belt mode off every belt-only key is a no-op.
// This is the one place to widen if a non-belt use ever needs them.
static bool axis_remap_enabled(const PrintConfig &config) { return config.belt_printer.value; }
static bool axis_remap_enabled(const DynamicPrintConfig &config)
{
auto *opt = config.option<ConfigOptionBool>("belt_printer");
return opt != nullptr && opt->value;
}
static bool has_preslice_remap(const PrintConfig &config)
{
return axis_remap_enabled(config) &&
(int(config.preslice_remap_x.value) != int(RemapAxis::PosX) ||
int(config.preslice_remap_y.value) != int(RemapAxis::PosY) ||
int(config.preslice_remap_z.value) != int(RemapAxis::PosZ));
}
// Overload accepting DynamicPrintConfig (used in static slicing_parameters).
static bool has_preslice_remap(const DynamicPrintConfig &config)
{
if (! axis_remap_enabled(config))
return false;
auto get_int = [&](const char *key) -> int {
auto *opt = config.option<ConfigOptionEnum<RemapAxis>>(key);
return opt ? int(opt->value) : 0;
};
return get_int("preslice_remap_x") != int(RemapAxis::PosX) ||
get_int("preslice_remap_y") != int(RemapAxis::PosY) ||
get_int("preslice_remap_z") != int(RemapAxis::PosZ);
}
static bool has_rotation(const PrintConfig &config)
{
@@ -118,26 +90,16 @@ public:
// ---- Matrix builders --------------------------------------------------
// Build the pre-slice axis remap transform (includes Rev-mode translation).
static Transform3d build_preslice_remap(const PrintConfig &config);
// Build the 3x3 rotation matrix from belt_slice_rotation* config.
// Returns Identity if rotation axis is None or angle is ~0.
// Also sets has_rot_out if non-null.
static Matrix3d build_rotation_matrix(const PrintConfig &config, bool *has_rot_out = nullptr);
// Combined forward transform (rotation * pre_remap) — the mesh-side belt
// transform that BeltSliceStrategy applies and BeltBackTransform inverts.
// Forward transform (the rotation) — the mesh-side belt transform that
// BeltSliceStrategy applies and BeltBackTransform inverts.
// Does NOT include the per-object Z-shift.
static Transform3d build_forward_transform(const PrintConfig &config);
// ---- Bounding box remap -----------------------------------------------
// Remap a bounding box through the pre-slice axis remap.
// Returns the original bbox if remap is identity.
static BoundingBoxf3 remap_bbox(const BoundingBoxf3 &bb, const PrintConfig &config);
static BoundingBoxf3 remap_bbox(const ModelObject &model_object, const PrintConfig &config);
// ---- Belt floor parameters --------------------------------------------
struct BeltFloorParams {
@@ -146,6 +108,19 @@ public:
double z_shift = 0.0;
};
// Shear factor and from-axis of the belt floor in the rotated slicer frame
// (z_floor = shear_factor * u, u = the from-axis coordinate), for the
// rotation the config selects. z_shift is left at 0. Returns false (and
// zero shear) when the config has no tilt.
static bool floor_shear(const PrintConfig &config, BeltFloorParams &out);
// How far below the lowest belt-floor point under the footprint the slicing
// frame starts, in slicing Z. A support column meeting the belt is wider at
// its base than at its tip, so under a leading overhang the base reaches ahead
// of the part along the belt, and the layers that trim it to the belt plane
// lie below that lowest point: 10 mm along the belt.
static double frame_margin(const BeltFloorParams &fp) { return 10. * std::abs(fp.shear_factor); }
// Result of computing belt height + floor params.
struct BeltHeightResult {
double object_height; // Effective object height after shear/scale
@@ -153,15 +128,15 @@ public:
};
// Compute effective object height and belt floor parameters from config
// and pre-remapped bounding box. original_height is the input height
// and the object's bounding box. original_height is the input height
// (bb.size().z() or model_object.max_z()).
static BeltHeightResult compute_belt_height_and_floor(
const PrintConfig &config, const BoundingBoxf3 &remapped_bbox,
const PrintConfig &config, const BoundingBoxf3 &bbox,
double original_height);
// Overload for DynamicPrintConfig (used by static slicing_parameters).
static BeltHeightResult compute_belt_height_and_floor(
const DynamicPrintConfig &config, const BoundingBoxf3 &remapped_bbox,
const DynamicPrintConfig &config, const BoundingBoxf3 &bbox,
double original_height);
};
-2
View File
@@ -94,8 +94,6 @@ set(lisbslic3r_sources
BeltSliceStrategy.hpp
BeltTransform.cpp
BeltTransform.hpp
FirstLayerPlane.cpp
FirstLayerPlane.hpp
Brim.cpp
BrimEarsPoint.hpp
Brim.hpp
-229
View File
@@ -1,229 +0,0 @@
#include "FirstLayerPlane.hpp"
#include "BeltTransform.hpp"
#include "BoundingBox.hpp"
#include "Point.hpp"
#include "PrintConfig.hpp"
#include "libslic3r.h"
#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
-76
View File
@@ -1,76 +0,0 @@
#ifndef slic3r_FirstLayerPlane_hpp_
#define slic3r_FirstLayerPlane_hpp_
#include "libslic3r.h"
#include "Point.hpp"
#include "BoundingBox.hpp"
#include "PrintConfig.hpp"
namespace Slic3r {
// Decides which extrusions get "first layer" treatment (no fan, slow speed,
// initial-layer accel/jerk, deferred temperature drop) by reference to a
// configurable plane in slicing-frame coordinates rather than the slicing
// layer index.
//
// On a normal flat-bed printer the plane is XY at slicing_Z = 0 and the
// evaluator is INACTIVE — every call site short-circuits back to the legacy
// `Layer::id() == 0` test. On a belt printer with a Z-from-Y shear the
// belt surface (machine_Z = 0) maps to a plane in slicing-frame coordinates
// derived from the gcode axis remap, so layer-index-based detection no
// longer matches the physical first printed surface.
//
// Plane representation: unit normal `n` (slicing frame) and offset along
// the normal such that the plane equation is `n · p == offset`. Signed
// perpendicular distance is `d(p) = n · p - offset`. Positive distance
// means "away from the belt surface", negative means "below the plane".
class FirstLayerPlane
{
public:
explicit FirstLayerPlane(const PrintConfig &config);
// Inactive when the legacy XY layer-index path should be used. This
// covers all non-belt printers and any belt printer where the user
// explicitly picked XY mode.
bool is_active() const { return m_active; }
FirstLayerPlaneMode effective_mode() const{ return m_mode; }
double band_thickness_mm() const { return m_thickness_mm; }
const Vec3d & normal() const { return m_normal; }
double plane_offset() const { return m_offset; }
// Signed perpendicular distance from a slicing-frame point to the plane.
double distance_from_plane(const Vec3d &point_slicing_mm) const;
// True if perpendicular distance < first_layer_height_mm. When the
// evaluator is inactive this returns false (call sites should fall back
// to the legacy per-layer path before reaching this function).
bool is_first_layer(const Vec3d &point_slicing_mm,
double first_layer_height_mm) const;
// floor((distance - 0) / band_thickness), clamped to [0, +inf). Used
// for "first N layers" thresholds (fan, slow_down_layers). Returns 0
// for points within the band. Returns INT_MAX/2 when inactive.
int effective_layer_index(const Vec3d &point_slicing_mm) const;
// Min effective index over a 2D bbox at a fixed slicing_Z. Used for
// layer-level decisions (e.g. temperature transition gate) where we
// don't want to walk every extrusion in the layer. For axis-aligned
// planes this is exact; for tilted planes it's a tight lower bound
// (the plane projection of the bbox's extreme corner).
int min_effective_index_for_xy_bbox(const BoundingBoxf &xy_bbox_mm,
double slicing_z_mm) const;
// Same as above but the bbox spans a Z range too.
int min_effective_index_for_bbox3(const BoundingBoxf3 &bbox_mm) const;
private:
bool m_active = false;
FirstLayerPlaneMode m_mode = FirstLayerPlaneMode::XY;
Vec3d m_normal = Vec3d::UnitZ(); // unit, slicing frame
double m_offset = 0.0; // n·p == m_offset
double m_thickness_mm = 0.0;
};
} // namespace Slic3r
#endif // slic3r_FirstLayerPlane_hpp_
+12 -60
View File
@@ -107,7 +107,6 @@
#include "calib.hpp"
#include "libslic3r_version.h"
#include "GCode/BeltKinematics.hpp"
#include "FirstLayerPlane.hpp"
// Intel redesigned some TBB interface considerably when merging TBB with their oneAPI set of libraries, see GH #7332.
// We are using quite an old TBB 2017 U7. Before we update our build servers, let's use the old API, which is deprecated in up to date TBB.
#if ! defined(TBB_VERSION_MAJOR)
@@ -3153,19 +3152,13 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
print.config().printable_height.value));
}
// Build the FirstLayerPlane evaluator. When inactive (non-belt printers
// and belt printers without Z shear), all per-path call sites short-
// circuit to the legacy Layer::id() == 0 path so g-code stays bit-
// identical to the pre-feature behavior.
m_first_layer_plane = std::make_unique<FirstLayerPlane>(print.config());
// Belt writers only: travel-speed selection becomes per-point (see
// GCodeWriter::uses_pointwise_travel_speed()), which must not change for
// non-belt printers. The writer gets the same test the extrusions use, so a
// travel is judged against the belt surface (belt_height_above_floor) exactly
// like the path it leads to, and not against the FirstLayerPlane, which
// misreports the height under a non-identity gcode_remap_*. Writer points
// carry the G-code origin and extruder offset that point_to_gcode() added;
// the belt surface is described in the object's own frame.
// like the path it leads to. Writer points carry the G-code origin and
// extruder offset that point_to_gcode() added; the belt surface is described
// in the object's own frame.
if (print.config().belt_printer.value) {
m_writer.set_first_layer_point_test([this](const Vec3d &point_logical) {
const Vec2d extruder_offset = m_writer.filament() != nullptr ? EXTRUDER_CONFIG(extruder_offset) : Vec2d::Zero();
@@ -5540,7 +5533,7 @@ std::string GCode::generate_object_brim(const Print &print, const PrintObject &o
// apron band has no Layer, and giving it a synthetic one would feed a fabricated
// Layer::id() into initial-layer temperature selection, the spiral vase probe,
// gradual interpolation and cooling. Correct first-layer treatment comes from
// FirstLayerPlane in BeltAffine mode, which is evaluated per point.
// the height above the belt, which is evaluated per point.
LayerResult GCode::process_belt_brim_layer(
const Print &print,
const std::vector<LayerToPrint> &layers,
@@ -6302,40 +6295,13 @@ LayerResult GCode::process_layer(
}
}
// First-layer plane: defer the temperature/PLR transition until the
// entire layer is past the first-layer band. When the evaluator is
// inactive (non-belt printers and belt printers without Z shear) we
// fall back to the legacy `!first_layer` predicate so behavior is
// bit-identical to the pre-feature path.
// Belt printers: defer the temperature/PLR transition until the entire layer
// is past the first-layer band above the belt. Elsewhere (non-belt printers,
// support-only layers) the legacy `!first_layer` predicate applies, so
// behavior is bit-identical to the pre-feature path.
bool past_first_layer_band = !first_layer;
if (int past = this->belt_layer_past_first_layer_band(object_layer); past >= 0) {
// Belt surface known for this object: measured from the belt itself, as the
// extrusions and travels are, rather than through FirstLayerPlane.
if (int past = this->belt_layer_past_first_layer_band(object_layer); past >= 0)
past_first_layer_band = past > 0;
} else if (m_first_layer_plane && m_first_layer_plane->is_active()) {
past_first_layer_band = false;
if (object_layer != nullptr) {
// Conservatively walk the layer's lslice bboxes; if every bbox's
// most-belt-side corner is outside the first-layer band, the
// layer is fully past it.
const Layer *ol = object_layer;
int min_eff = INT_MAX;
for (const BoundingBox &bb : ol->lslices_bboxes) {
BoundingBoxf bbf(
Vec2d(unscale<double>(bb.min.x()), unscale<double>(bb.min.y())),
Vec2d(unscale<double>(bb.max.x()), unscale<double>(bb.max.y())));
int eff = m_first_layer_plane->min_effective_index_for_xy_bbox(
bbf, ol->print_z);
if (eff < min_eff) min_eff = eff;
if (min_eff <= 0) break;
}
past_first_layer_band = (min_eff > 0 && min_eff != INT_MAX);
} else if (support_layer != nullptr) {
// Support-only layers: gate on the support layer's bottom_z
// proximity to the plane. Conservative.
past_first_layer_band = !first_layer;
}
}
if (past_first_layer_band && !m_second_layer_things_done) {
// Orca: set power loss recovery
@@ -8812,7 +8778,7 @@ std::string GCode::_extrude(const ExtrusionPath &path, const std::string &path_d
if (speed == 0)
speed = filament_max_volumetric_speed / _mm3_per_mm;
// Use the FirstLayerPlane-aware effective layer index when active so
// Use the belt-aware effective layer index when on a belt printer so
// the speed fade tracks perpendicular distance from the plane on
// belt printers; otherwise this falls back to the slicing layer id.
const int _layer = this->effective_layer_index_for_point(path_point_mm);
@@ -10637,8 +10603,8 @@ std::string GCode::set_object_info(Print *print) {
// Whether an object layer lies entirely past the first-layer band above the belt:
// 1 when its lowest point is at least one band thickness above the belt, 0 when
// any of it is inside the band, -1 when the belt surface is not known for this
// layer (not a belt print, an explicit first-layer plane, or no object layer), in
// which case the caller falls back to FirstLayerPlane / the slicing layer index.
// layer (not a belt print, or no object layer), in which case the caller falls
// back to the slicing layer index.
int GCode::belt_layer_past_first_layer_band(const Layer *object_layer) const
{
if (object_layer == nullptr)
@@ -10674,20 +10640,6 @@ bool GCode::belt_height_above_floor(const Vec3d &point_slicing_mm, double &heigh
: (m_layer != nullptr ? m_layer->object() : nullptr);
if (object == nullptr)
return false;
// Respect an explicit first-layer-plane choice: only Auto and BeltAffine mean
// "use the belt". A user who selected XY, YZ or XZ has asked for the
// FirstLayerPlane evaluator and must keep it.
const FirstLayerPlaneMode mode = m_config.first_layer_plane.value;
if (mode != FirstLayerPlaneMode::Auto && mode != FirstLayerPlaneMode::BeltAffine)
return false;
// Likewise for a dialled-in plane offset. It is expressed as a machine-Z
// shift that FirstLayerPlane converts into a perpendicular distance in the
// slicing frame; this evaluator measures along slicing Z instead, so there is
// no faithful translation of it here. Honour the user's setting by deferring
// to the evaluator that implements it rather than silently dropping it.
if (std::abs(m_config.first_layer_plane_offset.value) > EPSILON)
return false;
const SlicingParameters &sp = object->slicing_parameters();
// Deliberately NOT BeltFloorContext: its init() folds in
// belt_support_floor_offset, a support-generator diagnostic. Letting that
+11 -32
View File
@@ -8,7 +8,6 @@
#include "libslic3r.h"
#include "GCodeWriter.hpp"
#include "GCode/BeltKinematics.hpp"
#include "FirstLayerPlane.hpp"
#include "Layer.hpp"
#include "Point.hpp"
#include "PlaceholderParser.hpp"
@@ -400,7 +399,6 @@ public:
// first-layer-plane access points (on_first_layer overload, effective
// index helper) are in the protected section since they're called from
// GCode internals only.
const FirstLayerPlane *first_layer_plane() const { return m_first_layer_plane.get(); }
protected:
class GCodeOutputStream {
@@ -845,7 +843,6 @@ protected:
// PrintConfig. is_active() == false on non-belt printers and on belt
// printers without a Z-axis shear; in that case all per-path plane
// checks short-circuit to the legacy Layer::id() == 0 path.
std::unique_ptr<FirstLayerPlane> m_first_layer_plane;
// Plate origin, kept so a writer replaced during export can be given it again.
std::unique_ptr<PressureEqualizer> m_pressure_equalizer;
@@ -938,45 +935,32 @@ protected:
// On the first printing layer. This flag triggers first layer speeds.
//BBS
bool on_first_layer() const { return m_layer != nullptr && m_layer->id() == 0 && abs(m_layer->bottom_z()) < EPSILON; }
// Per-point first-layer test. When the FirstLayerPlane evaluator is
// active, the result depends on the supplied slicing-frame point;
// otherwise we delegate to the legacy per-layer test. This is the
// entry point used by per-path call sites in _extrude.
// Per-point first-layer test. On a belt printer the result depends on the
// supplied slicing-frame point (its height above the belt); otherwise we
// delegate to the legacy per-layer test. This is the entry point used by
// per-path call sites in _extrude.
bool on_first_layer(const Vec3d &point_slicing_mm) const {
// Belt printers: measure height above the belt surface itself, in the
// slicing frame. See belt_height_above_floor() for why this does not go
// through FirstLayerPlane.
double h;
if (this->belt_height_above_floor(point_slicing_mm, h))
return h <= m_config.initial_layer_print_height.value + EPSILON;
if (m_first_layer_plane && m_first_layer_plane->is_active())
return m_first_layer_plane->is_first_layer(
point_slicing_mm, m_config.initial_layer_print_height.value);
return on_first_layer();
}
// "Effective layer index" used to drive layer-count thresholds like
// slow_down_layers. When the evaluator is active this returns the
// perpendicular distance to the plane in band_thickness_mm units;
// otherwise it returns the legacy slicing layer index.
// slow_down_layers. On a belt printer this is the height above the belt in
// first_layer_band_mm() units; otherwise it is the legacy slicing layer index.
int effective_layer_index_for_point(const Vec3d &point_slicing_mm) const {
double h;
if (this->belt_height_above_floor(point_slicing_mm, h)) {
const double lh = this->first_layer_band_mm();
return h <= 0. ? 0 : int(std::floor(h / lh));
}
if (m_first_layer_plane && m_first_layer_plane->is_active())
return m_first_layer_plane->effective_layer_index(point_slicing_mm);
return on_first_layer() ? 0 : layer_id();
}
// Band thickness for the *effective layer index* only. FirstLayerPlane keeps
// two separate thresholds and so must this path: is_first_layer() tests
// against initial_layer_print_height, while effective_layer_index() counts
// bands of first_layer_plane_thickness. Conflating them would apply
// first-layer treatment through a whole 1mm band on a 0.2mm first layer.
// Band thickness for the *effective layer index*: one first layer height, so
// "the first N layers" means the same height above the belt as on a flat bed.
double first_layer_band_mm() const {
double band = m_config.first_layer_plane_thickness.value;
if (band <= 0.) band = m_config.initial_layer_print_height.value;
const double band = m_config.initial_layer_print_height.value;
return band > 0. ? band : 0.2;
}
@@ -985,13 +969,8 @@ protected:
//
// The belt surface is known exactly in the slicing frame from the slicing
// parameters (belt_floor_shear_factor / _from_axis / _z_shift) -- the same
// description the support generator uses. FirstLayerPlane instead derives its
// plane by composing gcode_remap_* with the g-code back-transform, so its
// answer changes with the machine's *output* axis convention: on a printer
// with a non-identity remap it reported ~86mm of clearance for geometry
// sitting directly on the belt, and no extrusion was ever classified as
// first-layer. Measuring against the belt itself is independent of every
// remap and back-transform.
// description the support generator uses, independent of every remap and
// back-transform.
bool belt_height_above_floor(const Vec3d &point_slicing_mm, double &height_mm) const;
// 1 / 0 / -1: the object layer is entirely past the first-layer band above the
// belt / reaches into it / the belt surface is not known for it.
+2 -11
View File
@@ -10,19 +10,10 @@ bool BeltBackTransform::init_from_config(const PrintConfig &config)
m_active = false;
m_inverse = Transform3d::Identity();
if (!config.belt_printer.value || !config.gcode_back_transform.value)
if (!config.belt_printer.value)
return false;
// Require at least one active transform to proceed.
bool has_global_rotation = config.belt_slice_rotation_global.value
&& config.belt_slice_rotation.value != BeltRotationAxis::None;
bool has_preslice_global = config.belt_preslice_global.value
|| config.preslice_remap_global.value;
if (!has_global_rotation && !has_preslice_global
&& !BeltTransformPipeline::has_preslice_remap(config))
return false;
// Build the forward pipeline (rotation * pre_remap) and store its inverse.
// Build the forward pipeline (the rotation) and store its inverse.
Transform3d forward = BeltTransformPipeline::build_forward_transform(config);
if (forward.isApprox(Transform3d::Identity()))
return false;
+6 -10
View File
@@ -7,25 +7,21 @@
namespace Slic3r {
// Reverses the pre-slice remap + shear + scale transforms that
// PrintObjectSlice.cpp applies to belt printer geometry, converting G-code
// coordinates from the sliced (remapped/sheared/scaled) frame back to the
// machine's real coordinate space.
// Reverses the pre-slice rotation that PrintObjectSlice.cpp applies to belt
// printer geometry, converting G-code coordinates from the sliced (rotated)
// frame back to the machine's real coordinate space.
//
// Initialized once from PrintConfig, then applied per-point in
// BeltKinematics::to_machine() before axis remapping.
//
// Active when gcode_back_transform is true AND at least one of:
// - a shear axis has global mode enabled, or
// - a pre-slice axis remap is non-identity.
// Active on belt printers with a non-identity pre-slice rotation.
class BeltBackTransform
{
public:
BeltBackTransform() = default;
// Initialize from belt printer config. Rebuilds the same pre-slice remap,
// shear, and scale matrices as PrintObjectSlice.cpp and precomputes the
// affine inverse. Returns true if a non-identity back-transform was computed.
// Initialize from belt printer config. Rebuilds the same pre-slice rotation
// as PrintObjectSlice.cpp and precomputes the affine inverse. Returns true if a non-identity back-transform was computed.
bool init_from_config(const PrintConfig &config);
// Apply the inverse transform to a point. Returns pos unchanged if
+2 -36
View File
@@ -2619,9 +2619,6 @@ void GCodeProcessorResult::reset() {
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;
@@ -3201,9 +3198,8 @@ void GCodeProcessor::apply_config(const DynamicPrintConfig& config)
const auto *belt = config.option<ConfigOptionBool>("belt_printer");
if (belt != nullptr) {
static const char *belt_keys[] = {
"belt_printer", "belt_slice_rotation", "belt_slice_rotation_angle", "belt_slice_rotation_global",
"belt_preslice_global", "preslice_remap_x", "preslice_remap_y", "preslice_remap_z", "preslice_remap_global",
"gcode_remap_x", "gcode_remap_y", "gcode_remap_z", "gcode_back_transform",
"belt_printer", "belt_slice_rotation", "belt_slice_rotation_angle",
"gcode_remap_x", "gcode_remap_y", "gcode_remap_z",
"belt_frame_tilt_decouple", "belt_frame_tilt_angle",
};
for (const char *key : belt_keys)
@@ -4328,36 +4324,6 @@ void GCodeProcessor::process_tags(const std::string_view comment, bool producers
} catch (...) {}
return;
}
// Belt printer: parse pre-slice axis remap from header comments.
{
auto trim = [](const std::string &s) -> std::string {
size_t start = s.find_first_not_of(" \t\r\n");
size_t end = s.find_last_not_of(" \t\r\n");
return (start == std::string::npos) ? "" : s.substr(start, end - start + 1);
};
// Pre-slice axis remap
auto parse_remap_axis = [](const std::string &s) -> RemapAxis {
if (s == "pos_x") return RemapAxis::PosX;
if (s == "pos_y") return RemapAxis::PosY;
if (s == "pos_z") return RemapAxis::PosZ;
if (s == "neg_x") return RemapAxis::NegX;
if (s == "neg_y") return RemapAxis::NegY;
if (s == "neg_z") return RemapAxis::NegZ;
if (s == "rev_x") return RemapAxis::RevX;
if (s == "rev_y") return RemapAxis::RevY;
if (s == "rev_z") return RemapAxis::RevZ;
return RemapAxis::PosX;
};
if (boost::starts_with(comment, " preslice_remap_x = ")) {
m_result.preslice_remap_x = parse_remap_axis(trim(std::string(comment.substr(20)))); return;
}
if (boost::starts_with(comment, " preslice_remap_y = ")) {
m_result.preslice_remap_y = parse_remap_axis(trim(std::string(comment.substr(20)))); return;
}
if (boost::starts_with(comment, " preslice_remap_z = ")) {
m_result.preslice_remap_z = parse_remap_axis(trim(std::string(comment.substr(20)))); return;
}
}
// wipe start tag
if (boost::starts_with(comment, reserved_tag(ETags::Wipe_Start))) {
m_wiping = true;
-6
View File
@@ -304,9 +304,6 @@ class Print;
// machine frame and should not be compared against `printable_height`
// (which lives in the build-volume frame).
bool machine_frame_transform_active{ false };
RemapAxis preslice_remap_x{ RemapAxis::PosX };
RemapAxis preslice_remap_y{ RemapAxis::PosY };
RemapAxis preslice_remap_z{ RemapAxis::PosZ };
SettingsIds settings_ids;
size_t filaments_count;
bool backtrace_enabled;
@@ -405,9 +402,6 @@ class Print;
belt_tilt_angle = std::forward<Other>(other).belt_tilt_angle;
belt_z_origin = std::forward<Other>(other).belt_z_origin;
machine_frame_transform_active = std::forward<Other>(other).machine_frame_transform_active;
preslice_remap_x = std::forward<Other>(other).preslice_remap_x;
preslice_remap_y = std::forward<Other>(other).preslice_remap_y;
preslice_remap_z = std::forward<Other>(other).preslice_remap_z;
#if ENABLE_GCODE_VIEWER_STATISTICS
time = std::forward<Other>(other).time;
#endif
+3 -6
View File
@@ -1560,13 +1560,10 @@ static std::vector<std::string> s_Preset_machine_limits_options {
static std::vector<std::string> s_Preset_printer_options {
"printer_technology",
"printable_area", "extruder_printable_area", "support_parallel_printheads", "parallel_printheads_count", "parallel_printheads_bed_exclude_areas", "bed_exclude_area","bed_custom_texture", "bed_custom_model", "build_plate_tilt_x", "build_plate_tilt_y", "belt_printer", "belt_printer_infinite_y",
"belt_slice_rotation", "belt_slice_rotation_angle", "belt_slice_rotation_global",
"preslice_remap_x", "preslice_remap_y", "preslice_remap_z", "preslice_remap_global",
"gcode_remap_x", "gcode_remap_y", "gcode_remap_z", "gcode_back_transform",
"belt_slice_rotation", "belt_slice_rotation_angle",
"gcode_remap_x", "gcode_remap_y", "gcode_remap_z",
"belt_frame_tilt_decouple", "belt_frame_tilt_angle",
"belt_preslice_global",
"first_layer_plane", "first_layer_plane_offset", "first_layer_plane_thickness",
"belt_support_floor_offset", "belt_support_floor_mode", "belt_support_z_offset_mode",
"belt_support_floor_offset",
"enable_belt_purge_tower",
"gcode_flavor", "gcode_skip_config_block",
"fan_kickstart", "part_cooling_fan_min_pwm", "fan_speedup_time", "fan_speedup_overhangs",
+10 -29
View File
@@ -176,8 +176,6 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n
"gcode_remap_z",
// Machine-frame transform (derived from belt tilt; only affects G-code output).
"belt_frame_tilt_decouple", "belt_frame_tilt_angle",
"gcode_back_transform",
"first_layer_plane", "first_layer_plane_offset", "first_layer_plane_thickness",
// Only inflates the GUI bed volume, like printable_area.
"belt_printer_infinite_y",
//BBS
@@ -388,18 +386,10 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n
// Belt printer transform options change the mesh geometry before slicing.
|| opt_key == "belt_printer"
|| opt_key == "belt_slice_rotation"
|| opt_key == "belt_slice_rotation_angle"
|| opt_key == "belt_slice_rotation_global"
|| opt_key == "belt_preslice_global"
|| opt_key == "preslice_remap_global"
|| opt_key == "preslice_remap_x"
|| opt_key == "preslice_remap_y"
|| opt_key == "preslice_remap_z") {
|| opt_key == "belt_slice_rotation_angle") {
osteps.emplace_back(posSlice);
} else if (
opt_key == "belt_support_floor_offset"
|| opt_key == "belt_support_floor_mode"
|| opt_key == "belt_support_z_offset_mode") {
opt_key == "belt_support_floor_offset") {
osteps.emplace_back(posSupportMaterial);
} else if (
opt_key == "print_sequence"
@@ -1914,14 +1904,6 @@ StringObjectException Print::validate(std::vector<StringObjectException> *warnin
"which is the edge it is meant to anchor. Set the gap to 0 when using leading "
"brim length."),
"brim_object_gap", object->model_object());
// Unconditional: this suppresses the WHOLE belt brim, not just the apron, so a
// user asking for any brim at all needs to be told they are getting none.
if (! object->belt_brim_instances_compatible())
warn(L("This object's copies are spaced along the belt, so they would each need "
"their own brim and none is generated. Print them as separate objects, or "
"arrange the copies side by side across the belt."),
"brim_type", object->model_object());
}
if (this->has_belt_brim() && m_objects.size() > 1)
warn(L("Leading brim length extends ahead of each object along the belt, and Arrange does "
@@ -2107,9 +2089,7 @@ StringObjectException Print::validate(std::vector<StringObjectException> *warnin
bool have_height = false;
if (belt_printer) {
double raw_z = print_object.model_object()->max_z();
if (BeltTransformPipeline::has_preslice_remap(this->config()))
raw_z = BeltTransformPipeline::remap_bbox(*print_object.model_object(), this->config()).size().z();
const double raw_z = print_object.model_object()->max_z();
effective_max_z = raw_z;
have_height = raw_z > 0;
} else {
@@ -3069,11 +3049,7 @@ void Print::process(long long *time_cost_with_cache, bool use_cache)
std::set<PrintObject*> re_slicing_objects;
// Belt global modes couple each object's bed position into its layer Z values,
// so sharing layers between "identical" objects is wrong.
bool belt_no_share = m_config.belt_printer.value &&
((m_config.belt_slice_rotation_global.value
&& m_config.belt_slice_rotation.value != BeltRotationAxis::None)
|| m_config.preslice_remap_global.value
|| m_config.belt_preslice_global.value);
bool belt_no_share = m_config.belt_printer.value;
if (!use_cache) {
for (int index = 0; index < object_count; index++)
{
@@ -3233,7 +3209,8 @@ void Print::process(long long *time_cost_with_cache, bool use_cache)
for (int i = range.begin(); i < range.end(); i++) {
PrintObject* obj = m_objects[i];
if (need_slicing_objects.count(obj) != 0) {
obj->generate_support_material();
// The belt brim follows sequentially below.
obj->generate_support_material(false);
}
else {
if (obj->set_started(posSupportMaterial))
@@ -3242,6 +3219,10 @@ void Print::process(long long *time_cost_with_cache, bool use_cache)
}
}
);
// The belt brim keeps clear of every object's layers and support layers,
// so it runs once no support step is rebuilding them any more.
for (PrintObject *obj : m_objects)
obj->generate_belt_brim();
if (m_pipeline_plugin_active)
for (size_t i = 0; i < m_objects.size(); ++i)
+12 -6
View File
@@ -231,11 +231,10 @@ class ConstSupportLayerPtrsAdaptor : public ConstVectorOfPtrsAdaptor<SupportLaye
ConstSupportLayerPtrsAdaptor(const SupportLayerPtrs *data) : ConstVectorOfPtrsAdaptor<SupportLayer>(data) {}
};
// Returns the model's raw bounding box with pre-slice axis remap applied.
// When no remap is active, returns the unmodified raw_bounding_box().
inline BoundingBoxf3 belt_remapped_bbox(const ModelObject &model_object, const PrintConfig &config)
// The model's raw bounding box, in the frame the belt floor parameters refer to.
inline BoundingBoxf3 belt_remapped_bbox(const ModelObject &model_object, const PrintConfig & /*config*/)
{
return BeltTransformPipeline::remap_bbox(model_object, config);
return model_object.raw_bounding_box();
}
// Single instance of a PrintObject.
@@ -440,7 +439,6 @@ public:
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;
const std::vector<ExtrusionEntityCollection>& belt_brim_by_layer() const { return m_belt_brim_by_layer; }
const std::vector<ExPolygons>& belt_brim_areas_by_layer() const { return m_belt_brim_areas_by_layer; }
const std::vector<BeltBrimBand>& belt_brim_prologue() const { return m_belt_brim_prologue; }
@@ -584,7 +582,13 @@ private:
void ironing();
bool need_z_contouring() const;
void contour_z();
void generate_support_material();
// with_belt_brim = false leaves the belt brim to generate_belt_brim(), for a
// caller that runs the support step of several objects in parallel.
void generate_support_material(bool with_belt_brim = true);
// The belt brim keeps clear of every object's layers and support layers, so
// it has to run after all support steps finished. A no-op unless a support
// step left it pending.
void generate_belt_brim();
void estimate_curled_extrusions();
void simplify_extrusion_path();
@@ -713,6 +717,8 @@ private:
// Belt printer: global Z offset applied to this object's layers for shear positioning.
double m_belt_global_z_offset { 0.0 };
// generate_support_material(false) finished and generate_belt_brim() has not run yet.
bool m_belt_brim_pending { false };
// Belt printer: min_z of mesh after belt shear (before Z-shift), for z_offset calc.
double m_belt_min_z { 0.0 };
// Belt printer: XY correction from global pre-slice mode, applied to G-code origin.
+4 -15
View File
@@ -1842,14 +1842,9 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
for (ModelObject *model_object : m_model.objects) {
ModelObjectStatus &model_object_status = const_cast<ModelObjectStatus&>(model_object_status_db.reuse(*model_object));
// Orca: Updated for XYZ filament shrink compensation
// Belt global mode: force each instance into its own PrintObject
// so each gets independent layer Z values.
bool belt_force_separate = m_config.belt_printer.value && (
(m_config.belt_slice_rotation_global.value
&& m_config.belt_slice_rotation.value != BeltRotationAxis::None
&& std::abs(m_config.belt_slice_rotation_angle.value) > EPSILON)
|| m_config.belt_preslice_global.value
|| (m_config.preslice_remap_global.value && BeltTransformPipeline::has_preslice_remap(m_config)));
// Belt printers: force each instance into its own PrintObject so each
// gets independent layer Z values (its bed position is folded into them).
bool belt_force_separate = m_config.belt_printer.value;
model_object_status.print_instances = print_objects_from_model_object(*model_object, this->shrinkage_compensation(), belt_force_separate);
std::vector<const PrintObjectStatus*> old;
old.reserve(print_object_status_db.count(*model_object));
@@ -1938,13 +1933,7 @@ Print::ApplyStatus Print::apply(const Model &model, DynamicPrintConfig new_full_
// Belt printer global mode: when any object's instances shifted,
// recompute m_belt_global_z_offset for ALL objects (it depends on
// min_shift across all objects, so one move affects everyone).
if (belt_instances_shifted
&& m_config.belt_printer.value
&& ((m_config.belt_slice_rotation_global.value
&& m_config.belt_slice_rotation.value != BeltRotationAxis::None
&& std::abs(m_config.belt_slice_rotation_angle.value) > EPSILON)
|| m_config.belt_preslice_global.value
|| (m_config.preslice_remap_global.value && BeltTransformPipeline::has_preslice_remap(m_config)))) {
if (belt_instances_shifted && m_config.belt_printer.value) {
for (PrintObject *object : m_objects)
update_apply_status(object->invalidate_step(posSlice));
}
+12 -170
View File
@@ -398,31 +398,6 @@ static t_config_enum_values s_keys_map_RemapAxis {
};
CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(RemapAxis)
static t_config_enum_values s_keys_map_BeltSupportFloorMode {
{ "none", int(BeltSupportFloorMode::None) },
{ "generator_only", int(BeltSupportFloorMode::GeneratorOnly) },
};
CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(BeltSupportFloorMode)
static t_config_enum_values s_keys_map_BeltSupportZOffsetMode {
{ "none", int(BeltSupportZOffsetMode::None) },
{ "unconditional", int(BeltSupportZOffsetMode::Unconditional) },
{ "raft_only", int(BeltSupportZOffsetMode::RaftOnly) },
};
CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(BeltSupportZOffsetMode)
static t_config_enum_values s_keys_map_FirstLayerPlaneMode {
{ "auto", int(FirstLayerPlaneMode::Auto) },
{ "xy", int(FirstLayerPlaneMode::XY) },
{ "yz", int(FirstLayerPlaneMode::YZ) },
{ "xz", int(FirstLayerPlaneMode::XZ) },
{ "belt_affine", int(FirstLayerPlaneMode::BeltAffine) },
// Back-compat alias: pre-rotation builds serialised this mode as
// "belt_shear". Accept it on parse so old 3MFs / presets keep loading.
{ "belt_shear", int(FirstLayerPlaneMode::BeltAffine) },
};
CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(FirstLayerPlaneMode)
static t_config_enum_values s_keys_map_SupportMaterialPattern {
{ "rectilinear", smpRectilinear },
{ "rectilinear-grid", smpRectilinearGrid },
@@ -7409,7 +7384,7 @@ void PrintConfigDef::init_fff_params()
def->enum_keys_map = &ConfigOptionEnum<BeltRotationAxis>::get_enum_values();
def->enum_values = {"none", "x", "y", "z"};
def->enum_labels = {L("None"), L("X"), L("Y"), L("Z")};
def->mode = comAdvanced;
def->mode = comDevelop;
def->set_default_value(new ConfigOptionEnum<BeltRotationAxis>(BeltRotationAxis::X));
def = this->add("belt_slice_rotation_angle", coFloat);
@@ -7425,16 +7400,6 @@ void PrintConfigDef::init_fff_params()
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionFloat(45.));
def = this->add("belt_slice_rotation_global", coBool);
def->label = L("Global");
def->category = L("Printable space");
def->tooltip = L("Treat the slicing rotation as part of the global forward transform "
"that BeltBackTransform inverts before the machine-frame remap. "
"Required for rotation-mode belt printers. "
"Defaults to on because virtually all rotation-mode printers need it.");
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionBool(true));
def = this->add("belt_frame_tilt_decouple", coBool);
def->label = L("Decouple machine-frame tilt");
def->category = L("Printable space");
@@ -7458,7 +7423,7 @@ void PrintConfigDef::init_fff_params()
def->mode = comExpert;
def->set_default_value(new ConfigOptionFloat(45.));
// G-code axis remap with sign
// G-code axis remap with sign. Each field is its own row in the settings tab.
auto add_belt_remap = [this](const char *key, const char *label, const char *tooltip,
RemapAxis default_axis, ConfigOptionMode mode = comSimple) {
auto def = this->add(key, coEnum);
@@ -7472,115 +7437,14 @@ void PrintConfigDef::init_fff_params()
def->set_default_value(new ConfigOptionEnum<RemapAxis>(default_axis));
};
add_belt_remap("preslice_remap_x", "X",
"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 "
"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. "
"Default +X: no change.",
RemapAxis::PosX, comDevelop);
add_belt_remap("preslice_remap_y", "Y",
"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 "
"belt surface runs along the Z axis, map Y to +Z here so the slicer slices "
"along the correct plane. Default +Y: no change.",
RemapAxis::PosY, comDevelop);
add_belt_remap("preslice_remap_z", "Z",
"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 "
"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.",
RemapAxis::PosZ, comDevelop);
def = this->add("preslice_remap_global", coBool);
def->label = L("Global");
def->category = L("Printable space");
def->tooltip = L("When enabled, the pre-slice axis remap accounts for each object's bed position. "
"Without this, the remap is applied locally around each object's center, so "
"objects at different positions don't get a position-dependent contribution. "
"Mirrors the 'Global' option on the belt slicing rotation, but for the remap.");
def->mode = comDevelop;
def->set_default_value(new ConfigOptionBool(false));
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, comDevelop);
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, comDevelop);
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, comDevelop);
add_belt_remap("gcode_remap_x", "G-code remap X", "Which slicing axis maps to machine X in G-code output. Applied AFTER slicing, during G-code generation.", RemapAxis::PosX, comDevelop);
add_belt_remap("gcode_remap_y", "G-code remap Y", "Which slicing axis maps to machine Y in G-code output. Applied AFTER slicing, during G-code generation.", RemapAxis::PosY, comDevelop);
add_belt_remap("gcode_remap_z", "G-code remap Z", "Which slicing axis maps to machine Z in G-code output. Applied AFTER slicing, during G-code generation.", RemapAxis::PosZ, comDevelop);
// The machine-frame G-code transform (shear + scale) is no longer configured
// by per-axis keys: it is derived from the belt tilt (belt_slice_rotation axis
// + angle, or belt_frame_tilt_angle when decoupled) in MachineFrameTransform.
def = this->add("gcode_back_transform", coBool);
def->label = L("G-code back-transform");
def->category = L("Printable space");
def->tooltip = L("Undo the pre-slice mesh transform before applying the G-code axis remap "
"and machine-frame shear/scale. Required for the standard belt-printer "
"rotation pipeline.");
def->mode = comExpert;
def->set_default_value(new ConfigOptionBool(true));
def = this->add("belt_preslice_global", coBool);
def->label = L("Global mesh transforms");
def->category = L("Printable space");
def->tooltip = L("When enabled, pre-slice belt transforms (remap, shear, scale) account for "
"each object's bed position, producing correct machine coordinates without "
"relying on origin snap. Each instance gets its own PrintObject.");
def->mode = comExpert;
def->set_default_value(new ConfigOptionBool(true));
// First-layer plane: which surface defines "first layer" for fan / speed /
// accel decisions. On belt printers the slicing-frame layer 0 is a tilted
// slab that no longer corresponds to the physical first printed layer.
// Auto picks BeltAffine when any belt-side affine transform is active
// (Z shear or slicing rotation), otherwise XY (legacy).
def = this->add("first_layer_plane", coEnum);
def->label = L("First layer plane");
def->category = L("Printable space");
def->tooltip = L("Selects the reference plane used to decide which extrusions get "
"first-layer settings (no fan, slow speed, initial-layer accel/jerk, "
"deferred temperature drop). On belt printers a single slicing layer "
"contains paths at many machine-Z values, so layer-index based detection "
"fails. Auto resolves to Belt affine plane when any belt-side affine "
"transform (Z shear or slicing rotation) is active, otherwise XY (legacy). "
"Pick XY explicitly to opt out and force the legacy slicing-layer-0 "
"detection.");
def->enum_keys_map = &ConfigOptionEnum<FirstLayerPlaneMode>::get_enum_values();
def->enum_values = {"auto", "xy", "yz", "xz", "belt_affine"};
def->enum_labels = {L("Auto"), L("XY (machine bed)"), L("YZ"), L("XZ"), L("Belt affine plane")};
def->mode = comExpert;
// Auto, not BeltAffine: BeltAffine activates the plane evaluator unconditionally, so on a
// non-belt printer on_first_layer(point) stopped agreeing with the legacy slicing-layer-0
// test and first-layer speeds were skipped (brim printed at the volumetric fallback rather
// than initial_layer_speed). Auto resolves to BeltAffine only when belt_printer is set with
// a non-zero slicing rotation, and to XY (evaluator inactive, legacy behaviour) otherwise --
// which is what this option's own description promises.
def->set_default_value(new ConfigOptionEnum<FirstLayerPlaneMode>(FirstLayerPlaneMode::Auto));
def = this->add("first_layer_plane_offset", coFloat);
def->label = L("Belt plane offset");
def->category = L("Printable space");
def->tooltip = L("Shifts the first-layer plane along its normal (mm). For axis-aligned "
"planes this is just a coordinate shift. Positive values move the plane "
"away from the belt surface (deeper into the model).");
def->sidetext = L("mm");
def->min = -1000;
def->max = 1000;
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionFloat(0.0));
def = this->add("first_layer_plane_thickness", coFloat);
def->label = L("Plane band thickness");
def->category = L("Printable space");
def->tooltip = L("Thickness of one 'band' relative to the first-layer plane, in mm. "
"Used as the unit by which 'No cooling for the first N layers' (and "
"similar layer-count thresholds) is multiplied when the first-layer "
"plane is active. -1 means use initial_layer_print_height.");
def->sidetext = L("mm");
def->min = -1;
def->max = 100;
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionFloat(-1.0));
// Belt support floor debug controls
def = this->add("belt_support_floor_offset", coFloat);
def->label = L("Support Floor Z offset");
@@ -7592,32 +7456,6 @@ void PrintConfigDef::init_fff_params()
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionFloat(0));
{
auto def = this->add("belt_support_floor_mode", coEnum);
def->label = L("Floor mode");
def->category = L("Printable space");
def->tooltip = L("Controls belt floor awareness for supports. 'None' disables belt floor logic. "
"'Generator only' stops support generation at the belt floor plane.");
def->enum_keys_map = &ConfigOptionEnum<BeltSupportFloorMode>::get_enum_values();
def->enum_values = {"none", "generator_only"};
def->enum_labels = {L("None"), L("Generator only")};
def->mode = comDevelop;
def->set_default_value(new ConfigOptionEnum<BeltSupportFloorMode>(BeltSupportFloorMode::GeneratorOnly));
}
{
auto def = this->add("belt_support_z_offset_mode", coEnum);
def->label = L("Z offset mode");
def->category = L("Printable space");
def->tooltip = L("How global Z offset is applied to support layers for belt printers with global shear. "
"'None' = don't offset. 'Unconditional' = offset all layers. 'Raft only' = only offset raft layers.");
def->enum_keys_map = &ConfigOptionEnum<BeltSupportZOffsetMode>::get_enum_values();
def->enum_values = {"none", "unconditional", "raft_only"};
def->enum_labels = {L("None"), L("Unconditional"), L("Raft only")};
def->mode = comExpert;
def->set_default_value(new ConfigOptionEnum<BeltSupportZOffsetMode>(BeltSupportZOffsetMode::Unconditional));
}
def = this->add("enable_belt_purge_tower", coBool);
def->label = L("Enable belt purge tower");
def->category = L("Multimaterial");
@@ -9551,9 +9389,6 @@ void PrintConfigDef::handle_legacy(t_config_option_key &opt_key, std::string &va
//BBS: handle legacy options
if (opt_key == "curr_bed_type" && value == "SuperTack Plate") {
value = "Supertack Plate";
} else if (opt_key == "belt_support_floor_mode" && (value == "clip_only" || value == "both")) {
// Never implemented; both behaved like "none".
value = "none";
} else if (opt_key == "enable_wipe_tower") {
opt_key = "enable_prime_tower";
} else if (opt_key == "wipe_tower_width") {
@@ -9793,6 +9628,13 @@ void PrintConfigDef::handle_legacy(t_config_option_key &opt_key, std::string &va
"smooth_coefficient", "overhang_totally_speed", "silent_mode",
"overhang_speed_classic",
"anisotropic_surfaces", // superseded by top_surface_fill_order / bottom_surface_fill_order
// Belt printer keys retired before the first release: the global-mode and
// back-transform switches are presumed on, and the pre-slice axis remap, the
// support Z offset mode, the support floor mode (always on) and the first-layer
// plane evaluator were removed.
"belt_slice_rotation_global", "preslice_remap_x", "preslice_remap_y", "preslice_remap_z", "preslice_remap_global",
"belt_support_z_offset_mode", "first_layer_plane", "first_layer_plane_offset",
"belt_preslice_global", "gcode_back_transform", "belt_support_floor_mode", "first_layer_plane_thickness",
};
if (ignore.find(opt_key) != ignore.end()) {
-49
View File
@@ -294,40 +294,6 @@ enum class RemapAxis
RevX = 6, RevY = 7, RevZ = 8, // Reversed: max - pos
};
enum class BeltSupportFloorMode
{
None, // No belt floor awareness
GeneratorOnly, // Only in tree support drop_nodes/contact_points
};
enum class BeltSupportZOffsetMode
{
None, // Don't apply global_z_offset to support layers
Unconditional, // Apply to all support layers
RaftOnly, // Only apply to raft layers
};
// Selects which plane the slicer treats as the "first layer plane" — the
// reference surface used to decide which extrusions get first-layer settings
// (no fan, slow speed, initial-layer accel/jerk, deferred temperature drop).
//
// Auto resolves to:
// - XY (inactive, legacy behavior) for non-belt printers and for belt
// printers with no active belt-side transform.
// - BeltAffine for belt printers with any active belt-side affine
// transform (Z shear, slicing rotation, or both).
//
// XY is also used as an explicit "opt out" mode that forces legacy
// per-layer first-layer detection even on belt printers.
enum class FirstLayerPlaneMode
{
Auto = 0,
XY,
YZ,
XZ,
BeltAffine, // formerly BeltShear; renamed to reflect rotation support
};
enum SupportMaterialPattern {
smpDefault,
smpRectilinear, smpRectilinearGrid, smpHoneycomb,
@@ -774,9 +740,6 @@ CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(IroningType)
CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(SlicingMode)
CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(BeltRotationAxis)
CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(RemapAxis)
CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(BeltSupportFloorMode)
CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(BeltSupportZOffsetMode)
CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(FirstLayerPlaneMode)
CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(SupportMaterialPattern)
CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(SupportMaterialStyle)
CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(SupportMaterialInterfacePattern)
@@ -1898,26 +1861,14 @@ PRINT_CONFIG_CLASS_DERIVED_DEFINE(
// g-code back-transform inverts the rotation before the machine-frame stage.
((ConfigOptionEnum<BeltRotationAxis>, belt_slice_rotation))
((ConfigOptionFloat, belt_slice_rotation_angle))
((ConfigOptionBool, belt_slice_rotation_global))
// Expert override: decouple the machine-frame tilt angle from the pre-slice
// rotation angle. When disabled, the machine frame uses belt_slice_rotation_angle.
((ConfigOptionBool, belt_frame_tilt_decouple))
((ConfigOptionFloat, belt_frame_tilt_angle))
((ConfigOptionEnum<RemapAxis>, preslice_remap_x))
((ConfigOptionEnum<RemapAxis>, preslice_remap_y))
((ConfigOptionEnum<RemapAxis>, preslice_remap_z))
((ConfigOptionBool, preslice_remap_global))
((ConfigOptionEnum<RemapAxis>, gcode_remap_x))
((ConfigOptionEnum<RemapAxis>, gcode_remap_y))
((ConfigOptionEnum<RemapAxis>, gcode_remap_z))
((ConfigOptionBool, gcode_back_transform))
((ConfigOptionBool, belt_preslice_global))
((ConfigOptionEnum<FirstLayerPlaneMode>, first_layer_plane))
((ConfigOptionFloat, first_layer_plane_offset))
((ConfigOptionFloat, first_layer_plane_thickness))
((ConfigOptionFloat, belt_support_floor_offset))
((ConfigOptionEnum<BeltSupportFloorMode>, belt_support_floor_mode))
((ConfigOptionEnum<BeltSupportZOffsetMode>, belt_support_z_offset_mode))
// Width (machine X, across the belt) of the auto-generated belt purge prism.
((ConfigOptionFloat, belt_purge_tower_width))
// Belt-printer-only "type" of purge tower: enables the auto-generated belt
+67 -38
View File
@@ -984,7 +984,7 @@ void PrintObject::detect_overhangs_for_lift()
}
}
void PrintObject::generate_support_material()
void PrintObject::generate_support_material(bool with_belt_brim)
{
if (this->set_started(posSupportMaterial)) {
this->clear_support_layers();
@@ -1032,12 +1032,25 @@ void PrintObject::generate_support_material()
// those must exist before ToolOrdering is built at psWipeTower - one step
// ahead of psSkirtBrim. The brim options already invalidate
// posSupportMaterial, so this needs no extra invalidation edges.
make_belt_brim(*this);
m_print->throw_if_canceled();
m_belt_brim_pending = true;
if (with_belt_brim)
this->generate_belt_brim();
this->set_done(posSupportMaterial);
}
}
void PrintObject::generate_belt_brim()
{
if (! m_belt_brim_pending)
return;
// belt_brim_obstacles() looks up the layers and support layers of every object
// on the plate by print_z. Another object's support step rebuilds those (and
// temporarily shifts its layer Z values), so this must not overlap with it.
make_belt_brim(*this);
m_print->throw_if_canceled();
m_belt_brim_pending = false;
}
void PrintObject::estimate_curled_extrusions()
{
if (this->set_started(posEstimateCurledExtrusions)) {
@@ -1245,8 +1258,6 @@ bool PrintObject::has_belt_brim() const
// this keeps it brimless whatever its config says, so a brim on the parts never blocks purging.
if (m_config.belt_purge_tower_object.value)
return false;
if (! this->belt_brim_instances_compatible())
return false;
if (m_config.brim_type == btNoBrim)
return false;
// An inner-only brim has no leading/extra geometry: leading_brim_length and
@@ -1280,30 +1291,6 @@ unsigned int PrintObject::belt_brim_filament() const
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
// the same height on the belt. Moving an instance ALONG the belt axis changes its
// physical belt-floor Z and would put its brim at the wrong height; moving it ACROSS
// the belt does not, so side-by-side copies are fine.
//
// belt_force_separate() in PrintApply.cpp already gives one instance per PrintObject
// whenever a global belt flag is set, which the shipped belt profiles do - this only
// matters for configurations that do not.
if (m_instances.size() <= 1)
return true;
// From the config, not m_slicing_params: this runs while those can be stale. A tilt
// about Y runs the belt along X, any other tilt along Y (see compute_belt_height_and_floor).
const int axis = m_print->config().belt_slice_rotation.value == BeltRotationAxis::Y ? 0 : 1;
const Point &ref = m_instances.front().shift;
for (const PrintInstance &inst : m_instances) {
const coord_t along = axis == 0 ? inst.shift.x() - ref.x() : inst.shift.y() - ref.y();
if (std::abs(along) > SCALED_EPSILON)
return false;
}
return true;
}
void PrintObject::clear_belt_brim()
{
m_belt_brim_by_layer.clear();
@@ -4154,9 +4141,17 @@ void PrintObject::update_slicing_parameters()
BeltTransformPipeline::BeltFloorParams belt_floor;
const auto &pcfg = this->print()->config();
if (pcfg.belt_printer.value) {
BoundingBoxf3 bb = BeltTransformPipeline::remap_bbox(*this->model_object(), pcfg);
if (BeltTransformPipeline::has_preslice_remap(pcfg))
object_height = bb.size().z();
// The box of the mesh in the frame it is sliced in: XY centred and Z as
// placed on the bed (trafo_centered()). raw_bounding_box() has the
// instance's Z offset removed, and the belt floor is not invariant to a
// Z shift (a point's z and the floor under it move in opposite
// directions under the rotation), so an offset box under-estimates the
// height by twice the shift and the layers stop part way up the object.
BoundingBoxf3 bb;
const Transform3d trafo = this->trafo_centered();
for (const ModelVolume *v : this->model_object()->volumes)
if (v->is_model_part())
bb.merge(v->mesh().transformed_bounding_box(trafo * v->get_matrix()));
auto hr = BeltTransformPipeline::compute_belt_height_and_floor(pcfg, bb, object_height);
object_height = hr.object_height;
belt_floor = hr.floor_params;
@@ -4217,9 +4212,13 @@ SlicingParameters PrintObject::slicing_parameters(const DynamicPrintConfig &full
BoundingBoxf3 bb = model_object.raw_bounding_box();
object_max_z = (float)bb.size().z();
if (print_config.belt_printer.value) {
bb = BeltTransformPipeline::remap_bbox(model_object, print_config);
if (BeltTransformPipeline::has_preslice_remap(print_config))
object_max_z = (float)bb.size().z();
// Z as placed on the bed, XY around the instance origin: the belt floor
// depends on where the box sits in Z (see update_slicing_parameters()).
if (! model_object.instances.empty()) {
bb = model_object.instance_bounding_box(0, false);
const Vec3d off = model_object.instances.front()->get_offset();
bb.translate(-off.x(), -off.y(), 0.);
}
auto hr = BeltTransformPipeline::compute_belt_height_and_floor(print_config, bb, object_max_z);
object_max_z = (float)hr.object_height;
belt_floor = hr.floor_params;
@@ -4746,11 +4745,41 @@ void PrintObject::_generate_support_material()
tree_support.generate();
}
else {
PrintObjectSupportMaterial support_material(this, m_slicing_params);
support_material.generate(*this);
// The normal generator anchors its layer grid at the slicing frame origin
// (SlicingParameters: first layer at first_print_layer_height, raft at
// z = 0), so it has to see the object layers in that frame. On a belt the
// object layers carry the global Z offset (PrintObject::slice()), which is
// negative for the leading half of the belt: a top contact below z = 0
// then turns the intermediate-layer count negative and the generator
// allocates layers until memory runs out. Lift the offset off the object
// layers and the belt floor for the duration of the run and put it back
// on everything, including the new support layers, afterwards (organic
// tree support is shifted the same way below).
const double global_z = m_belt_global_z_offset;
const bool unshift = std::abs(global_z) > EPSILON;
auto shift_object_frame = [this, global_z](double sign) {
for (Layer *layer : m_layers)
layer->print_z += sign * global_z;
m_slicing_params.belt_floor_z_shift += sign * global_z;
};
if (unshift)
shift_object_frame(-1.);
try {
PrintObjectSupportMaterial support_material(this, m_slicing_params);
support_material.generate(*this);
} catch (...) {
if (unshift)
shift_object_frame(1.);
throw;
}
if (unshift) {
shift_object_frame(1.);
for (SupportLayer *sl : m_support_layers)
sl->print_z += global_z;
}
}
// Global Z offset for support layers:
// - Normal support: layers already inherit global_z_offset from object layers.
// - Normal support: generated in the object frame above and shifted afterwards.
// - Non-organic tree support (slim/strong/hybrid): plan_layer_heights() reads
// from globally-offset object layers, so support layers already have it.
// - Organic tree support: generate_tree_support_3D() computes its own Z values
+9 -41
View File
@@ -343,8 +343,7 @@ static std::vector<std::vector<ExPolygons>> slices_to_regions(
// pushes those layers into the parallel_for path below, which handles multi-volume
// clipping per layer without relying on the bbox Z range.
const bool bbox_z_in_layer_frame = !(print_config.belt_printer.value &&
(BeltTransformPipeline::has_rotation(print_config)
|| BeltTransformPipeline::has_preslice_remap(print_config)));
BeltTransformPipeline::has_rotation(print_config));
// Belt-transform addendum: with bbox-Z untrusted, the simple path's
// "first model_part wins" logic drops subsequent volumes' slices unless
// they XY-overlap with the first. Assemblies whose volumes are stacked
@@ -926,14 +925,9 @@ void PrintObject::slice()
// So: belt_floor_z_shift = remapped_bb.min.z() + z_shift_val
if (std::abs(m_slicing_params.belt_floor_shear_factor) > EPSILON) {
double z_shift_val = (m_belt_min_z < 0.) ? -m_belt_min_z : 0.;
// With pre-remap, the belt surface (model_Y=0) may not be at Z=0 in
// centered slicer space — add the remapped bbox min Z to compensate.
// Without pre-remap, the belt surface IS at Z=0 and bb.min.z() is
// already folded into m_belt_min_z, so use 0.
const auto &pcfg = this->print()->config();
double belt_surface_z = BeltTransformPipeline::has_preslice_remap(pcfg)
? BeltTransformPipeline::remap_bbox(*this->model_object(), pcfg).min.z() : 0.;
m_slicing_params.belt_floor_z_shift = belt_surface_z + z_shift_val;
// The belt surface is at Z=0 in centered slicer space and bb.min.z() is
// already folded into m_belt_min_z.
m_slicing_params.belt_floor_z_shift = z_shift_val;
}
int firstLayerReplacedBy = 0;
@@ -982,8 +976,6 @@ void PrintObject::slice()
const auto &pcfg = this->print()->config();
BOOST_LOG_TRIVIAL(trace) << "Belt global check: belt_printer=" << pcfg.belt_printer.value
<< " belt_slice_rotation=" << int(pcfg.belt_slice_rotation.value)
<< " belt_slice_rotation_global=" << pcfg.belt_slice_rotation_global.value
<< " belt_preslice_global=" << pcfg.belt_preslice_global.value
<< " object=" << this->model_object()->name;
if (pcfg.belt_printer.value) {
@@ -1003,8 +995,7 @@ void PrintObject::slice()
// couples slicer_z back into both machine_y and machine_z. Compensating
// layer.print_z by belt_z_shift here makes the back-transform produce
// correct machine-frame coordinates whether or not a global mode is active.
double belt_surface_z = BeltTransformPipeline::has_preslice_remap(pcfg)
? BeltTransformPipeline::remap_bbox(*this->model_object(), pcfg).min.z() : 0.;
const double belt_surface_z = 0.; // the belt surface is Z=0 in centered slicer space
// The compensation must mirror the Z-shift actually applied, which
// is max(0, -m_belt_min_z): when the transformed mesh starts ABOVE
// slicer Z=0 (m_belt_min_z > 0 — possible for counter-rotated or
@@ -1034,9 +1025,11 @@ void PrintObject::slice()
global_z_offset += centering_z_corr;
}
if (pcfg.belt_preslice_global.value) {
{
// Global pre-slice mode: compute full correction c = (T.linear() - I) * d
// where T is the belt forward transform and d is the bed position.
// where T is the belt forward transform and d is the bed position, so
// objects at different bed positions print at different machine Z values
// along the inclined belt.
Transform3d T = BeltTransformPipeline::build_forward_transform(pcfg);
Vec3d d(unscale<double>(inst_shift.x()), unscale<double>(inst_shift.y()), 0.);
Vec3d c = T.linear() * d - d;
@@ -1046,31 +1039,6 @@ void PrintObject::slice()
BOOST_LOG_TRIVIAL(trace) << "Belt preslice_global: correction=("
<< c.x() << ", " << c.y() << ", " << c.z() << ")"
<< " belt_z_shift=" << belt_z_shift << " (m_belt_min_z=" << m_belt_min_z << ")";
} else {
// Slicing rotation in global mode: bed-position-dependent Z offset.
// For R(α, X): c.z = sin(α)*d.y so objects at different bed-Y
// values print at different machine Z values along the inclined belt.
if (pcfg.belt_slice_rotation_global.value
&& pcfg.belt_slice_rotation.value != BeltRotationAxis::None
&& std::abs(pcfg.belt_slice_rotation_angle.value) > EPSILON) {
Transform3d T = BeltTransformPipeline::build_forward_transform(pcfg);
Vec3d d(unscale<double>(inst_shift.x()), unscale<double>(inst_shift.y()), 0.);
Vec3d c = T.linear() * d - d;
global_z_offset += c.z();
m_belt_global_xy_correction = Vec2d(c.x(), c.y());
}
// Pre-slice remap global mode: when on, the remap accounts for the
// instance bed position. The Z component of the correction
// (R - I) * d shifts layer print_z so e.g. a Y↔Z swap with an
// object at Y=50 prints at Z=50.
if (pcfg.preslice_remap_global.value
&& BeltTransformPipeline::has_preslice_remap(pcfg)) {
Transform3d R = BeltTransformPipeline::build_preslice_remap(pcfg);
Vec3d d(unscale<double>(inst_shift.x()), unscale<double>(inst_shift.y()), 0.);
Vec3d remap_correction = R.linear() * d - d;
global_z_offset += remap_correction.z();
}
}
BOOST_LOG_TRIVIAL(trace) << "Belt global: z_offset=" << global_z_offset
+7 -1
View File
@@ -320,7 +320,13 @@ SupportGeneratorLayersPtr generate_raft_base(
// How much to inflate the support columns to be stable. This also applies to the 1st layer, if no raft layers are to be printed.
const float inflate_factor_fine = float(scale_((slicing_params.raft_layers() > 1) ? 0.5 : EPSILON));
const float inflate_factor_1st_layer = std::max(0.f, float(scale_(object.config().raft_first_layer_expansion)) - inflate_factor_fine);
// On a belt the first support layer is the leading tip of the support, a sliver
// where the belt crosses the layer, not a flange on a flat bed: inflating it
// puts lines in the air ahead of the belt crossing (and into the belt behind
// it). The belt brim takes the adhesion role instead.
const bool belt_floor_active = std::abs(slicing_params.belt_floor_shear_factor) > EPSILON;
const float inflate_factor_1st_layer = belt_floor_active ? 0.f :
std::max(0.f, float(scale_(object.config().raft_first_layer_expansion)) - inflate_factor_fine);
SupportGeneratorLayer *contacts = top_contacts .empty() ? nullptr : top_contacts .front();
SupportGeneratorLayer *interfaces = interface_layers .empty() ? nullptr : interface_layers .front();
SupportGeneratorLayer *base_interfaces = base_interface_layers.empty() ? nullptr : base_interface_layers.front();
+1 -4
View File
@@ -2768,8 +2768,7 @@ SupportGeneratorLayersPtr PrintObjectSupportMaterial::bottom_contact_layers_and_
//const auto expansion_to_slice = m_support_material_flow.scaled_spacing() / 2 + 25;
const SupportGridParams grid_params(*m_object_config, m_support_params.support_material_flow);
const bool buildplate_only = ! buildplate_covered.empty();
const bool has_belt_floor = std::abs(m_slicing_params.belt_floor_shear_factor) > EPSILON
&& m_print_config->belt_support_floor_mode.value == BeltSupportFloorMode::GeneratorOnly;
const bool has_belt_floor = std::abs(m_slicing_params.belt_floor_shear_factor) > EPSILON;
// Allocate empty surface areas, one per object layer.
layer_support_areas.assign(object.total_layer_count(), Polygons());
@@ -3311,8 +3310,6 @@ static void trim_support_layers_by_belt_floor(
BeltFloorContext ctx;
if (!ctx.init(slicing_params, print_config))
return;
if (print_config.belt_support_floor_mode.value != BeltSupportFloorMode::GeneratorOnly)
return;
tbb::parallel_for(tbb::blocked_range<size_t>(0, support_layers.size()),
[&](const tbb::blocked_range<size_t> &range) {
+10 -49
View File
@@ -117,58 +117,20 @@ TreeModelVolumes::TreeModelVolumes(
m_increase_until_radius = config.increase_radius_until_radius;
m_radius_0 = config.getRadius(0);
m_raft_layers = config.raft_layers;
// Belt printer: add virtual belt raft layers below the object, matching
// the extra layers added in generate_support_areas() so both use the
// same layer indexing.
{
const auto &sp2 = print_object.slicing_parameters();
const auto &pcfg2 = print_object.print()->config();
double belt_sf = sp2.belt_floor_shear_factor;
if (std::abs(belt_sf) > EPSILON && std::abs(print_object.belt_global_z_offset()) > EPSILON
&& pcfg2.belt_support_floor_mode.value == BeltSupportFloorMode::GeneratorOnly) {
double bb_min_z = std::abs(belt_remapped_bbox(*print_object.model_object(), pcfg2).min.z());
double extra_depth = bb_min_z + 10.;
int num_extra = std::max(0, (int)std::ceil(extra_depth / sp2.layer_height));
if (num_extra > 0) {
std::vector<coordf_t> belt_layers;
belt_layers.reserve(num_extra);
for (int i = num_extra; i >= 1; --i)
belt_layers.push_back(sp2.first_object_layer_height - i * sp2.layer_height);
m_raft_layers.insert(m_raft_layers.begin(), belt_layers.begin(), belt_layers.end());
}
}
}
// Belt floor: add belt surface polygons to anti_overhang so support is
// never generated inside the belt.
// Support blockers are consumed in the same index space as m_layer_outlines
// (object layer i lives at index num_raft_layers + i), but
// slice_support_blockers() returns them in object-layer space. Shift them.
//
// This MUST run after m_raft_layers is final. m_anti_overhang is consumed
// in the same index space as m_layer_outlines -- object layer i lives at
// index num_raft_layers + i -- but slice_support_blockers() returns it in
// object-layer space. Without the shift below, every entry lands
// num_raft_layers too low: with the belt raft that is tens of layers, so
// the belt suppression is applied to the wrong layers entirely and the
// topmost object layers get none at all.
// The belt surface is deliberately NOT a blocker. A blocker is a collision,
// and a branch descending onto a collision slides off it: on a belt that
// walks the branch down the tilted surface, ahead of the part, until it
// reaches the bottom layer floating in mid-air. The belt is where branches
// END: organic_draw_branches() clips their slices with m_belt_floor and the
// first clipped slice is the contact.
{
const size_t num_raft = m_raft_layers.size();
const size_t num_obj = print_object.layer_count();
if (num_raft > 0 && ! m_anti_overhang.empty())
// Shift the support blockers into the same space.
m_anti_overhang.insert(m_anti_overhang.begin(), num_raft, Polygons{});
const auto &sp = print_object.slicing_parameters();
const auto &pcfg = print_object.print()->config();
BeltFloorContext ctx;
ctx.init_local(sp, pcfg, print_object.belt_global_z_offset());
if (ctx.is_active()
&& std::abs(print_object.belt_global_z_offset()) > EPSILON
&& pcfg.belt_support_floor_mode.value == BeltSupportFloorMode::GeneratorOnly) {
if (m_anti_overhang.size() < num_raft + num_obj)
m_anti_overhang.resize(num_raft + num_obj, Polygons{});
for (size_t i = 0; i < num_obj; ++i) {
const double print_z = print_object.get_layer(i)->print_z
- print_object.belt_global_z_offset();
append(m_anti_overhang[num_raft + i], ctx.surface_polygon(print_z));
}
}
}
m_current_outline_idx = 0;
@@ -192,8 +154,7 @@ TreeModelVolumes::TreeModelVolumes(
const auto &pcfg2 = print_object.print()->config();
BeltFloorContext ctx;
ctx.init_local(slicing_params, pcfg2, print_object.belt_global_z_offset());
if (ctx.is_active()
&& pcfg2.belt_support_floor_mode.value == BeltSupportFloorMode::GeneratorOnly) {
if (ctx.is_active()) {
m_belt_floor = ctx.compute_per_layer_floors(num_layers, [&](size_t layer_idx) -> double {
// Object layers: local print_z (subtract global offset).
if (layer_idx >= num_raft_layers)
+37 -149
View File
@@ -716,6 +716,25 @@ double TreeSupport::belt_floor_print_z(const Point &pos_slicing) const
return ctx.floor_print_z(pos_slicing);
}
bool TreeSupport::belt_node_landed(const Point &pos_slicing, double radius, double print_z) const
{
BeltFloorContext ctx;
if (!ctx.init(m_slicing_params, *m_print_config))
return false;
return print_z <= ctx.floor_print_z(pos_slicing) - std::abs(ctx.shear_factor()) * std::max(0., radius);
}
bool TreeSupport::belt_polygon_landed(const ExPolygon &poly, double print_z) const
{
BeltFloorContext ctx;
if (!ctx.init(m_slicing_params, *m_print_config))
return false;
double min_floor = std::numeric_limits<double>::max();
for (const Point &pt : poly.contour.points)
min_floor = std::min(min_floor, ctx.floor_print_z(pt));
return print_z <= min_floor;
}
#define SUPPORT_SURFACES_OFFSET_PARAMETERS jtSquare, 0.
void TreeSupport::detect_overhangs(bool check_support_necessity/* = false*/)
{
@@ -1616,8 +1635,7 @@ void TreeSupport::generate_toolpaths()
// reads as a stray brim/skirt. Gate those layer_id==0 special cases off when
// the belt floor is active; false on non-belt printers so behavior is unchanged.
BeltFloorContext belt_ctx;
const bool belt_floor_active = belt_ctx.init(m_slicing_params, *m_print_config)
&& m_print_config->belt_support_floor_mode.value == BeltSupportFloorMode::GeneratorOnly;
const bool belt_floor_active = belt_ctx.init(m_slicing_params, *m_print_config);
// generate tree support tool paths
tbb::parallel_for(
@@ -1898,133 +1916,6 @@ void TreeSupport::generate()
// Belt floor: extend support below the object's first layer by creating
// additional support layers with geometry copied from the lowest content
// layer and clipped at the belt surface. These layers bypass the tree
// algorithm entirely — they're pure geometry added after draw_circles().
{
BeltFloorContext ctx;
if (ctx.init(m_slicing_params, *m_print_config)
&& m_print_config->belt_support_floor_mode.value == BeltSupportFloorMode::GeneratorOnly) {
const auto &sp = m_slicing_params;
// Find the lowest non-empty, non-brim support layer.
ExPolygons source_areas;
double source_z = 0;
int layers_with_content = 0;
for (size_t i = 0; i < m_object->support_layer_count(); ++i) {
SupportLayer *sl = m_object->get_support_layer(i);
if (sl && !sl->base_areas.empty()) {
layers_with_content++;
if (layers_with_content >= 2) {
source_areas = sl->base_areas;
source_z = sl->print_z;
break;
}
}
}
// Fallback to first content layer.
if (source_areas.empty()) {
for (size_t i = 0; i < m_object->support_layer_count(); ++i) {
SupportLayer *sl = m_object->get_support_layer(i);
if (sl && !sl->base_areas.empty()) {
source_areas = sl->base_areas;
source_z = sl->print_z;
break;
}
}
}
// ORCA-Belt calibration: a counter-rotated calibration object
// stands on a support wedge that lies entirely below the object's
// first layer, where the tree pipeline has no layers at all — so
// no support content can exist yet. Seed the extension directly
// from the floating portion of the first layer (anything more
// than one layer height above the belt floor). For objects whose
// first layer rests on the belt the floating region is empty and
// behavior is unchanged.
double first_z = m_object->support_layer_count() > 0 ? m_object->get_support_layer(0)->print_z : 0.;
bool seeded = false;
if (source_areas.empty() && m_object_config->enable_support.value && !m_object->layers().empty()) {
// The layer grid may start with an empty ghost layer just below
// the object (grid rounding against the belt global Z offset) —
// anchor the seed to the first layer that has geometry. Object
// layer print_z and the floor plane are both in the globally
// offset frame here (belt_floor_z_shift was adjusted alongside
// the layer Z values in PrintObject::slice()).
const Layer *first_layer = nullptr;
for (const Layer *l : m_object->layers())
if (!l->lslices_extrudable.empty()) { first_layer = l; break; }
if (first_layer != nullptr) {
ExPolygons floating = diff_ex(first_layer->lslices_extrudable,
ctx.surface_polygon(first_layer->bottom_z() - first_layer->height));
if (!floating.empty()) {
source_areas = std::move(floating);
first_z = first_layer->bottom_z();
seeded = true;
}
}
}
if (!source_areas.empty()) {
BoundingBoxf3 bb = belt_remapped_bbox(*m_object->model_object(), m_object->print()->config());
double from_extent = std::abs(bb.min(ctx.from_axis()));
double bb_min_z = std::abs(bb.min.z());
// Depth = from-axis extent + pre-shear bbox Z offset (ensure_on_bed
// distance) + 10mm safety margin. The 10mm is a bodge to avoid
// small cutoff artifacts — ideally computed exactly from belt geometry.
double extra_depth = std::min(from_extent + bb_min_z + 10., std::max(0., first_z));
if (seeded) {
// Seeded wedge: the depth is known exactly — down to the lowest
// belt-floor point under the floating footprint. The bbox
// heuristic above under-estimates it for meshes centered
// around their origin (every object loaded through the GUI).
double min_floor = first_z;
for (const ExPolygon &ep : source_areas)
for (const Point &pt : ep.contour.points)
min_floor = std::min(min_floor, ctx.floor_print_z(pt));
extra_depth = std::min(std::max(0., first_z), first_z - min_floor + 2.);
}
int num_extra = std::max(0, (int)std::ceil(extra_depth / sp.layer_height));
// Seeded wedge: top layers become a dense support interface so the
// object's floating first layer bridges a roof, not sparse infill.
const int interface_layers = seeded ? std::max(0, m_object_config->support_interface_top_layers.value) : 0;
ExPolygons prev_areas = source_areas;
// Build belt extension layers (lowest Z first).
SupportLayerPtrs belt_ext_layers;
for (int i = num_extra; i >= 1 && !prev_areas.empty(); --i) {
double print_z = first_z - i * sp.layer_height;
if (print_z < -sp.layer_height) continue;
Polygons belt_surface = ctx.surface_polygon(print_z);
ExPolygons clipped = diff_ex(source_areas, belt_surface);
if (clipped.empty()) continue;
SupportLayer *sl = new SupportLayer(0, 0, m_object, sp.layer_height, print_z, -1);
sl->base_areas = clipped;
// Populate area_groups — generate_toolpaths() iterates these,
// not base_areas directly.
// Note: base areas only get infill when support_base_pattern
// is explicitly set (with the default pattern tree bases are
// walls-only) — the calibration flow sets rectilinear.
const bool roof = i <= interface_layers;
for (auto &expoly : sl->base_areas) {
sl->area_groups.emplace_back(&expoly, roof ? SupportLayer::RoofType : SupportLayer::BaseType, 0);
if (roof)
sl->area_groups.back().interface_id = i & 1;
}
sl->lslices = clipped;
sl->lslices_bboxes.reserve(clipped.size());
for (const ExPolygon &ep : clipped)
sl->lslices_bboxes.emplace_back(get_extents(ep));
belt_ext_layers.push_back(sl);
}
// Insert at the front of support_layers (they're already in Z order).
if (!belt_ext_layers.empty()) {
auto &sl_vec = m_object->support_layers();
sl_vec.insert(sl_vec.begin(), belt_ext_layers.begin(), belt_ext_layers.end());
for (size_t i = 0; i < sl_vec.size(); ++i)
sl_vec[i]->set_id(i);
}
}
}
}
profiler.stage_start(STAGE_GENERATE_TOOLPATHS);
m_object->print()->set_status(70, _u8L("Generating support"));
generate_toolpaths();
@@ -2288,8 +2179,7 @@ void TreeSupport::draw_circles()
// the Z=0 belt plane around the support footprint. false on non-belt printers,
// so behavior there is unchanged.
BeltFloorContext belt_ctx;
const bool belt_floor_active = belt_ctx.init(m_slicing_params, *m_print_config)
&& m_print_config->belt_support_floor_mode.value == BeltSupportFloorMode::GeneratorOnly;
const bool belt_floor_active = belt_ctx.init(m_slicing_params, *m_print_config);
if (m_object->support_layer_count() <= m_raft_layers)
return;
@@ -2484,8 +2374,7 @@ void TreeSupport::draw_circles()
// is 0 so init() and init_local() coincide — this is a no-op there.
{
BeltFloorContext ctx;
if (ctx.init(m_slicing_params, *m_print_config)
&& m_print_config->belt_support_floor_mode.value == BeltSupportFloorMode::GeneratorOnly) {
if (ctx.init(m_slicing_params, *m_print_config)) {
Polygons belt_surface = ctx.surface_polygon(ts_layer->print_z);
base_areas = diff_ex(base_areas, belt_surface);
roof_areas = diff_ex(roof_areas, belt_surface);
@@ -2962,9 +2851,7 @@ 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 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;
const bool has_belt_floor = std::abs(m_slicing_params.belt_floor_shear_factor) > EPSILON;
const size_t bottom_interface_layers = number_of_support_interface_bottom_layers(config);
SupportNode::diameter_angle_scale_factor = diameter_angle_scale_factor;
float DO_NOT_MOVER_UNDER_MM = is_slim ? 0 : 5; // do not move contact points under 5mm
@@ -3200,10 +3087,12 @@ void TreeSupport::drop_nodes()
node_parent = p_node->parent ? p_node : neighbour;
// Make sure the next pass doesn't drop down either of these (since that already happened).
node_parent->merged_neighbours.push_front(node_parent == p_node ? neighbour : p_node);
// Belt floor: don't drop merged node below belt surface.
// 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)) {
// Belt floor: a merged node ends once its whole circle is in the belt
// (its slices are clipped to the belt plane in draw_circles(), so it
// tapers to a tip on the belt). Treat as object-surface termination
// (not buildplate) so the node gets floor/interface areas instead of
// base pads.
if (has_belt_floor && belt_node_landed(next_position, std::max(node.radius, neighbour->radius), print_z_next)) {
std::scoped_lock lock(m_ts_data->m_mutex);
node_parent->to_buildplate = false;
neighbour->valid = false;
@@ -3287,9 +3176,9 @@ void TreeSupport::drop_nodes()
ExPolygons overhangs_next = diff_clipped({ node.overhang }, get_collision(0, obj_layer_nr_next));
for(auto& overhang:overhangs_next) {
Point next_pt = overhang.contour.centroid();
// Belt floor: don't drop polygon node below belt surface.
// Belt floor: a polygon node ends once all of it is in the belt.
// Treat as object-surface termination (not buildplate).
if (has_belt_floor && print_z_next <= belt_floor_print_z(next_pt)) {
if (has_belt_floor && belt_polygon_landed(overhang, print_z_next)) {
p_node->to_buildplate = false;
continue;
}
@@ -3440,9 +3329,11 @@ void TreeSupport::drop_nodes()
if (is_outside) { next_layer_vertex = candidate_vertex; }
}
}
// Belt floor: don't drop regular node below belt surface.
// Belt floor: a node ends once its whole circle is in the belt; until
// then it keeps dropping and draw_circles() clips each layer's circle
// to the belt plane, so the branch tapers to a tip on the belt.
// Treat as object-surface termination (not buildplate).
if (has_belt_floor && print_z_next <= belt_floor_print_z(next_layer_vertex)) {
if (has_belt_floor && belt_node_landed(next_layer_vertex, node.radius, print_z_next)) {
p_node->to_buildplate = false;
return; // from parallel_for_each lambda
}
@@ -3771,9 +3662,7 @@ void TreeSupport::generate_contact_points()
const coordf_t max_bridge_length = scale_(config.max_bridge_length.value);
coord_t radius_scaled = scale_(base_radius);
bool on_buildplate_only = m_object_config->support_on_build_plate_only.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;
const bool has_belt_floor = std::abs(m_slicing_params.belt_floor_shear_factor) > EPSILON;
//First generate grid points to cover the entire area of the print.
BoundingBox bounding_box = m_object->bounding_box();
@@ -4044,8 +3933,7 @@ TreeSupportData::TreeSupportData(const PrintObject &object, coordf_t xy_distance
BeltFloorContext ctx;
double local_print_z = layer->print_z - object.belt_global_z_offset();
if (ctx.init_local(object.slicing_parameters(), object.print()->config(),
object.belt_global_z_offset())
&& object.print()->config().belt_support_floor_mode.value == BeltSupportFloorMode::GeneratorOnly) {
object.belt_global_z_offset())) {
Polygons belt_surface = ctx.surface_polygon(local_print_z);
for (auto &p : belt_surface)
outline.emplace_back(ExPolygon(p));
+7
View File
@@ -463,6 +463,13 @@ private:
// Belt printer: compute the belt floor print_z at a given XY position (in slicing coords).
// Returns -infinity if belt floor is not active.
double belt_floor_print_z(const Point &pos_slicing) const;
// Whether a node's whole circle (radius in mm) sits at or below the belt at
// print_z. The belt is a tilted plane, so the circle's leading edge crosses it
// |shear| * radius lower than its centre; stopping a node when its centre crosses
// would leave that edge floating a radius above the belt.
bool belt_node_landed(const Point &pos_slicing, double radius, double print_z) const;
// The same for a polygon: every point of it is at or below the belt.
bool belt_polygon_landed(const ExPolygon &poly, double print_z) const;
+1 -40
View File
@@ -3462,37 +3462,6 @@ static void generate_support_areas(Print &print, TreeSupport* tree_support, cons
// this struct is used to easy retrieve setting. No other function except those in TreeModelVolumes and generate_initial_areas() have knowledge of the existence of multiple meshes being processed.
//FIXME this is a copy
// Contains config settings to avoid loading them in every function. This was done to improve readability of the code.
// Belt printer: add virtual "belt raft" layers below the object so
// organic branches can extend below the model's first layer and
// terminate at the belt surface instead of creating a flat base at Z=0.
{
PrintObject &po = *print.get_object(processing.second.front());
const auto &sp = po.slicing_parameters();
const auto &pcfg = po.print()->config();
BeltFloorContext ctx;
ctx.init_local(sp, pcfg, po.belt_global_z_offset());
if (ctx.is_active() && std::abs(po.belt_global_z_offset()) > EPSILON
&& pcfg.belt_support_floor_mode.value == BeltSupportFloorMode::GeneratorOnly) {
// z_shift_local is the belt surface height at Y=0 in local coords.
// Extend below the belt so the base expansion and build-plate
// termination happen inside the belt region and get clipped.
// Use the distance from the pre-shear bbox min Z to the part's
// post-shear min Z, plus 10mm for base expansion headroom.
double bb_min_z = std::abs(belt_remapped_bbox(*po.model_object(), pcfg).min.z());
double extra_depth = bb_min_z + 10.;
int num_extra = std::max(0, (int)std::ceil(extra_depth / sp.layer_height));
if (num_extra > 0) {
// Insert belt raft layers at the front, from lowest Z to highest.
std::vector<coordf_t> belt_layers;
belt_layers.reserve(num_extra);
for (int i = num_extra; i >= 1; --i)
belt_layers.push_back(sp.first_object_layer_height - i * sp.layer_height);
// Prepend to existing raft_layers (if any).
auto &rl = processing.first.raft_layers;
rl.insert(rl.begin(), belt_layers.begin(), belt_layers.end());
}
}
}
const TreeSupportSettings &config = processing.first;
BOOST_LOG_TRIVIAL(info) << "Processing support tree mesh group " << counter + 1 << " of " << grouped_meshes.size() << " containing " << grouped_meshes[counter].second.size() << " meshes.";
auto t_start = std::chrono::high_resolution_clock::now();
@@ -3687,8 +3656,7 @@ static void generate_support_areas(Print &print, TreeSupport* tree_support, cons
const auto &pcfg = print_object.print()->config();
BeltFloorContext ctx;
ctx.init_local(sp, pcfg, print_object.belt_global_z_offset());
if (ctx.is_active()
&& pcfg.belt_support_floor_mode.value == BeltSupportFloorMode::GeneratorOnly) {
if (ctx.is_active()) {
tbb::parallel_for_each(layers_sorted.begin(), layers_sorted.end(), [&](SupportGeneratorLayer *layer) {
if (!layer || layer->polygons.empty())
return;
@@ -3984,13 +3952,6 @@ void organic_draw_branches(
const double tiny_area = tiny_area_threshold();
//FIXME parallelize?
for (LayerIndex i = 0; i < LayerIndex(slices.size()); ++i) {
// ORCA: safety offset when trimming collision/bed to improve robustness.
slices[i] = diff_clipped(slices[i], volumes.getCollision(0, layer_begin + i, true), ApplySafetyOffset::Yes); // FIXME parent_uses_min || draw_area.element->state.use_min_xy_dist);
slices[i] = intersection(slices[i], volumes.m_bed_area, ApplySafetyOffset::Yes);
// Belt floor: clip branch slices against the belt surface plane.
LayerIndex belt_idx = layer_begin + i;
if (belt_idx < LayerIndex(volumes.m_belt_floor.size()) && !volumes.m_belt_floor[belt_idx].empty())
slices[i] = diff(slices[i], volumes.m_belt_floor[belt_idx]);
remove_small(slices[i], tiny_area);
}
+14 -13
View File
@@ -36,6 +36,7 @@
#include <tuple>
#include "libslic3r/Preset.hpp"
#include "libslic3r/Config.hpp"
#include <Eigen/Geometry>
#if BOOST_VERSION >= 107800
#include <boost/timer/timer.hpp>
@@ -766,25 +767,25 @@ void Bed3D::render_gravity_arrow(const Transform3d& view_matrix, const Transform
m_gravity_arrow.reset();
return;
}
const Vec3d gravity_dir = -up_dir;
// Build the arrow model (same dimensions as the axis arrows)
if (!m_gravity_arrow.is_initialized()) {
const float stem_length = Axes::DefaultStemLength;
const float tip_radius = Axes::DefaultTipRadius;
const float tip_length = Axes::DefaultTipLength;
const float stem_radius = stem_length / 75.f; // same ratio as axis cylinders
m_gravity_arrow.init_from(stilized_arrow(16, tip_radius, tip_length, stem_radius, stem_length));
// A plain line along the tilted "up" direction -- the way the layers lean, i.e.
// the gantry -- drawn like the bed axes (no tip: the other direction is not
// possible) and shorter than them, so it reads as a hint inside the YZ corner.
const float length = 0.6f * m_axes.get_total_length();
if (!m_gravity_arrow.is_initialized() || m_gravity_arrow_length != length) {
m_gravity_arrow.reset();
m_gravity_arrow.init_from(smooth_cylinder(16, /*Radius*/ length / 75.f, length));
m_gravity_arrow_length = length;
}
// The arrow model points along +Z by default. Compute rotation to align with gravity_dir.
// Rotation axis = cross(+Z, gravity_dir), angle = acos(dot(+Z, gravity_dir))
// The cylinder model points along +Z. Compute the rotation that aligns it with
// up_dir: rotation axis = cross(+Z, up_dir), angle = acos(dot(+Z, up_dir)).
Vec3d from = Vec3d::UnitZ();
Vec3d to = gravity_dir;
Vec3d to = up_dir;
double dot = from.dot(to);
Transform3d rot = Transform3d::Identity();
if (dot < -0.9999) {
// Nearly opposite — rotate 180° around X
// Nearly opposite -- rotate 180 degrees around X
rot = Eigen::AngleAxisd(M_PI, Vec3d::UnitX()) * rot;
} else if (dot < 0.9999) {
Vec3d axis = from.cross(to).normalized();
@@ -800,7 +801,7 @@ void Bed3D::render_gravity_arrow(const Transform3d& view_matrix, const Transform
shader->start_using();
const Camera& camera = wxGetApp().plater()->get_camera();
Transform3d model_matrix = rot;
Transform3d model_matrix = Eigen::Translation3d(m_axes.get_origin()) * rot;
shader->set_uniform("view_model_matrix", camera.get_view_matrix() * model_matrix);
shader->set_uniform("projection_matrix", camera.get_projection_matrix());
+1
View File
@@ -116,6 +116,7 @@ private:
GLModel m_model;
Vec3d m_model_offset{ Vec3d::Zero() };
GLModel m_gravity_arrow;
float m_gravity_arrow_length{ 0.f };
Axes m_axes;
float m_scale_factor{ 1.0f };
+16 -57
View File
@@ -1318,13 +1318,11 @@ std::vector<int> GCodeViewer::get_plater_extruder()
// Belt printers: compute the full machine->model back-transform from the print
// config, so the "designed" (upright) G-code preview maps each toolpath vertex
// back to Cartesian space. The G-code forward pipeline is (BeltKinematics::
// to_machine_coords): gcode = MachineFrame( AxisRemap( X ) ), with X = model if
// gcode_back_transform (write already un-rotated to Cartesian) else BeltForward(
// model). So the inverse is:
// model = [BeltForward^-1 if !gcode_back_transform] . AxisRemap^-1 . MachineFrame^-1
// All parts are config-driven affines -> handles any rotation/shear/scale/axis-
// remap combination. (origin-snap is a per-instance translation that only shifts
// position, not orientation, so it is intentionally omitted.)
// to_machine_coords): gcode = MachineFrame( AxisRemap( X ) ), with X the model
// already un-rotated to Cartesian by the back-transform. So the inverse is:
// model = AxisRemap^-1 . MachineFrame^-1
// (origin-snap is a per-instance translation that only shifts position, not
// orientation, so it is intentionally omitted.)
static Transform3d compute_belt_back_transform(const PrintConfig& cfg)
{
if (!cfg.belt_printer.value)
@@ -1339,10 +1337,9 @@ static Transform3d compute_belt_back_transform(const PrintConfig& cfg)
// build-volume offset for Rev axes). This is the matrix form of the per-point
// GCodeWriter::apply_axis_remap (row convention: each OUTPUT axis selects an input
// axis + sign) and MUST stay in sync with it. The build-volume max matches what the
// writer is fed in GCode.cpp (printable_area max + printable_height). NB: this is the
// transpose of the column convention used by BeltTransformPipeline::build_preslice_remap
// — the two remaps are not interchangeable. (Follow-up: precompute this matrix once in
// GCodeWriter and share it with apply_axis_remap to remove the parallel encoding.)
// writer is fed in GCode.cpp (printable_area max + printable_height). (Follow-up:
// precompute this matrix once in GCodeWriter and share it with apply_axis_remap to
// remove the parallel encoding.)
Transform3d ar = Transform3d::Identity();
const int rr[3] = { int(cfg.gcode_remap_x.value), int(cfg.gcode_remap_y.value), int(cfg.gcode_remap_z.value) };
if (rr[0] != 0 || rr[1] != 1 || rr[2] != 2) {
@@ -1361,11 +1358,7 @@ static Transform3d compute_belt_back_transform(const PrintConfig& cfg)
}
const Transform3d ar_inv = ar.inverse();
Transform3d bf_inv = Transform3d::Identity();
if (!cfg.gcode_back_transform.value)
bf_inv = BeltTransformPipeline::build_forward_transform(cfg).inverse();
return bf_inv * ar_inv * mf_inv;
return ar_inv * mf_inv;
}
//BBS: always load shell at preview
@@ -1394,11 +1387,7 @@ void GCodeViewer::load_as_gcode(const GCodeProcessorResult& gcode_result, const
m_viewer.set_dim_previous_layers_brightness(0.01f * std::stoi(get_app_config()->get("preview_dim_previous_layers_brightness")));
// avoid processing if called with the same gcode_result.
// On a belt printer the toolpath geometry fed to libvgcode also depends on the
// designed/raw view state (the back-transform is applied in convert), so the
// same result is converted again only when that view has been toggled.
const bool same_belt_view = !m_belt_view_enabled || m_last_belt_show_designed == m_belt_show_designed;
if (m_last_result_id == gcode_result.id && wxGetApp().is_editor() && same_belt_view) {
if (m_last_result_id == gcode_result.id && wxGetApp().is_editor()) {
//BBS: add logs
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": the same id %1%, return directly, result %2% ") % m_last_result_id % (&gcode_result);
@@ -1440,22 +1429,20 @@ void GCodeViewer::load_as_gcode(const GCodeProcessorResult& gcode_result, const
}
// convert data from PrusaSlicer format to libvgcode format.
// Belt printers: when the "designed (upright) view" is active, back-transform
// the toolpath geometry into model/Cartesian space using the general belt
// inverse (handles any mesh rotation + shear + axis remap). When off, the raw
// machine-frame G-code is shown (useful for debugging the transform itself).
// Belt printers: back-transform the toolpath geometry into model/Cartesian
// space using the general belt inverse (handles the mesh rotation, shear and
// axis remap), so the part is shown upright, the way it was designed.
const bool is_belt = m_belt_view_enabled && print.config().belt_printer.value;
Transform3d belt_inv = (is_belt && m_belt_show_designed)
? compute_belt_back_transform(print.config()) : Transform3d::Identity();
Transform3d belt_inv = is_belt ? compute_belt_back_transform(print.config()) : Transform3d::Identity();
// Belt: move positions are stored as gcode_Z + belt_z_origin (the start G-code's
// purge-blob advance baked into the machine-Z origin by its G92 Z0 resets). Subtract
// that constant before the linear back-transform so every toolpath maps to the model's
// belt coordinate. Without it the back-transform mixes the offset with the gantry-Y
// term, leaving a per-move designed-Y error that min-corner anchoring cannot remove
// when a bridge/keel move happens to cancel it at the bbox minimum.
if (is_belt && m_belt_show_designed && gcode_result.belt_z_origin != 0.0f)
if (is_belt && gcode_result.belt_z_origin != 0.0f)
belt_inv = belt_inv * Transform3d(Eigen::Translation3d(Vec3d(0.0, 0.0, -double(gcode_result.belt_z_origin))));
const bool apply_belt = is_belt && m_belt_show_designed
const bool apply_belt = is_belt
&& !belt_inv.matrix().isApprox(Transform3d::Identity().matrix());
if (apply_belt) {
// The linear belt back-transform recovers the print's shape and orientation but not
@@ -1692,7 +1679,6 @@ void GCodeViewer::load_as_gcode(const GCodeProcessorResult& gcode_result, const
//BBS: move the id to the end of reset
m_last_result_id = gcode_result.id;
m_last_belt_show_designed = m_belt_show_designed;
m_gcode_result = &gcode_result;
m_move_type_counts.fill(0);
for (auto& move_type_times : m_move_type_times)
@@ -5147,33 +5133,6 @@ void GCodeViewer::render_legend(float &legend_height, int canvas_width, int canv
if (m_nozzle_nums > 1 && (m_viewer.get_view_type() == libvgcode::EViewType::Summary || m_viewer.get_view_type() == libvgcode::EViewType::ColorPrint)) // ORCA show only on summary and filament tab
render_legend_color_arr_recommen(window_padding);
// Belt printer: toggle for viewing designed (upright) vs. machine-frame G-code.
// Rendered with a separator and hint text so users can find it easily.
if (m_belt_view_enabled) {
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
ImGui::Dummy({ window_padding, 0 });
ImGui::SameLine();
ImGui::TextColored(ImVec4(0.f, 0.59f, 0.53f, 1.f), "%s", _u8L("Belt printer").c_str());
ImGui::Dummy({ window_padding, 0 });
ImGui::SameLine();
// Checked = show the raw machine-frame G-code (designed/upright view off). Worded to
// match the canvas-toolbar menu item "Show raw G-code (belt only)". m_belt_show_designed
// is the inverse of this checkbox, so bind a temporary and flip it on change.
bool show_raw = !m_belt_show_designed;
const std::string key = wxGetApp().shortcuts().display(Shortcut::ToggleBeltRawGcode);
const std::string label = _u8L("Show raw G-code (belt only)") + (key.empty() ? std::string() : " [" + key + "]");
if (ImGui::Checkbox(label.c_str(), &show_raw)) {
m_belt_show_designed = !show_raw;
// The designed-view back-transform is baked into the toolpath geometry at load
// time, so the toggle only takes effect once the preview is re-converted. Defer
// the refresh to the next event-loop tick (CallAfter) to avoid re-entering the
// preview load from inside legend rendering.
if (Plater* plater = wxGetApp().plater())
plater->CallAfter([plater]() { plater->refresh_belt_view(); });
}
}
legend_height = ImGui::GetCurrentWindow()->Size.y;
imgui.end();
-6
View File
@@ -199,8 +199,6 @@ private:
std::vector<int> m_plater_extruder;
bool m_gl_data_initialized{ false };
unsigned int m_last_result_id{ 0 };
// Belt printers: the view the loaded result was converted for (see load_as_gcode).
bool m_last_belt_show_designed{ true };
//BBS: save m_gcode_result as well
const GCodeProcessorResult* m_gcode_result;
std::array<unsigned int, static_cast<size_t>(EMoveType::Count)> m_move_type_counts{};
@@ -265,8 +263,6 @@ mutable bool m_no_render_path { false };
bool m_belt_view_enabled = false;
float m_belt_angle_deg = 0.f;
bool m_belt_show_designed = true; // Toggle: designed (upright, back-transformed) view by default;
// turn off (hotkey B) to inspect the raw machine-frame G-code.
libvgcode::Viewer m_viewer;
// ORCA: section view, as the viewer has it. What it cuts away casts no shadow.
@@ -406,8 +402,6 @@ public:
void set_belt_printer(bool enabled, float angle_deg) { m_belt_view_enabled = enabled; m_belt_angle_deg = angle_deg; }
bool is_belt_view() const { return m_belt_view_enabled && m_belt_angle_deg > 0.f; }
void toggle_belt_show_designed() { if (m_belt_view_enabled) m_belt_show_designed = !m_belt_show_designed; }
bool is_belt_show_designed() const { return m_belt_show_designed; }
size_t get_extruders_count() { return m_extruders_count; }
void push_combo_style();
-24
View File
@@ -3800,16 +3800,6 @@ bool GLCanvas3D::handle_shortcut(const KeyChord& chord)
m_dirty = true;
request_extra_frame();
break;
case Shortcut::ToggleBeltRawGcode:
// Same state as the legend checkbox and the canvas-toolbar menu item. The designed-view
// back-transform is baked into the toolpaths at load time, so the preview is re-converted.
if (m_gcode_viewer.is_belt_view()) {
m_gcode_viewer.toggle_belt_show_designed();
if (Plater* plater = wxGetApp().plater())
plater->refresh_belt_view();
m_dirty = true;
}
break;
case Shortcut::ToggleOneLayerMode:
get_gcode_viewer().get_layers_slider()->switch_one_layer_mode();
m_dirty = true;
@@ -10256,20 +10246,6 @@ void GLCanvas3D::_render_canvas_toolbar()
ImGui::TextColored(enable ? ImVec4(1,1,1,1) : ImGui::GetStyleColorVec4(ImGuiCol_TextDisabled), "%s", into_u8(condition ? ImGui::VisibleIcon : ImGui::HiddenIcon).c_str());
};
// Belt printers, G-code preview only: toggle the designed (upright) view vs the raw
// machine-frame G-code. Same state as the shortcut and the legend checkbox; the reload is
// deferred (CallAfter) so the preview is not rebuilt mid-render.
if (m_canvas_type == ECanvasType::CanvasPreview && m_gcode_viewer.is_belt_view()) {
create_menu_item( _utf8(L("Show raw G-code (belt only)")),
true,
!m_gcode_viewer.is_belt_show_designed(), // eye lit = raw machine-frame G-code (designed view off)
[this, p]{
m_gcode_viewer.toggle_belt_show_designed();
p->CallAfter([p]{ p->refresh_belt_view(); });
}
);
ImGui::Separator();
}
create_menu_item( _utf8(L("3D Navigator")),
m_canvas_type != ECanvasType::CanvasAssembleView, // not work on assembly
-11
View File
@@ -369,17 +369,6 @@ void Preview::reload_print(bool only_gcode)
m_only_gcode = only_gcode;
}
void Preview::refresh_belt_view()
{
// Re-run the G-code preview conversion so the belt "designed view" toggle takes effect
// (the back-transform is baked into the toolpath geometry in GCodeViewer::load_as_gcode,
// whose same-result cache also keys on the view state, so the re-convert runs).
// Reset m_loaded_print to bypass the "already loaded" guard the way reload_print does, but
// keep the current layer (Z) range and only-gcode mode so the view doesn't jump on toggle.
m_loaded_print = nullptr;
load_print(true /*keep_z_range*/, m_only_gcode);
}
//BBS: always load shell at preview
void Preview::load_shells(const Print& print, bool force_previewing)
{
-1
View File
@@ -149,7 +149,6 @@ public:
void load_print(bool keep_z_range = false, bool only_gcode = false);
void reload_print(bool only_gcode = false);
// Belt printers: re-convert the G-code preview so the "designed view" toggle takes effect.
void refresh_belt_view();
//BBS: always load shell at preview
void load_shells(const Print& print, bool force_previewing = false);
void reset_shells();
-5
View File
@@ -17885,11 +17885,6 @@ void Plater::reload_print()
p->preview->reload_print();
}
void Plater::refresh_belt_view()
{
p->preview->refresh_belt_view();
}
// BBS
wxString Plater::get_project_name()
{
-1
View File
@@ -388,7 +388,6 @@ public:
// Belt printers: re-run the G-code preview conversion so the "designed view" toggle
// (hotkey B / legend checkbox) takes effect; the back-transform is applied to the
// toolpath geometry at load time. Keeps the current layer range and only-gcode mode.
void refresh_belt_view();
// SoftFever
void calib_pa(const Calib_Params& params);
-1
View File
@@ -149,7 +149,6 @@ constexpr std::array<ShortcutInfo, size_t(Shortcut::Count)> shortcut_table = {{
SHORTCUT(ShowWireframe, "show_wireframe", L("Show/Hide wireframe"), CANVAS, { WXK_RETURN, CTRL_SHIFT }),
SHORTCUT(ToggleGcodeWindow, "toggle_gcode_window", L("On/Off G-code window"), PREVIEW, { 'C' }),
SHORTCUT(ToggleOneLayerMode, "toggle_one_layer_mode", L("On/Off one layer mode of the vertical slider"), PREVIEW, { 'L' }),
SHORTCUT(ToggleBeltRawGcode, "toggle_belt_raw_gcode", L("Show raw G-code (belt only)"), PREVIEW, { 'B' }),
// Application
SHORTCUT(Preferences, "preferences", L("Preferences"), GLOBAL, PREFERENCES_CHORD),
+1 -1
View File
@@ -46,7 +46,7 @@ enum class Shortcut : uint8_t {
// Camera
ViewDefault, ViewTop, ViewBottom, ViewFront, ViewRear, ViewLeft, ViewRight, ViewPlate, ZoomIn, ZoomOut, Mouse3DSettings,
// Display
ShowLabels, ShowWireframe, ToggleGcodeWindow, ToggleOneLayerMode, ToggleBeltRawGcode,
ShowLabels, ShowWireframe, ToggleGcodeWindow, ToggleOneLayerMode,
// Application
Preferences, Search, SwitchView, CollapseSidebar, ReloadDevicePage, KeyboardShortcuts,
// Speed Dial
+22 -86
View File
@@ -5250,77 +5250,24 @@ void TabPrinter::build_fff()
// Belt tilt: the sole mesh-side transform and the single source of truth for
// the physical tilt (drives bed rendering and support gravity tilt too).
// Isometric rotation, no distortion; the back-transform inverts it before the
// machine-frame remap.
{
Line line = { L("Belt tilt"),
L("Belt tilt axis and angle, applied as a mesh rotation before "
"slicing. Also drives bed rendering and support gravity tilt. "
"Isometric (no distortion); the back-transform inverts it before "
"the machine-frame remap.") };
line.label_path = "printer_basic_information_belt_printer#belt-tilt";
line.append_option(belt_og->get_option("belt_slice_rotation"));
line.append_option(belt_og->get_option("belt_slice_rotation_angle"));
line.append_option(belt_og->get_option("belt_slice_rotation_global"));
belt_og->append_line(line);
}
{
Line line = { L("Pre-slice axis remap"),
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, "
"use this to swap axes so layers are stacked in the correct physical direction.") };
line.label_path = "printer_basic_information_belt_printer#pre-slice-axis-remap";
line.append_option(belt_og->get_option("preslice_remap_x"));
line.append_option(belt_og->get_option("preslice_remap_y"));
line.append_option(belt_og->get_option("preslice_remap_z"));
line.append_option(belt_og->get_option("preslice_remap_global"));
belt_og->append_line(line);
}
belt_og->append_single_option_line("belt_preslice_global", "printer_basic_information_belt_printer#global-mesh-transforms");
belt_og->append_single_option_line("gcode_back_transform", "printer_basic_information_belt_printer#g-code-back-transform");
{
Line line = { L("First layer plane"),
L("Reference plane used to decide which extrusions get first-layer "
"settings (no fan, slow speed, deferred temperature drop). On belt "
"printers, Auto resolves to the tilted belt-shear plane so that "
"first-layer treatment follows perpendicular distance from the belt "
"surface, not slicing layer index.") };
line.label_path = "printer_basic_information_belt_printer#first-layer-plane";
line.append_option(belt_og->get_option("first_layer_plane"));
line.append_option(belt_og->get_option("first_layer_plane_offset"));
line.append_option(belt_og->get_option("first_layer_plane_thickness"));
belt_og->append_line(line);
}
// Support floor: split across lines so each setting's own mode controls
// its visibility (floor_mode = Develop, floor_offset = Advanced, z_offset_mode = Expert).
// machine-frame remap. The angle is what a user checks against the machine;
// the axis is a profile-level kinematics choice, so it is Develop-only. They
// are separate rows because a shared line is shown by its first option's mode.
belt_og->append_single_option_line("belt_slice_rotation_angle", "printer_basic_information_belt_printer#tilt-angle");
belt_og->append_single_option_line("belt_slice_rotation", "printer_basic_information_belt_printer#tilt-axis");
belt_og->append_single_option_line("belt_support_floor_offset", "printer_basic_information_belt_printer#support-floor-z-offset");
belt_og->append_single_option_line("belt_support_z_offset_mode", "printer_basic_information_belt_printer#z-offset-mode");
belt_og->append_single_option_line("belt_support_floor_mode", "printer_basic_information_belt_printer#floor-mode");
// Machine-frame transform: the shear (tan) + scale (1/cos) that map
// Machine-frame transform: the shear (cot) + scale (1/sin) that map
// Cartesian G-code into the printer's physical machine frame are derived
// from the belt tilt angle. Only the post-slice axis remap and the expert
// decouple override are exposed here.
// decouple override are exposed here, one option per row.
{
auto mf = page->new_optgroup(L("Machine frame transforms"), L"param_advanced");
{
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.label_path = "printer_basic_information_machine_frame_transforms#g-code-axis-remap";
line.append_option(mf->get_option("gcode_remap_x"));
line.append_option(mf->get_option("gcode_remap_y"));
line.append_option(mf->get_option("gcode_remap_z"));
mf->append_line(line);
}
{
Line line = { L("Machine-frame tilt"),
L("The machine-frame shear (tan) and scale (1/cos) are derived from "
"the belt tilt angle. Enable 'Decouple' to set an independent "
"machine-frame angle when the physical gantry tilt differs from "
"the slicing rotation.") };
line.label_path = "printer_basic_information_machine_frame_transforms#machine-frame-tilt";
line.append_option(mf->get_option("belt_frame_tilt_decouple"));
line.append_option(mf->get_option("belt_frame_tilt_angle"));
mf->append_line(line);
}
mf->append_single_option_line("gcode_remap_x", "printer_basic_information_machine_frame_transforms#g-code-axis-remap");
mf->append_single_option_line("gcode_remap_y", "printer_basic_information_machine_frame_transforms#g-code-axis-remap");
mf->append_single_option_line("gcode_remap_z", "printer_basic_information_machine_frame_transforms#g-code-axis-remap");
mf->append_single_option_line("belt_frame_tilt_decouple", "printer_basic_information_machine_frame_transforms#machine-frame-tilt");
mf->append_single_option_line("belt_frame_tilt_angle", "printer_basic_information_machine_frame_transforms#machine-frame-tilt");
}
option = optgroup->get_option("thumbnails");
@@ -6366,41 +6313,30 @@ void TabPrinter::toggle_options()
bool expert_or_above = (m_mode >= comExpert);
toggle_line("belt_printer_infinite_y", is_belt);
// Belt tilt: the sole mesh-side belt transform (visible by default in belt mode).
toggle_line("belt_slice_rotation_angle", is_belt);
toggle_line("belt_slice_rotation", is_belt);
// Remap, back-transform, and global mesh-transforms toggles are gated by belt
// mode here; finer mode-based visibility is handled by each option's
// ConfigOptionMode in PrintConfig.cpp. Both axis remaps are Develop-only: a
// printer profile sets them once for its kinematics, and a wrong value sends
// ConfigOptionMode in PrintConfig.cpp. The axis remap is Develop-only: a
// printer profile sets it once for its kinematics, and a wrong value sends
// the gantry outside the machine.
for (auto el : {"preslice_remap_x", "gcode_remap_x", "gcode_back_transform"})
for (auto el : {"gcode_remap_x", "gcode_remap_y", "gcode_remap_z"})
toggle_line(el, is_belt);
toggle_line("belt_preslice_global", is_belt);
bool belt_global = is_belt && m_config->opt_bool("belt_preslice_global");
// preslice_remap_global: superseded by belt_preslice_global
toggle_option("preslice_remap_global", is_belt && !belt_global);
// Rotation is the only mesh-side belt transform. Gray out its angle/global
// sub-options when no rotation axis is selected.
// Rotation is the only mesh-side belt transform. Gray out its angle when no
// rotation axis is selected.
auto rot_axis = m_config->option<ConfigOptionEnum<BeltRotationAxis>>("belt_slice_rotation")->value;
toggle_option("belt_slice_rotation_angle", is_belt && rot_axis != BeltRotationAxis::None);
toggle_option("belt_slice_rotation_global", is_belt && rot_axis != BeltRotationAxis::None);
// Machine-frame transform: derived from the belt tilt. Only the expert
// decouple override is exposed; its angle is enabled only when decoupled.
// decouple override is exposed; its angle is shown only when decoupled.
toggle_line("belt_frame_tilt_decouple", is_belt && expert_or_above);
toggle_option("belt_frame_tilt_angle",
is_belt && expert_or_above && m_config->opt_bool("belt_frame_tilt_decouple"));
toggle_line("belt_frame_tilt_angle",
is_belt && expert_or_above && m_config->opt_bool("belt_frame_tilt_decouple"));
// First-layer plane: visible alongside the rest of belt-printer settings.
toggle_line("first_layer_plane", is_belt);
toggle_option("first_layer_plane_offset", is_belt);
toggle_option("first_layer_plane_thickness", is_belt);
for (auto el : {"belt_support_floor_mode", "belt_support_floor_offset", "belt_support_z_offset_mode"})
toggle_line(el, is_belt);
toggle_line("belt_support_floor_offset", is_belt);
const bool support_parallel_printheads = printer_cfg.opt_bool("support_parallel_printheads");
toggle_line("parallel_printheads_count", support_parallel_printheads);
-5
View File
@@ -56,8 +56,6 @@ TEST_CASE("Belt machine coordinates retain a non-45-degree slicing angle", "[GCo
config.belt_printer.value = true;
config.belt_slice_rotation.value = BeltRotationAxis::X;
config.belt_slice_rotation_angle.value = 30.;
config.belt_slice_rotation_global.value = true;
config.gcode_back_transform.value = true;
config.gcode_remap_x.value = RemapAxis::PosX;
config.gcode_remap_y.value = RemapAxis::PosZ;
config.gcode_remap_z.value = RemapAxis::PosY;
@@ -1027,11 +1025,8 @@ SCENARIO("Belt: the first travel does not lift through the uninitialised origin"
// Machine-frame + slicer->world back-transform config (X tilt, 45 deg).
PrintConfig belt_config;
belt_config.belt_printer.value = true;
belt_config.gcode_back_transform.value = true;
belt_config.belt_slice_rotation.value = BeltRotationAxis::X;
belt_config.belt_slice_rotation_angle.value = 45.0;
belt_config.belt_slice_rotation_global.value = true;
belt_config.belt_preslice_global.value = true;
belt_config.belt_frame_tilt_decouple.value = false;
belt_config.belt_frame_tilt_angle.value = 45.0;
-1
View File
@@ -1438,7 +1438,6 @@ TEST_CASE("Belt printers slice Precise Seam modifiers in the frame the object wa
{ "belt_printer", 1 },
{ "belt_slice_rotation", "x" },
{ "belt_slice_rotation_angle", 45 },
{ "belt_slice_rotation_global", 1 },
{ "layer_height", 0.2 },
{ "initial_layer_print_height", 0.2 },
{ "skirt_loops", 0 },
+116 -19
View File
@@ -30,6 +30,10 @@
#include "libslic3r/BuildVolume.hpp"
#include "libslic3r/Support/TreeModelVolumes.hpp"
#include "libslic3r/Support/TreeSupportCommon.hpp"
#include "libslic3r/Support/BeltFloorContext.hpp"
#include "libslic3r/ExtrusionEntity.hpp"
#include "libslic3r/Polyline.hpp"
#include <limits>
#include "libslic3r/Polygon.hpp"
#include "libslic3r/Model.hpp"
#include "libslic3r/GCodeReader.hpp"
@@ -765,7 +769,6 @@ TEST_CASE("Belt printers never start a scarf seam below the layer", "[Print][bel
{ "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" },
@@ -815,7 +818,6 @@ TEST_CASE("Belt printers refuse an object taller than the gantry clearance", "[P
{ "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" },
@@ -851,7 +853,6 @@ TEST_CASE("Belt printers keep the part fan off within the band above the belt",
{ "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" },
@@ -930,7 +931,6 @@ TEST_CASE("Belt printers slice organic tree supports that reach the belt", "[Pri
{ "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" },
@@ -1003,7 +1003,6 @@ static DynamicPrintConfig belt_test_config()
{ "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" },
@@ -1039,30 +1038,19 @@ TEST_CASE("Belt-only keys at non-default values leave non-belt G-code unchanged"
// Every belt key a profile can carry, at a value that would change a belt print.
// belt_printer stays off, so none of them may reach the G-code: the axis remaps are
// gated on belt mode, the rest is only read on belt printers. build_plate_tilt_x/y
// and an explicit first_layer_plane are features of their own on a flat bed and are
// left alone here; "leading_edge_only" prints as an outer brim by design.
// is a feature of its own on a flat bed and is left alone here; "leading_edge_only"
// prints as an outer brim by design.
config.set_deserialize_strict({
{ "belt_printer", 0 },
{ "belt_printer_infinite_y", 0 },
{ "belt_slice_rotation", "y" },
{ "belt_slice_rotation_angle", 30 },
{ "belt_slice_rotation_global", 0 },
{ "belt_preslice_global", 0 },
{ "preslice_remap_x", "pos_x" },
{ "preslice_remap_y", "pos_z" },
{ "preslice_remap_z", "neg_y" },
{ "preslice_remap_global", 1 },
{ "gcode_remap_x", "rev_x" },
{ "gcode_remap_y", "pos_z" },
{ "gcode_remap_z", "pos_y" },
{ "gcode_back_transform", 0 },
{ "belt_frame_tilt_decouple", 1 },
{ "belt_frame_tilt_angle", 30 },
{ "first_layer_plane_offset", 1 },
{ "first_layer_plane_thickness", 1 },
{ "belt_support_floor_offset", -5 },
{ "belt_support_floor_mode", "none" },
{ "belt_support_z_offset_mode", "raft_only" },
{ "enable_belt_purge_tower", 1 },
{ "belt_purge_tower_width", 10 },
{ "leading_brim_length", 10 },
@@ -1136,7 +1124,6 @@ TEST_CASE("A support-only change on a belt purge print matches a fresh slice", "
{ "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" },
@@ -1255,3 +1242,113 @@ TEST_CASE("Organic tree supports place a support blocker at its own height above
CHECK(collides(last + 1));
CHECK(collides(last + num_raft));
}
// A part with an overhang on its LEADING side (the end that prints first) needs
// supports below the object's own lowest slicing layer: the belt under that overhang
// is reached before the object's first contact with it, so the support layers sit at
// a lower slicing Z than any object layer. A generator that stops at the object's
// first layer, or at global Z = 0, leaves those supports floating above the belt.
TEST_CASE("Belt supports reach the belt under a leading overhang", "[Print][belt][Support][Regression]")
{
// default resolves to organic for tree support; tree_hybrid is the classic tree.
const char *support_type = GENERATE("normal(auto)", "tree(auto)");
const char *support_style = GENERATE("default", "organic", "tree_hybrid");
if (std::string(support_type) == "normal(auto)" && std::string(support_style) != "default")
return; // organic and tree_hybrid are tree styles
DYNAMIC_SECTION(support_type << " / " << support_style) {
// A 20 mm cube with a 2 mm thick fin that leaves its top edge and reaches
// 20 mm toward -Y, the end of the part that prints first, climbing at 45 deg
// as it goes (from z = 18 at the cube to z = 38 at the tip). With the layers
// leaning toward -Y at 45 deg the fin's underside is parallel to the layers:
// a ceiling 20 x 28 mm in one layer, with nothing but air between it and the
// belt, which lies up to 41 mm (of slicing Z) below the object's own lowest
// point. Support has to span all of it.
indexed_triangle_set its = its_make_cube(20., 20., 20.);
indexed_triangle_set fin = its_make_cube(20., 20., 2.);
Transform3d shear = Transform3d::Identity();
shear.matrix() << 1., 0., 0., 0.,
0., 1., 0., -20.,
0., -1., 1., 38.,
0., 0., 0., 1.;
its_transform(fin, shear);
its_merge(its, fin);
TriangleMesh mesh(std::move(its));
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
config.set_deserialize_strict({
{ "belt_printer", 1 },
{ "belt_slice_rotation", "x" },
{ "belt_slice_rotation_angle", 45 },
{ "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 },
{ "z_hop", 0 },
{ "enable_support", 1 },
{ "support_type", support_type },
{ "support_style", support_style },
{ "support_threshold_angle", 30 },
{ "machine_start_gcode", "T[initial_tool]\n" },
{ "layer_change_gcode", "G92 E0\n" },
});
Print print;
Model model;
init_print({ mesh }, print, model, config);
// On the bed, not at its corner: organic tree support clips its branches to
// the bed outline, and the fixture leaves the object at the origin.
model.objects.front()->instances.front()->set_offset(Vec3d(100., 100., 0.));
print.apply(model, config);
print.set_status_silent();
print.process();
const PrintObject &object = *print.objects().front();
REQUIRE(! object.layers().empty());
// The whole part is sliced: the layers lean at 45 deg, so the part spans
// (y + z) / sqrt(2) of slicing Z, and every layer in that span has geometry.
{
double lo = std::numeric_limits<double>::max(), hi = std::numeric_limits<double>::lowest();
for (const stl_vertex &v : mesh.its.vertices) {
lo = std::min<double>(lo, v.y() + v.z());
hi = std::max<double>(hi, v.y() + v.z());
}
const double span = (hi - lo) / std::sqrt(2.);
size_t nonempty = 0;
for (const Layer *layer : object.layers())
if (! layer->lslices.empty())
++ nonempty;
INFO("non-empty object layers " << nonempty << ", slicing span " << span << " mm");
CHECK(double(nonempty) * 0.2 > span - 0.6);
}
BeltFloorContext floor;
REQUIRE(floor.init(object.slicing_parameters(), print.config()));
// The lowest support layer that prints anything, and the belt floor beneath it.
const SupportLayer *lowest = nullptr;
for (const SupportLayer *layer : object.support_layers())
if (! layer->support_fills.empty() && (lowest == nullptr || layer->print_z < lowest->print_z))
lowest = layer;
REQUIRE(lowest != nullptr);
double floor_under_lowest = std::numeric_limits<double>::max();
for (const ExtrusionEntity *entity : lowest->support_fills.flatten().entities)
for (const Polyline &pl : entity->as_polylines())
for (const Point &pt : pl.points)
floor_under_lowest = std::min(floor_under_lowest, floor.floor_print_z(pt));
// The object's lowest geometry. The slicing frame starts at the lowest
// belt-floor point under the footprint, so the layers below the leading
// tip of the overhang are empty.
double first_object_z = std::numeric_limits<double>::max();
for (const Layer *layer : object.layers())
if (! layer->lslices.empty()) { first_object_z = layer->print_z; break; }
REQUIRE(first_object_z < std::numeric_limits<double>::max());
INFO("lowest support z " << lowest->print_z << ", floor under it " << floor_under_lowest
<< ", first object layer " << first_object_z);
// Well below the object's own lowest layer (the belt under the tip of the fin
// is ~41 mm of slicing Z below the cube's leading edge, which rests on it)...
CHECK(lowest->print_z < first_object_z - 5.);
// ...and resting on the belt: within a few layers of the floor beneath its own lines.
CHECK(lowest->print_z - floor_under_lowest < 4. * 0.2 + EPSILON);
CHECK(lowest->print_z - floor_under_lowest > -0.2 - EPSILON);
}
}
+39 -57
View File
@@ -641,7 +641,6 @@ static DynamicPrintConfig belt_brim_config()
{ "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" },
@@ -667,7 +666,6 @@ static DynamicPrintConfig belt_brim_multifilament_config(unsigned int filaments,
{ "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" },
@@ -717,9 +715,10 @@ static double first_role_z(const std::string &gcode, const std::string &role)
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").
// Number of object layers that carry a belt brim band. Every band prints at its own
// layer Z, so this equals role_layers(gcode, "brim") (plus any apron bands below the
// first object layer). It is not a pass count: the bands on the empty lead-in layers
// ahead of the object's first contact print back to back, so they fold into one pass.
static int nonempty_belt_brim_layers(const PrintObject &object)
{
int n = 0;
@@ -729,6 +728,30 @@ static int nonempty_belt_brim_layers(const PrintObject &object)
return n;
}
// Number of distinct Z heights at which `role` extrudes: one per layer that prints it.
static int role_layers(const std::string &gcode, const std::string &role)
{
std::set<long> zs;
GCodeReader reader;
reader.parse_buffer(gcode, [&](GCodeReader &self, const GCodeReader::GCodeLine &line) {
if (! line.extruding(self) || line.dist_XY(self) <= EPSILON)
return;
if (line.comment().find(role) != std::string_view::npos)
zs.insert(std::lround(self.z() * 1000.));
});
return int(zs.size());
}
// Apron bands below the object's first layer that print something.
static int belt_brim_apron_bands(const PrintObject &object)
{
int n = 0;
for (const BeltBrimBand &band : object.belt_brim_prologue())
if (! band.fills.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.
@@ -803,12 +826,15 @@ TEST_CASE("Belt brim on an object layer precedes its perimeters, once", "[SkirtB
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());
// Exactly once: every band prints at its own layer Z, so the number of Z heights with
// brim equals the number of bands - not fewer, which a dropped band would give. (A
// double emission would print twice at one Z: the pass count below catches that for
// the bands that sit on layers with perimeters.)
const PrintObject &object = *print.objects().front();
const int bands = nonempty_belt_brim_layers(object);
REQUIRE(bands > 0);
CHECK(role_passes(gc, "brim") == bands);
CHECK(role_layers(gc, "brim") == bands + belt_brim_apron_bands(object));
CHECK(role_passes(gc, "brim") <= bands);
}
// B - single extruder (filament id 1). Every band must survive the 1-based -> 0-based
@@ -828,9 +854,10 @@ TEST_CASE("Belt brim on a single extruder emits every band once", "[SkirtBrim][b
init_print({ cube(20) }, print, model, config);
const std::string gc = gcode(print);
const int expected = nonempty_belt_brim_layers(*print.objects().front());
const PrintObject &object = *print.objects().front();
const int expected = nonempty_belt_brim_layers(object) + belt_brim_apron_bands(object);
REQUIRE(expected > 0);
CHECK(role_passes(gc, "brim") == expected);
CHECK(role_layers(gc, "brim") == expected);
CHECK(belt_tools_for_role(gc, "brim") == std::set<int>{ 0 }); // filament 1 -> tool 0
}
@@ -1233,51 +1260,6 @@ TEST_CASE("Belt apron survives another object printing at the same Z", "[SkirtBr
CHECK(two >= 1.8 * one);
}
TEST_CASE("Belt brim allows instances placed across the belt", "[SkirtBrim][belt]")
{
// Only movement ALONG the belt changes an instance's belt-floor Z, so copies placed
// side by side ACROSS it share one set of bands and must still get a brim. The first
// version of this guard refused every multi-instance object outright, silently
// dropping the brim.
//
// The global belt flags are off here so the instances stay in one PrintObject; with
// them on, PrintApply splits each instance into its own object and the case cannot
// arise at all.
auto multi_instance_has_brim = [](double dx, double dy) {
DynamicPrintConfig config = belt_brim_config();
config.set_deserialize_strict({
{ "belt_slice_rotation_global", 0 },
{ "belt_preslice_global", 0 },
{ "preslice_remap_global", 0 },
{ "brim_type", "outer_only" },
{ "brim_width", 4 },
{ "brim_object_gap", 0 },
});
Print print;
Model model;
ModelObject *object = model.add_object();
object->name += "object.stl";
object->add_volume(cube(20));
object->add_instance()->set_offset(Vec3d(80., 80., 0.));
object->add_instance()->set_offset(Vec3d(80. + dx, 80. + dy, 0.));
object->ensure_on_bed();
print.auto_assign_extruders(object);
print.apply(model, config);
print.validate();
print.set_status_silent();
print.process();
REQUIRE(print.objects().size() == 1);
REQUIRE(print.objects().front()->instances().size() == 2);
return print.objects().front()->has_belt_brim();
};
// X is across the belt when the tilt is about X, since the shear then runs along Y.
CHECK(multi_instance_has_brim(40., 0.));
// Y is along the belt: the copies sit at different belt heights and would each need
// their own bands, so the brim is refused (and validate() warns).
CHECK_FALSE(multi_instance_has_brim(0., 40.));
}
TEST_CASE("Belt brim coexists with support material", "[SkirtBrim][belt]")
{
// Supports put extra layers into the same z stream as the apron bands, which is what